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/07/17 11:38:59 UTC

[incubator-echarts] branch remove-component created (now 3154459)

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

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


      at 3154459  Merge branch 'next' into remove-component

This branch includes the following new commits:

     new 23e84e8  feature: remove the member `dependentModes` from `ComponentModel`. Use `ecMode.getComponent` `ecModel.queryComponents` instead. Because: (1) `dependentModes` is rarely used. (2) It might be wrongly cached. If cached, it might not be updated when new `setOption`. (3) Not necessary to prepare all dependencies model table for each component. Should better to require them when really want to use them.
     new 065cf80  feature: add `setOption` control param: `replaceMerge`.
     new 39891c0  Merge branch 'next' into remove-component
     new 18c2156  ts: remove "any" from type ECUnitOption (which causes "any" spread).
     new fa7cd46  fix: fix replaceMerge in feature "restore" and add test cases.
     new 1db1349  feature: In replaceMerge, trade {xxx: null/undefined} the same as {xxx: []} for ec option.
     new af07980  fix: fix type and tweak component.getReferingComponent.
     new 32a4b66  feature: (1) support axis id reference in dataZoom. (except toolbox dataZoom) (2) enhance dataZoom auto choose axis logic to support multiple setOption. (except toolbox dataZoom) (3) enhance dataZoom to enable axis and dataZoom removal. (except toolbox dataZoom) (4) simplify the code of dataZoom.
     new a3d29cf  feature: add test case for id duplicated.
     new cc81a49  feature: (1) support toolbox dataZoom works on the second setOption. (2) change the mechanism of "internal component" to support adding them dynamically. (3) uniform the "get referring component". (4) support toolbox dataZoom use axis id to refer axis (previously only axisIndex can be used). (5) remove the support to restore on the second setOption temporarily.
     new 4bbe3b2  fix: fix toolbox dataZoom when on grid.
     new f5a9667  fix: (1) Inside dataZoom can not dispose when grid removed. (2) uniform the reference between components.
     new c4d442e  fix: fix noTarget logic for dataZoom.
     new 7606cb7  fix: fix feature reproduce and complete test case.
     new 548bf37  add test case.
     new 3154459  Merge branch 'next' into remove-component

The 16 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



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


[incubator-echarts] 01/16: feature: remove the member `dependentModes` from `ComponentModel`. Use `ecMode.getComponent` `ecModel.queryComponents` instead. Because: (1) `dependentModes` is rarely used. (2) It might be wrongly cached. If cached, it might not be updated when new `setOption`. (3) Not necessary to prepare all dependencies model table for each component. Should better to require them when really want to use them.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 23e84e819a6b6c7e9d83c5c5fb96f0105d1b6a44
Author: 100pah <su...@gmail.com>
AuthorDate: Tue Jul 7 13:51:05 2020 +0800

    feature: remove the member `dependentModes` from `ComponentModel`.
    Use `ecMode.getComponent` `ecModel.queryComponents` instead.
    Because:
    (1) `dependentModes` is rarely used.
    (2) It might be wrongly cached. If cached, it might not be updated when new `setOption`.
    (3) Not necessary to prepare all dependencies model table for each component. Should better to require them when really want to use them.
---
 src/chart/map/MapSeries.ts              |  2 +-
 src/component/dataZoom/DataZoomModel.ts | 16 +++++++---------
 src/coord/parallel/ParallelModel.ts     | 14 +++++++++-----
 src/model/Component.ts                  |  6 ------
 src/model/Global.ts                     | 26 --------------------------
 5 files changed, 17 insertions(+), 47 deletions(-)

diff --git a/src/chart/map/MapSeries.ts b/src/chart/map/MapSeries.ts
index 8861612..d07be64 100644
--- a/src/chart/map/MapSeries.ts
+++ b/src/chart/map/MapSeries.ts
@@ -169,7 +169,7 @@ class MapSeries extends SeriesModel<MapSeriesOption> {
     getHostGeoModel(): GeoModel {
         const geoIndex = this.option.geoIndex;
         return geoIndex != null
-            ? this.dependentModels.geo[geoIndex] as GeoModel
+            ? this.ecModel.getComponent('geo', geoIndex) as GeoModel
             : null;
     }
 
diff --git a/src/component/dataZoom/DataZoomModel.ts b/src/component/dataZoom/DataZoomModel.ts
index f711444..48ec294 100644
--- a/src/component/dataZoom/DataZoomModel.ts
+++ b/src/component/dataZoom/DataZoomModel.ts
@@ -275,7 +275,7 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
         const axisProxies = this._axisProxies;
 
         this.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel, ecModel) {
-            const axisModel = this.dependentModels[dimNames.axis][axisIndex];
+            const axisModel = this.ecModel.getComponent(dimNames.axis, axisIndex);
 
             // If exists, share axisProxy with other dataZoomModels.
             const axisProxy = (axisModel as ExtendedAxisBaseModel).__dzAxisProxy || (
@@ -344,18 +344,17 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
         let autoAxisIndex = true;
         const orient = this.get('orient', true);
         const thisOption = this.option;
-        const dependentModels = this.dependentModels;
 
         if (autoAxisIndex) {
             // Find axis that parallel to dataZoom as default.
             const dimName = orient === 'vertical' ? 'y' : 'x';
 
-            if (dependentModels[dimName + 'Axis'].length) {
+            if (this.ecModel.queryComponents({ mainType: dimName + 'Axis' }).length) {
                 thisOption[dimName + 'AxisIndex' as 'xAxisIndex' | 'yAxisIndex'] = [0];
                 autoAxisIndex = false;
             }
             else {
-                each(dependentModels.singleAxis, function (
+                each(this.ecModel.queryComponents({ mainType: 'singleAxis' }), function (
                     singleAxisModel: AxisBaseModel<{'orient': LayoutOrient} & AxisBaseOption>
                 ) {
                     if (autoAxisIndex && singleAxisModel.get('orient', true) === orient) {
@@ -373,7 +372,7 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
                     return;
                 }
                 const axisIndices = [];
-                const axisModels = this.dependentModels[dimNames.axis];
+                const axisModels = this.ecModel.queryComponents({ mainType: dimNames.axis });
                 if (axisModels.length && !axisIndices.length) {
                     for (let i = 0, len = axisModels.length; i < len; i++) {
                         if (axisModels[i].get('type') === 'category') {
@@ -445,13 +444,12 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
 
     private _isSeriesHasAllAxesTypeOf(seriesModel: SeriesModelOnAxis, axisType: OptionAxisType) {
         // FIXME
-        // 需要series的xAxisIndex和yAxisIndex都首先自动设置上。
-        // 例如series.type === scatter时。
+        // Depends on that series.xAxisIndex series.yAxisIndex are specified.
 
         let is = true;
         eachAxisDim(function (dimNames) {
             const seriesAxisIndex = seriesModel.get(dimNames.axisIndex);
-            const axisModel = this.dependentModels[dimNames.axis][seriesAxisIndex];
+            const axisModel = this.ecModel.getComponent(dimNames.axis, seriesAxisIndex);
 
             if (!axisModel || axisModel.get('type') !== axisType) {
                 is = false;
@@ -505,7 +503,7 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
             if (firstAxisModel == null) {
                 const indices = this.get(dimNames.axisIndex) as number[]; // Has been normalized to array
                 if (indices.length) {
-                    firstAxisModel = this.dependentModels[dimNames.axis][indices[0]] as AxisBaseModel;
+                    firstAxisModel = this.ecModel.getComponent(dimNames.axis, indices[0]) as AxisBaseModel;
                 }
             }
         }, this);
diff --git a/src/coord/parallel/ParallelModel.ts b/src/coord/parallel/ParallelModel.ts
index f07b76b..158b20f 100644
--- a/src/coord/parallel/ParallelModel.ts
+++ b/src/coord/parallel/ParallelModel.ts
@@ -153,11 +153,15 @@ class ParallelModel extends ComponentModel<ParallelCoordinateSystemOption> {
         const dimensions = this.dimensions = [] as DimensionName[];
         const parallelAxisIndex = this.parallelAxisIndex = [] as number[];
 
-        const axisModels = zrUtil.filter(this.dependentModels.parallelAxis, function (axisModel: ParallelAxisModel) {
-            // Can not use this.contains here, because
-            // initialization has not been completed yet.
-            return (axisModel.get('parallelIndex') || 0) === this.componentIndex;
-        }, this);
+        const axisModels = zrUtil.filter(
+            this.ecModel.queryComponents({ mainType: 'parallelAxis' }),
+            function (axisModel: ParallelAxisModel) {
+                // Can not use this.contains here, because
+                // initialization has not been completed yet.
+                return (axisModel.get('parallelIndex') || 0) === this.componentIndex;
+            },
+            this
+        );
 
         zrUtil.each(axisModels, function (axisModel: ParallelAxisModel) {
             dimensions.push('dim' + axisModel.get('dim'));
diff --git a/src/model/Component.ts b/src/model/Component.ts
index 9459f24..f686f0e 100644
--- a/src/model/Component.ts
+++ b/src/model/Component.ts
@@ -108,12 +108,6 @@ class ComponentModel<Opt extends ComponentOption = ComponentOption> extends Mode
      */
     static dependencies: string[];
 
-    /**
-     * key: componentType
-     * value: Component model list, can not be null.
-     * @readOnly
-     */
-    dependentModels: {[componentType: string]: ComponentModel[]} = {};
 
     readonly uid: string;
 
diff --git a/src/model/Global.ts b/src/model/Global.ts
index 0e6da65..572927d 100644
--- a/src/model/Global.ts
+++ b/src/model/Global.ts
@@ -218,10 +218,6 @@ class GlobalModel extends Model<ECUnitOption> {
                 }
             });
 
-            const dependentModels = getComponentsByTypes(
-                componentsMap, dependencies
-            );
-
             option[mainType] = [];
             componentsMap.set(mainType, []);
 
@@ -256,7 +252,6 @@ class GlobalModel extends Model<ECUnitOption> {
                         // PENDING Global as parent ?
                         const extraOpt = extend(
                             {
-                                dependentModels: dependentModels,
                                 componentIndex: index
                             },
                             resultItem.keyInfo
@@ -740,27 +735,6 @@ function mergeTheme(option: ECUnitOption, theme: ThemeOption): void {
     });
 }
 
-/**
- * @param types model types
- */
-function getComponentsByTypes(
-    componentsMap: HashMap<ComponentModel[]>,
-    types: string | string[]
-): {
-    [mainType: string]: ComponentModel[]
-} {
-    if (!isArray(types)) {
-        types = types ? [types] : [];
-    }
-
-    const ret: Dictionary<ComponentModel[]> = {};
-    each(types, function (type) {
-        ret[type] = (componentsMap.get(type) || []).slice();
-    });
-
-    return ret;
-}
-
 function determineSubType(
     mainType: ComponentMainType,
     newCptOption: ComponentOption,


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


[incubator-echarts] 13/16: fix: fix noTarget logic for dataZoom.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit c4d442eb596e71c5e84cc622805db2997066fc7a
Author: 100pah <su...@gmail.com>
AuthorDate: Thu Jul 16 19:13:39 2020 +0800

    fix: fix noTarget logic for dataZoom.
---
 src/component/dataZoom/DataZoomModel.ts | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/src/component/dataZoom/DataZoomModel.ts b/src/component/dataZoom/DataZoomModel.ts
index 38aa4e7..6212aa2 100644
--- a/src/component/dataZoom/DataZoomModel.ts
+++ b/src/component/dataZoom/DataZoomModel.ts
@@ -171,7 +171,7 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
 
     private _targetAxisInfoMap: DataZoomTargetAxisInfoMap;
 
-    private _noTarget: boolean;
+    private _noTarget: boolean = true;
 
     /**
      * It is `[rangeModeForMin, rangeModeForMax]`.
@@ -278,7 +278,12 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
             this._fillAutoTargetAxisByOrient(targetAxisIndexMap, this._orient);
         }
 
-        this._noTarget = !targetAxisIndexMap.keys().length;
+        this._noTarget = true;
+        targetAxisIndexMap.each(function (axisInfo) {
+            if (axisInfo.indexList.length) {
+                this._noTarget = false;
+            }
+        }, this);
     }
 
     private _fillSpecifiedTargetAxis(targetAxisIndexMap: DataZoomTargetAxisInfoMap): boolean {


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


[incubator-echarts] 02/16: feature: add `setOption` control param: `replaceMerge`.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 065cf80c1e7c34c2dec405e81e45f6f339b07e3e
Author: 100pah <su...@gmail.com>
AuthorDate: Tue Jul 7 23:02:06 2020 +0800

    feature: add `setOption` control param: `replaceMerge`.
---
 src/chart/helper/createListFromArray.ts  |   2 +-
 src/chart/treemap/TreemapSeries.ts       |   3 +-
 src/component/graphic.ts                 |   8 +-
 src/component/timeline/TimelineModel.ts  |   6 +-
 src/component/timeline/timelineAction.ts |   2 +-
 src/echarts.ts                           |  36 +-
 src/model/Component.ts                   |   1 +
 src/model/Global.ts                      | 396 ++++++++++++------
 src/model/OptionManager.ts               |  35 +-
 src/util/model.ts                        | 268 +++++++++----
 src/util/types.ts                        |   2 +-
 test/lib/testHelper.js                   |  58 +++
 test/option-replaceMerge.html            | 661 +++++++++++++++++++++++++++++++
 test/option-replaceMerge2.html           | 497 +++++++++++++++++++++++
 test/timeline-dynamic-series.html        | 201 +++++-----
 test/timeline-life.html                  | 279 +++++++++++++
 16 files changed, 2101 insertions(+), 354 deletions(-)

diff --git a/src/chart/helper/createListFromArray.ts b/src/chart/helper/createListFromArray.ts
index d55bf3b..253240f 100644
--- a/src/chart/helper/createListFromArray.ts
+++ b/src/chart/helper/createListFromArray.ts
@@ -47,7 +47,7 @@ function createListFromArray(source: Source | any[], seriesModel: SeriesModel, o
 
     let coordSysDimDefs: DimensionDefinitionLoose[];
 
-    if (coordSysInfo) {
+    if (coordSysInfo && coordSysInfo.coordSysDims) {
         coordSysDimDefs = zrUtil.map(coordSysInfo.coordSysDims, function (dim) {
             const dimInfo = {
                 name: dim
diff --git a/src/chart/treemap/TreemapSeries.ts b/src/chart/treemap/TreemapSeries.ts
index 18b036b..f325f49 100644
--- a/src/chart/treemap/TreemapSeries.ts
+++ b/src/chart/treemap/TreemapSeries.ts
@@ -36,6 +36,7 @@ import {
 import GlobalModel from '../../model/Global';
 import { LayoutRect } from '../../util/layout';
 import List from '../../data/List';
+import { normalizeToArray } from '../../util/model';
 
 // Only support numberic value.
 type TreemapSeriesDataValue = number | number[];
@@ -519,7 +520,7 @@ function completeTreeValue(dataNode: TreemapSeriesNodeItemOption) {
  * set default to level configuration
  */
 function setDefault(levels: TreemapSeriesLevelOption[], ecModel: GlobalModel) {
-    const globalColorList = ecModel.get('color');
+    const globalColorList = normalizeToArray(ecModel.get('color')) as ColorString[];
 
     if (!globalColorList) {
         return;
diff --git a/src/component/graphic.ts b/src/component/graphic.ts
index 7bc0247..1996918 100644
--- a/src/component/graphic.ts
+++ b/src/component/graphic.ts
@@ -142,18 +142,18 @@ const GraphicModel = echarts.extendComponentModel({
         const flattenedList = [];
         this._flatten(newList, flattenedList);
 
-        const mappingResult = modelUtil.mappingToExists(existList, flattenedList);
+        const mappingResult = modelUtil.mappingToExistsInNormalMerge(existList, flattenedList);
         modelUtil.makeIdAndName(mappingResult);
 
         // Clear elOptionsToUpdate
         const elOptionsToUpdate = this._elOptionsToUpdate = [];
 
         zrUtil.each(mappingResult, function (resultItem, index) {
-            const newElOption = resultItem.option;
+            const newElOption = resultItem.newOption;
 
             if (__DEV__) {
                 zrUtil.assert(
-                    zrUtil.isObject(newElOption) || resultItem.exist,
+                    zrUtil.isObject(newElOption) || resultItem.existing,
                     'Empty graphic option definition'
                 );
             }
@@ -502,7 +502,7 @@ function isSetLoc(obj, props) {
 }
 
 function setKeyInfoToNewElOption(resultItem, newElOption) {
-    const existElOption = resultItem.exist;
+    const existElOption = resultItem.existing;
 
     // Set id and type after id assigned.
     newElOption.id = resultItem.keyInfo.id;
diff --git a/src/component/timeline/TimelineModel.ts b/src/component/timeline/TimelineModel.ts
index 11c9982..385854c 100644
--- a/src/component/timeline/TimelineModel.ts
+++ b/src/component/timeline/TimelineModel.ts
@@ -36,7 +36,7 @@ import {
     ZREasing
 } from '../../util/types';
 import Model from '../../model/Model';
-import GlobalModel from '../../model/Global';
+import GlobalModel, { GlobalModelSetOptionOpts } from '../../model/Global';
 import { each, isObject, clone, isString } from 'zrender/src/core/util';
 
 
@@ -121,6 +121,10 @@ export interface TimelineOption extends ComponentOption, BoxLayoutOptionMixin, S
 
     inverse?: boolean
 
+    // If not specified, options will be changed by "normalMerge".
+    // If specified, options will be changed by "replaceMerge".
+    replaceMerge?: GlobalModelSetOptionOpts['replaceMerge']
+
     lineStyle?: TimelineLineStyleOption
     itemStyle?: ItemStyleOption
     checkpointStyle?: TimelineCheckpointStyle
diff --git a/src/component/timeline/timelineAction.ts b/src/component/timeline/timelineAction.ts
index 4e219d4..6972844 100644
--- a/src/component/timeline/timelineAction.ts
+++ b/src/component/timeline/timelineAction.ts
@@ -48,7 +48,7 @@ echarts.registerAction(
         }
 
         // Set normalized currentIndex to payload.
-        ecModel.resetOption('timeline');
+        ecModel.resetOption('timeline', { replaceMerge: timelineModel.get('replaceMerge', true) });
 
         return defaults({
             currentIndex: timelineModel.option.currentIndex
diff --git a/src/echarts.ts b/src/echarts.ts
index 765ee6a..515e041 100644
--- a/src/echarts.ts
+++ b/src/echarts.ts
@@ -27,7 +27,7 @@ import Eventful from 'zrender/src/core/Eventful';
 import Element, { ElementEvent } from 'zrender/src/Element';
 import CanvasPainter from 'zrender/src/canvas/Painter';
 import SVGPainter from 'zrender/src/svg/Painter';
-import GlobalModel, {QueryConditionKindA} from './model/Global';
+import GlobalModel, {QueryConditionKindA, GlobalModelSetOptionOpts} from './model/Global';
 import ExtensionAPI from './ExtensionAPI';
 import CoordinateSystemManager from './CoordinateSystem';
 import OptionManager from './model/OptionManager';
@@ -145,10 +145,13 @@ type ConnectStatus =
     | typeof CONNECT_STATUS_UPDATING
     | typeof CONNECT_STATUS_UPDATED;
 
-type SetOptionOpts = {
-    notMerge?: boolean,
-    lazyUpdate?: boolean,
-    silent?: boolean
+interface SetOptionOpts {
+    notMerge?: boolean;
+    lazyUpdate?: boolean;
+    silent?: boolean;
+    // Rule: only `id` mapped will be merged,
+    // other components of the certain `mainType` will be removed.
+    replaceMerge?: GlobalModelSetOptionOpts['replaceMerge']
 };
 
 type EventMethodName = 'on' | 'off';
@@ -435,9 +438,10 @@ class ECharts extends Eventful {
      * });
      *
      * @param opts opts or notMerge.
-     * @param opts.notMerge Default `false`
+     * @param opts.notMerge Default `false`.
      * @param opts.lazyUpdate Default `false`. Useful when setOption frequently.
      * @param opts.silent Default `false`.
+     * @param opts.replaceMerge Default undefined.
      */
     setOption(option: ECOption, notMerge?: boolean, lazyUpdate?: boolean): void;
     setOption(option: ECOption, opts?: SetOptionOpts): void;
@@ -451,9 +455,11 @@ class ECharts extends Eventful {
         }
 
         let silent;
+        let replaceMerge;
         if (isObject(notMerge)) {
             lazyUpdate = notMerge.lazyUpdate;
             silent = notMerge.silent;
+            replaceMerge = notMerge.replaceMerge;
             notMerge = notMerge.notMerge;
         }
 
@@ -467,7 +473,7 @@ class ECharts extends Eventful {
             ecModel.init(null, null, null, theme, optionManager);
         }
 
-        this._model.setOption(option, optionPreprocessorFuncs);
+        this._model.setOption(option, {replaceMerge: replaceMerge}, optionPreprocessorFuncs);
 
         if (lazyUpdate) {
             this[OPTION_UPDATED] = {silent: silent};
@@ -1190,9 +1196,18 @@ class ECharts extends Eventful {
                 : ecModel.eachSeries(doPrepare);
 
             function doPrepare(model: ComponentModel): void {
+                // By defaut view will be reused if possible for the case that `setOption` with "notMerge"
+                // mode and need to enable transition animation. (Usually, when they have the same id, or
+                // especially no id but have the same type & name & index. See the `model.id` generation
+                // rule in `makeIdAndName` and `viewId` generation rule here).
+                // But in `replaceMerge` mode, this feature should be able to disabled when it is clear that
+                // the new model has nothing to do with the old model.
+                const requireNewView = model.__requireNewView;
+                // This command should not work twice.
+                model.__requireNewView = false;
                 // Consider: id same and type changed.
                 const viewId = '_ec_' + model.id + '_' + model.type;
-                let view = viewMap[viewId];
+                let view = !requireNewView && viewMap[viewId];
                 if (!view) {
                     const classType = parseClassType(model.type);
                     const Clazz = isComponent
@@ -1203,7 +1218,6 @@ class ECharts extends Eventful {
                             // For backward compat, still support a chart type declared as only subType
                             // like "liquidfill", but recommend "series.liquidfill"
                             // But need a base class to make a type series.
-                            // ||
                             (ChartView as ChartViewConstructor).getClass(classType.sub)
                         );
 
@@ -1237,7 +1251,9 @@ class ECharts extends Eventful {
                     zr.remove(view.group);
                     view.dispose(ecModel, api);
                     viewList.splice(i, 1);
-                    delete viewMap[view.__id];
+                    if (viewMap[view.__id] === view) {
+                        delete viewMap[view.__id];
+                    }
                     view.__id = view.group.__ecComponentInfo = null;
                 }
                 else {
diff --git a/src/model/Component.ts b/src/model/Component.ts
index f686f0e..e9c89ce 100644
--- a/src/model/Component.ts
+++ b/src/model/Component.ts
@@ -128,6 +128,7 @@ class ComponentModel<Opt extends ComponentOption = ComponentOption> extends Mode
 
     // Injectable properties:
     __viewId: string;
+    __requireNewView: boolean;
 
     static protoInitialize = (function () {
         const proto = ComponentModel.prototype;
diff --git a/src/model/Global.ts b/src/model/Global.ts
index 572927d..fe9edae 100644
--- a/src/model/Global.ts
+++ b/src/model/Global.ts
@@ -27,14 +27,16 @@
  * (2) In `merge option` mode, if a component has no id/name specified, it
  * will be merged by index, and the result sequence of the components is
  * consistent to the original sequence.
- * (3) `reset` feature (in toolbox). Find detailed info in comments about
+ * (3) In `replaceMerge` mode, keep the result sequence of the components is
+ * consistent to the original sequence, even though there might result in "hole".
+ * (4) `reset` feature (in toolbox). Find detailed info in comments about
  * `mergeOption` in module:echarts/model/OptionManager.
  */
 
 import {__DEV__} from '../config';
 import {
-    each, filter, map, isArray, indexOf, isObject, isString,
-    createHashMap, assert, clone, merge, extend, mixin, HashMap
+    each, filter, isArray, isObject, isString,
+    createHashMap, assert, clone, merge, extend, mixin, HashMap, isFunction
 } from 'zrender/src/core/util';
 import * as modelUtil from '../util/model';
 import Model from './Model';
@@ -55,12 +57,18 @@ import {
 } from '../util/types';
 import OptionManager from './OptionManager';
 import Scheduler from '../stream/Scheduler';
-import { Dictionary } from 'zrender/src/core/types';
+
+export interface GlobalModelSetOptionOpts {
+    replaceMerge: ComponentMainType | ComponentMainType[];
+}
+export interface InnerSetOptionOpts {
+    replaceMergeMainTypeMap: HashMap<boolean>;
+}
 
 // -----------------------
 // Internal method names:
 // -----------------------
-let createSeriesIndices: (ecModel: GlobalModel, seriesModels: ComponentModel[]) => void;
+let reCreateSeriesIndices: (ecModel: GlobalModel) => void;
 let assertSeriesInitialized: (ecModel: GlobalModel) => void;
 let initBase: (ecModel: GlobalModel, baseOption: ECUnitOption) => void;
 
@@ -76,13 +84,23 @@ class GlobalModel extends Model<ECUnitOption> {
     private _componentsMap: HashMap<ComponentModel[]>;
 
     /**
+     * `_componentsMap` might have "hole" becuase of remove.
+     * So save components count for a certain mainType here.
+     */
+    private _componentsCount: HashMap<number>;
+
+    /**
      * Mapping between filtered series list and raw series list.
      * key: filtered series indices, value: raw series indices.
+     * Items of `_seriesIndices` never be null/empty/-1.
+     * If series has been removed by `replaceMerge`, those series
+     * also won't be in `_seriesIndices`, just like be filtered.
      */
     private _seriesIndices: number[];
 
     /**
-     * Key: seriesIndex
+     * Key: seriesIndex.
+     * Keep consistent with `_seriesIndices`.
      */
     private _seriesIndicesMap: HashMap<any>;
 
@@ -103,15 +121,21 @@ class GlobalModel extends Model<ECUnitOption> {
         this._optionManager = optionManager;
     }
 
-    setOption(option: ECOption, optionPreprocessorFuncs: OptionPreprocessor[]): void {
+    setOption(
+        option: ECOption,
+        opts: GlobalModelSetOptionOpts,
+        optionPreprocessorFuncs: OptionPreprocessor[]
+    ): void {
         assert(
             !(OPTION_INNER_KEY in option),
             'please use chart.getOption()'
         );
 
-        this._optionManager.setOption(option, optionPreprocessorFuncs);
+        const innerOpt = normalizeReplaceMergeInput(opts);
+
+        this._optionManager.setOption(option, optionPreprocessorFuncs, innerOpt);
 
-        this.resetOption(null);
+        this._resetOption(null, innerOpt);
     }
 
     /**
@@ -121,7 +145,17 @@ class GlobalModel extends Model<ECUnitOption> {
      *        'media': only reset media query option
      * @return Whether option changed.
      */
-    resetOption(type: string): boolean {
+    resetOption(
+        type: 'recreate' | 'timeline' | 'media',
+        opt?: GlobalModelSetOptionOpts
+    ): boolean {
+        return this._resetOption(type, normalizeReplaceMergeInput(opt));
+    }
+
+    private _resetOption(
+        type: 'recreate' | 'timeline' | 'media',
+        opt: InnerSetOptionOpts
+    ): boolean {
         let optionChanged = false;
         const optionManager = this._optionManager;
 
@@ -133,7 +167,7 @@ class GlobalModel extends Model<ECUnitOption> {
             }
             else {
                 this.restoreData();
-                this.mergeOption(baseOption);
+                this._mergeOption(baseOption, opt);
             }
             optionChanged = true;
         }
@@ -146,7 +180,7 @@ class GlobalModel extends Model<ECUnitOption> {
             const timelineOption = optionManager.getTimelineOption(this);
             if (timelineOption) {
                 optionChanged = true;
-                this.mergeOption(timelineOption);
+                this._mergeOption(timelineOption, opt);
             }
         }
 
@@ -155,7 +189,7 @@ class GlobalModel extends Model<ECUnitOption> {
             if (mediaOptions.length) {
                 each(mediaOptions, function (mediaOption) {
                     optionChanged = true;
-                    this.mergeOption(mediaOption);
+                    this._mergeOption(mediaOption, opt);
                 }, this);
             }
         }
@@ -163,10 +197,19 @@ class GlobalModel extends Model<ECUnitOption> {
         return optionChanged;
     }
 
-    mergeOption(newOption: ECUnitOption): void {
+    public mergeOption(option: ECUnitOption): void {
+        this._mergeOption(option, null);
+    }
+
+    private _mergeOption(
+        newOption: ECUnitOption,
+        opt: InnerSetOptionOpts
+    ): void {
         const option = this.option;
         const componentsMap = this._componentsMap;
-        const newCptTypes: ComponentMainType[] = [];
+        const componentsCount = this._componentsCount;
+        const newCmptTypes: ComponentMainType[] = [];
+        const replaceMergeMainTypeMap = opt && opt.replaceMergeMainTypeMap;
 
         resetSourceDefaulter(this);
 
@@ -184,12 +227,12 @@ class GlobalModel extends Model<ECUnitOption> {
                     : merge(option[mainType], componentOption, true);
             }
             else if (mainType) {
-                newCptTypes.push(mainType);
+                newCmptTypes.push(mainType);
             }
         });
 
         (ComponentModel as ComponentModelConstructor).topologicalTravel(
-            newCptTypes,
+            newCmptTypes,
             (ComponentModel as ComponentModelConstructor).getAllClassMainTypes(),
             visitComponent,
             this
@@ -201,41 +244,48 @@ class GlobalModel extends Model<ECUnitOption> {
             dependencies: string | string[]
         ): void {
 
-            const newCptOptionList = modelUtil.normalizeToArray(newOption[mainType]);
+            const newCmptOptionList = modelUtil.normalizeToArray(newOption[mainType]);
 
-            const mapResult = modelUtil.mappingToExists(
-                componentsMap.get(mainType), newCptOptionList
-            );
+            const oldCmptList = componentsMap.get(mainType);
+            const mapResult = replaceMergeMainTypeMap && replaceMergeMainTypeMap.get(mainType)
+                ? modelUtil.mappingToExistsInReplaceMerge(oldCmptList, newCmptOptionList)
+                : modelUtil.mappingToExistsInNormalMerge(oldCmptList, newCmptOptionList);
 
             modelUtil.makeIdAndName(mapResult);
 
             // Set mainType and complete subType.
             each(mapResult, function (item) {
-                const opt = item.option;
+                const opt = item.newOption;
                 if (isObject(opt)) {
                     item.keyInfo.mainType = mainType;
-                    item.keyInfo.subType = determineSubType(mainType, opt, item.exist);
+                    item.keyInfo.subType = determineSubType(mainType, opt, item.existing);
                 }
             });
 
-            option[mainType] = [];
-            componentsMap.set(mainType, []);
+            // Set it before the travel, in case that `this._componentsMap` is
+            // used in some `init` or `merge` of components.
+            option[mainType] = null;
+            componentsMap.set(mainType, null);
+            componentsCount.set(mainType, 0);
+            const optionsByMainType = [] as ComponentOption[];
+            const cmptsByMainType = [] as ComponentModel[];
+            let cmptsCountByMainType = 0;
 
             each(mapResult, function (resultItem, index) {
-                let componentModel = resultItem.exist;
-                const newCptOption = resultItem.option;
-
-                assert(
-                    isObject(newCptOption) || componentModel,
-                    'Empty component definition'
-                );
-
-                // Consider where is no new option and should be merged using {},
-                // see removeEdgeAndAdd in topologicalTravel and
-                // ComponentModel.getAllClassMainTypes.
-                if (!newCptOption) {
-                    componentModel.mergeOption({}, this);
-                    componentModel.optionUpdated({}, false);
+                let componentModel = resultItem.existing;
+                const newCmptOption = resultItem.newOption;
+
+                if (!newCmptOption) {
+                    if (componentModel) {
+                        // Consider where is no new option and should be merged using {},
+                        // see removeEdgeAndAdd in topologicalTravel and
+                        // ComponentModel.getAllClassMainTypes.
+                        componentModel.mergeOption({}, this);
+                        componentModel.optionUpdated({}, false);
+                    }
+                    // If no both `resultItem.exist` and `resultItem.option`,
+                    // either it is in `replaceMerge` and not matched by any id,
+                    // or it has been removed in previous `replaceMerge` and left a "hole" in this component index.
                 }
                 else {
                     const ComponentModelClass = (ComponentModel as ComponentModelConstructor).getClass(
@@ -245,8 +295,8 @@ class GlobalModel extends Model<ECUnitOption> {
                     if (componentModel && componentModel.constructor === ComponentModelClass) {
                         componentModel.name = resultItem.keyInfo.name;
                         // componentModel.settingTask && componentModel.settingTask.dirty();
-                        componentModel.mergeOption(newCptOption, this);
-                        componentModel.optionUpdated(newCptOption, false);
+                        componentModel.mergeOption(newCmptOption, this);
+                        componentModel.optionUpdated(newCmptOption, false);
                     }
                     else {
                         // PENDING Global as parent ?
@@ -257,32 +307,48 @@ class GlobalModel extends Model<ECUnitOption> {
                             resultItem.keyInfo
                         );
                         componentModel = new ComponentModelClass(
-                            newCptOption, this, this, extraOpt
+                            newCmptOption, this, this, extraOpt
                         );
                         extend(componentModel, extraOpt);
-                        componentModel.init(newCptOption, this, this);
+                        if (resultItem.brandNew) {
+                            componentModel.__requireNewView = true;
+                        }
+                        componentModel.init(newCmptOption, this, this);
 
                         // Call optionUpdated after init.
-                        // newCptOption has been used as componentModel.option
+                        // newCmptOption has been used as componentModel.option
                         // and may be merged with theme and default, so pass null
                         // to avoid confusion.
                         componentModel.optionUpdated(null, true);
                     }
                 }
 
-                componentsMap.get(mainType)[index] = componentModel;
-                option[mainType][index] = componentModel.option;
+                if (componentModel) {
+                    optionsByMainType.push(componentModel.option);
+                    cmptsByMainType.push(componentModel);
+                    cmptsCountByMainType++;
+                }
+                else {
+                    // Always do assign to avoid elided item in array.
+                    optionsByMainType.push(void 0);
+                    cmptsByMainType.push(void 0);
+                }
             }, this);
 
+            option[mainType] = optionsByMainType;
+            componentsMap.set(mainType, cmptsByMainType);
+            componentsCount.set(mainType, cmptsCountByMainType);
+
             // Backup series for filtering.
             if (mainType === 'series') {
-                createSeriesIndices(this, componentsMap.get('series'));
+                reCreateSeriesIndices(this);
             }
         }
 
-        this._seriesIndicesMap = createHashMap<number>(
-            this._seriesIndices = this._seriesIndices || []
-        );
+        // If no series declared, ensure `_seriesIndices` initialized.
+        if (!this._seriesIndices) {
+            reCreateSeriesIndices(this);
+        }
     }
 
     /**
@@ -294,12 +360,22 @@ class GlobalModel extends Model<ECUnitOption> {
         each(option, function (opts, mainType) {
             if ((ComponentModel as ComponentModelConstructor).hasClass(mainType)) {
                 opts = modelUtil.normalizeToArray(opts);
-                for (let i = opts.length - 1; i >= 0; i--) {
+                // Inner cmpts need to be removed.
+                // Inner cmpts might not be at last since ec5.0, but still
+                // compatible for users: if inner cmpt at last, splice the returned array.
+                let realLen = opts.length;
+                let metNonInner = false;
+                for (let i = realLen - 1; i >= 0; i--) {
                     // Remove options with inner id.
-                    if (modelUtil.isIdInner(opts[i])) {
-                        opts.splice(i, 1);
+                    if (opts[i] && !modelUtil.isIdInner(opts[i])) {
+                        metNonInner = true;
+                    }
+                    else {
+                        opts[i] = null;
+                        !metNonInner && realLen--;
                     }
                 }
+                opts.length = realLen;
                 option[mainType] = opts;
             }
         });
@@ -323,51 +399,41 @@ class GlobalModel extends Model<ECUnitOption> {
         }
     }
 
+    /**
+     * @return Never be null/undefined.
+     */
     queryComponents(condition: QueryConditionKindB): ComponentModel[] {
         const mainType = condition.mainType;
         if (!mainType) {
             return [];
         }
 
-        let index = condition.index;
+        const index = condition.index;
         const id = condition.id;
         const name = condition.name;
+        const cmpts = this._componentsMap.get(mainType);
 
-        const cpts = this._componentsMap.get(mainType);
-
-        if (!cpts || !cpts.length) {
+        if (!cmpts || !cmpts.length) {
             return [];
         }
 
-        let result;
+        let result: ComponentModel[];
 
         if (index != null) {
-            if (!isArray(index)) {
-                index = [index];
-            }
-            result = filter(map(index, function (idx) {
-                return cpts[idx];
-            }), function (val) {
-                return !!val;
+            result = [];
+            each(modelUtil.normalizeToArray(index), function (idx) {
+                cmpts[idx] && result.push(cmpts[idx]);
             });
         }
         else if (id != null) {
-            const isIdArray = isArray(id);
-            result = filter(cpts, function (cpt) {
-                return (isIdArray && indexOf(id as string[], cpt.id) >= 0)
-                    || (!isIdArray && cpt.id === id);
-            });
+            result = queryByIdOrName('id', id, cmpts);
         }
         else if (name != null) {
-            const isNameArray = isArray(name);
-            result = filter(cpts, function (cpt) {
-                return (isNameArray && indexOf(name as string[], cpt.name) >= 0)
-                    || (!isNameArray && cpt.name === name);
-            });
+            result = queryByIdOrName('name', name, cmpts);
         }
         else {
-            // Return all components with mainType
-            result = cpts.slice();
+            // Return all non-empty components in that mainType
+            result = filter(cmpts, cmpt => !!cmpt);
         }
 
         return filterBySubType(result, condition);
@@ -397,7 +463,8 @@ class GlobalModel extends Model<ECUnitOption> {
         const queryCond = getQueryCond(query);
         const result = queryCond
             ? this.queryComponents(queryCond)
-            : this._componentsMap.get(mainType);
+            // Retrieve all non-empty components.
+            : filter(this._componentsMap.get(mainType), cmpt => !!cmpt);
 
         return doFilter(filterBySubType(result, condition));
 
@@ -428,6 +495,8 @@ class GlobalModel extends Model<ECUnitOption> {
     }
 
     /**
+     * Travel components (before filtered).
+     *
      * @usage
      * eachComponent('legend', function (legendModel, index) {
      *     ...
@@ -466,31 +535,44 @@ class GlobalModel extends Model<ECUnitOption> {
     ) {
         const componentsMap = this._componentsMap;
 
-        if (typeof mainType === 'function') {
-            const contextReal = cb as T;
-            const cbReal = mainType as EachComponentAllCallback;
-            componentsMap.each(function (components, componentType) {
-                each(components, function (component, index) {
-                    cbReal.call(contextReal, componentType, component, index);
-                });
+        if (isFunction(mainType)) {
+            const ctxForAll = cb as T;
+            const cbForAll = mainType as EachComponentAllCallback;
+            componentsMap.each(function (cmpts, componentType) {
+                for (let i = 0; cmpts && i < cmpts.length; i++) {
+                    const cmpt = cmpts[i];
+                    cmpt && cbForAll.call(ctxForAll, componentType, cmpt, cmpt.componentIndex);
+                }
             });
         }
-        else if (isString(mainType)) {
-            each(componentsMap.get(mainType), cb as EachComponentInMainTypeCallback, context);
-        }
-        else if (isObject(mainType)) {
-            const queryResult = this.findComponents(mainType);
-            each(queryResult, cb as EachComponentInMainTypeCallback, context);
+        else {
+            const cmpts = isString(mainType)
+                ? componentsMap.get(mainType)
+                : isObject(mainType)
+                ? this.findComponents(mainType)
+                : null;
+            for (let i = 0; cmpts && i < cmpts.length; i++) {
+                const cmpt = cmpts[i];
+                cmpt && (cb as EachComponentInMainTypeCallback).call(
+                    context, cmpt, cmpt.componentIndex
+                );
+            }
         }
     }
 
+    /**
+     * Get series list before filtered by name.
+     */
     getSeriesByName(name: string): SeriesModel[] {
-        const series = this._componentsMap.get('series') as SeriesModel[];
-        return filter(series, function (oneSeries) {
-            return oneSeries.name === name;
-        });
+        return filter(
+            this._componentsMap.get('series') as SeriesModel[],
+            oneSeries => !!oneSeries && oneSeries.name === name
+        );
     }
 
+    /**
+     * Get series list before filtered by index.
+     */
     getSeriesByIndex(seriesIndex: number): SeriesModel {
         return this._componentsMap.get('series')[seriesIndex] as SeriesModel;
     }
@@ -500,18 +582,27 @@ class GlobalModel extends Model<ECUnitOption> {
      * FIXME: rename to getRawSeriesByType?
      */
     getSeriesByType(subType: ComponentSubType): SeriesModel[] {
-        const series = this._componentsMap.get('series') as SeriesModel[];
-        return filter(series, function (oneSeries) {
-            return oneSeries.subType === subType;
-        });
+        return filter(
+            this._componentsMap.get('series') as SeriesModel[],
+            oneSeries => !!oneSeries && oneSeries.subType === subType
+        );
     }
 
+    /**
+     * Get all series before filtered.
+     */
     getSeries(): SeriesModel[] {
-        return this._componentsMap.get('series').slice() as SeriesModel[];
+        return filter(
+            this._componentsMap.get('series').slice() as SeriesModel[],
+            oneSeries => !!oneSeries
+        );
     }
 
+    /**
+     * Count series before filtered.
+     */
     getSeriesCount(): number {
-        return this._componentsMap.get('series').length;
+        return this._componentsCount.get('series');
     }
 
     /**
@@ -539,7 +630,9 @@ class GlobalModel extends Model<ECUnitOption> {
         cb: (this: T, series: SeriesModel, rawSeriesIndex: number) => void,
         context?: T
     ): void {
-        each(this._componentsMap.get('series'), cb, context);
+        each(this._componentsMap.get('series'), function (series) {
+            series && cb.call(context, series, series.componentIndex);
+        });
     }
 
     /**
@@ -573,7 +666,7 @@ class GlobalModel extends Model<ECUnitOption> {
 
     isSeriesFiltered(seriesModel: SeriesModel): boolean {
         assertSeriesInitialized(this);
-        return this._seriesIndicesMap.get(seriesModel.componentIndex + '') == null;
+        return this._seriesIndicesMap.get(seriesModel.componentIndex) == null;
     }
 
     getCurrentSeriesIndices(): number[] {
@@ -585,17 +678,22 @@ class GlobalModel extends Model<ECUnitOption> {
         context?: T
     ): void {
         assertSeriesInitialized(this);
-        const filteredSeries = filter(
-            this._componentsMap.get('series') as SeriesModel[], cb, context
-        );
-        createSeriesIndices(this, filteredSeries);
+
+        const newSeriesIndices: number[] = [];
+        each(this._seriesIndices, function (seriesRawIdx) {
+            const series = this._componentsMap.get('series')[seriesRawIdx] as SeriesModel;
+            cb.call(context, series, seriesRawIdx) && newSeriesIndices.push(seriesRawIdx);
+        }, this);
+
+        this._seriesIndices = newSeriesIndices;
+        this._seriesIndicesMap = createHashMap(newSeriesIndices);
     }
 
     restoreData(payload?: Payload): void {
-        const componentsMap = this._componentsMap;
 
-        createSeriesIndices(this, componentsMap.get('series'));
+        reCreateSeriesIndices(this);
 
+        const componentsMap = this._componentsMap;
         const componentTypes: string[] = [];
         componentsMap.each(function (components, componentType) {
             componentTypes.push(componentType);
@@ -604,10 +702,16 @@ class GlobalModel extends Model<ECUnitOption> {
         (ComponentModel as ComponentModelConstructor).topologicalTravel(
             componentTypes,
             (ComponentModel as ComponentModelConstructor).getAllClassMainTypes(),
-            function (componentType, dependencies) {
+            function (componentType) {
                 each(componentsMap.get(componentType), function (component) {
-                    (componentType !== 'series' || !isNotTargetSeries(component as SeriesModel, payload))
-                        && component.restoreData();
+                    if (component
+                        && (
+                            componentType !== 'series'
+                            || !isNotTargetSeries(component as SeriesModel, payload)
+                        )
+                    ) {
+                        component.restoreData();
+                    }
                 });
             }
         );
@@ -615,12 +719,13 @@ class GlobalModel extends Model<ECUnitOption> {
 
     private static internalField = (function () {
 
-        createSeriesIndices = function (ecModel: GlobalModel, seriesModels: ComponentModel[]): void {
-            ecModel._seriesIndicesMap = createHashMap(
-                ecModel._seriesIndices = map(seriesModels, function (series) {
-                    return series.componentIndex;
-                }) || []
-            );
+        reCreateSeriesIndices = function (ecModel: GlobalModel): void {
+            const seriesIndices: number[] = ecModel._seriesIndices = [];
+            each(ecModel._componentsMap.get('series'), function (series) {
+                // series may have been removed by `replaceMerge`.
+                series && seriesIndices.push(series.componentIndex);
+            });
+            ecModel._seriesIndicesMap = createHashMap(seriesIndices);
         };
 
         assertSeriesInitialized = function (ecModel: GlobalModel): void {
@@ -644,13 +749,14 @@ class GlobalModel extends Model<ECUnitOption> {
             // Init with series: [], in case of calling findSeries method
             // before series initialized.
             ecModel._componentsMap = createHashMap({series: []});
+            ecModel._componentsCount = createHashMap();
 
             mergeTheme(baseOption, ecModel._theme.option);
 
             // TODO Needs clone when merging to the unexisted property
             merge(baseOption, globalDefault, false);
 
-            ecModel.mergeOption(baseOption);
+            ecModel._mergeOption(baseOption, null);
         };
 
     })();
@@ -692,10 +798,10 @@ export interface QueryConditionKindB {
     name?: string | string[];
 }
 export interface EachComponentAllCallback {
-    (mainType: string, model: ComponentModel, index: number): void;
+    (mainType: string, model: ComponentModel, componentIndex: number): void;
 }
 interface EachComponentInMainTypeCallback {
-    (model: ComponentModel, index: number): void;
+    (model: ComponentModel, componentIndex: number): void;
 }
 
 
@@ -719,7 +825,8 @@ function mergeTheme(option: ECUnitOption, theme: ThemeOption): void {
         if (name === 'colorLayer' && notMergeColorLayer) {
             return;
         }
-        // 如果有 component model 则把具体的 merge 逻辑交给该 model 处理
+        // If it is component model mainType, the model handles that merge later.
+        // otherwise, merge them here.
         if (!(ComponentModel as ComponentModelConstructor).hasClass(name)) {
             if (typeof themeItem === 'object') {
                 option[name] = !option[name]
@@ -737,20 +844,34 @@ function mergeTheme(option: ECUnitOption, theme: ThemeOption): void {
 
 function determineSubType(
     mainType: ComponentMainType,
-    newCptOption: ComponentOption,
+    newCmptOption: ComponentOption,
     existComponent: {subType: ComponentSubType} | ComponentModel
 ): ComponentSubType {
-    const subType = newCptOption.type
-        ? newCptOption.type
+    const subType = newCmptOption.type
+        ? newCmptOption.type
         : existComponent
         ? existComponent.subType
         // Use determineSubType only when there is no existComponent.
-        : (ComponentModel as ComponentModelConstructor).determineSubType(mainType, newCptOption);
+        : (ComponentModel as ComponentModelConstructor).determineSubType(mainType, newCmptOption);
 
     // tooltip, markline, markpoint may always has no subType
     return subType;
 }
 
+function queryByIdOrName<T extends { id?: string, name?: string }>(
+    attr: 'id' | 'name',
+    idOrName: string | string[],
+    cmpts: T[]
+): T[] {
+    let keyMap: HashMap<string>;
+    return isArray(idOrName)
+        ? (
+            keyMap = createHashMap(idOrName),
+            filter(cmpts, cmpt => cmpt && keyMap.get(cmpt[attr]) != null)
+        )
+        : filter(cmpts, cmpt => cmpt && cmpt[attr] === idOrName + '');
+}
+
 function filterBySubType(
     components: ComponentModel[],
     condition: QueryConditionKindA | QueryConditionKindB
@@ -758,14 +879,27 @@ function filterBySubType(
     // Using hasOwnProperty for restrict. Consider
     // subType is undefined in user payload.
     return condition.hasOwnProperty('subType')
-        ? filter(components, function (cpt) {
-            return cpt.subType === condition.subType;
-        })
+        ? filter(components, cmpt => cmpt && cmpt.subType === condition.subType)
         : components;
 }
 
-// @ts-ignore FIXME:GlobalOption
-interface GlobalModel extends ColorPaletteMixin {}
+function normalizeReplaceMergeInput(opts: GlobalModelSetOptionOpts): InnerSetOptionOpts {
+    const replaceMergeMainTypeMap = createHashMap<boolean>();
+    opts && each(modelUtil.normalizeToArray(opts.replaceMerge), function (mainType) {
+        if (__DEV__) {
+            assert(
+                (ComponentModel as ComponentModelConstructor).hasClass(mainType),
+                '"' + mainType + '" is not valid component main type in "replaceMerge"'
+            );
+        }
+        replaceMergeMainTypeMap.set(mainType, true);
+    });
+    return {
+        replaceMergeMainTypeMap: replaceMergeMainTypeMap
+    };
+}
+
+interface GlobalModel extends ColorPaletteMixin<ECUnitOption> {}
 mixin(GlobalModel, ColorPaletteMixin);
 
 export default GlobalModel;
diff --git a/src/model/OptionManager.ts b/src/model/OptionManager.ts
index e5b123f..ee195e8 100644
--- a/src/model/OptionManager.ts
+++ b/src/model/OptionManager.ts
@@ -27,7 +27,7 @@ import * as modelUtil from '../util/model';
 import ComponentModel, { ComponentModelConstructor } from './Component';
 import ExtensionAPI from '../ExtensionAPI';
 import { OptionPreprocessor, MediaQuery, ECUnitOption, MediaUnit, ECOption } from '../util/types';
-import GlobalModel from './Global';
+import GlobalModel, { InnerSetOptionOpts } from './Global';
 
 const each = zrUtil.each;
 const clone = zrUtil.clone;
@@ -80,7 +80,11 @@ class OptionManager {
         this._api = api;
     }
 
-    setOption(rawOption: ECOption, optionPreprocessorFuncs: OptionPreprocessor[]): void {
+    setOption(
+        rawOption: ECOption,
+        optionPreprocessorFuncs: OptionPreprocessor[],
+        opt: InnerSetOptionOpts
+    ): void {
         if (rawOption) {
             // That set dat primitive is dangerous if user reuse the data when setOption again.
             zrUtil.each(modelUtil.normalizeToArray((rawOption as ECUnitOption).series), function (series) {
@@ -106,7 +110,7 @@ class OptionManager {
         // For setOption at second time (using merge mode);
         if (oldOptionBackup) {
             // Only baseOption can be merged.
-            mergeOption(oldOptionBackup.baseOption, newParsedOption.baseOption);
+            mergeOption(oldOptionBackup.baseOption, newParsedOption.baseOption, opt);
 
             // For simplicity, timeline options and media options do not support merge,
             // that is, if you `setOption` twice and both has timeline options, the latter
@@ -184,7 +188,8 @@ class OptionManager {
         }
 
         // FIXME
-        // 是否mediaDefault应该强制用户设置,否则可能修改不能回归。
+        // Whether mediaDefault should force users to provide? Otherwise
+        // the change by media query can not be recorvered.
         if (!indices.length && mediaDefault) {
             indices = [-1];
         }
@@ -345,8 +350,18 @@ function indicesEquals(indices1: number[], indices2: number[]): boolean {
  * 1. Each model handle its self restoration but not uniform treatment.
  *     (Too complex in logic and error-prone)
  * 2. Use a shadow ecModel. (Performace expensive)
+ *
+ * FIXME: A possible solution:
+ * Add a extra level of model for each component model. The inheritance chain would be:
+ * ecModel <- componentModel <- componentActionModel <- dataItemModel
+ * And all of the actions can only modify the `componentActionModel` rather than
+ * `componentModel`. `setOption` will only modify the `ecModel` and `componentModel`.
+ * When "resotre" action triggered, model from `componentActionModel` will be discarded
+ * instead of recreating the "ecModel" from the "_optionBackup".
  */
-function mergeOption(oldOption: ECUnitOption, newOption: ECUnitOption): void {
+function mergeOption(
+    oldOption: ECUnitOption, newOption: ECUnitOption, opt: InnerSetOptionOpts
+): void {
     newOption = newOption || {} as ECUnitOption;
 
     each(newOption, function (newCptOpt, mainType) {
@@ -363,12 +378,14 @@ function mergeOption(oldOption: ECUnitOption, newOption: ECUnitOption): void {
             newCptOpt = modelUtil.normalizeToArray(newCptOpt);
             oldCptOpt = modelUtil.normalizeToArray(oldCptOpt);
 
-            const mapResult = modelUtil.mappingToExists(oldCptOpt, newCptOpt);
+            const mapResult = opt.replaceMergeMainTypeMap.get(mainType)
+                ? modelUtil.mappingToExistsInReplaceMerge(oldCptOpt, newCptOpt)
+                : modelUtil.mappingToExistsInNormalMerge(oldCptOpt, newCptOpt);
 
             oldOption[mainType] = map(mapResult, function (item) {
-                return (item.option && item.exist)
-                    ? merge(item.exist, item.option, true)
-                    : (item.exist || item.option);
+                return (item.newOption && item.existing)
+                    ? merge(item.existing, item.newOption, true)
+                    : (item.existing || item.newOption);
             });
         }
     });
diff --git a/src/util/model.ts b/src/util/model.ts
index 1589c80..95cb000 100644
--- a/src/util/model.ts
+++ b/src/util/model.ts
@@ -46,6 +46,7 @@ import { Dictionary } from 'zrender/src/core/types';
 import SeriesModel from '../model/Series';
 import CartesianAxisModel from '../coord/cartesian/AxisModel';
 import GridModel from '../coord/cartesian/GridModel';
+import { __DEV__ } from '../config';
 
 /**
  * Make the name displayable. But we should
@@ -142,11 +143,24 @@ export function isDataItemOption(dataItem: OptionDataItem): boolean {
 }
 
 type MappingExistItem = {id?: string, name?: string} | ComponentModel;
+/**
+ * The array `MappingResult<T>[]` exactly represents the content of the result
+ * components array after merge.
+ * The indices are the same as the `existings`.
+ * Items will not be `null`/`undefined` even if the corresponding `existings` will be removed.
+ */
+type MappingResult<T> = MappingResultItem<T>[];
 interface MappingResultItem<T> {
-    exist?: T;
-    option?: ComponentOption;
-    id?: string;
-    name?: string;
+    // Existing component instance.
+    existing?: T;
+    // The mapped new component option.
+    newOption?: ComponentOption;
+    // Mark that the new component has nothing to do with any of the old components.
+    // So they won't share view. Also see `__requireNewView`.
+    brandNew?: boolean;
+    // id?: string;
+    // name?: string;
+    // keyInfo for new component option.
     keyInfo?: {
         name?: string,
         id?: string,
@@ -156,106 +170,192 @@ interface MappingResultItem<T> {
 }
 
 /**
- * Mapping to exists for merge.
+ * Mapping to existings for merge.
+ * The mapping result (merge result) will keep the order of the existing
+ * component, rather than the order of new option. Because we should ensure
+ * some specified index reference (like xAxisIndex) keep work.
+ * And in most cases, "merge option" is used to update partial option but not
+ * be expected to change the order.
  *
- * @public
- * @param exists
- * @param newCptOptions
- * @return Result, like [{exist: ..., option: ...}, {}],
- *                          index of which is the same as exists.
+ * @return See the comment of <MappingResult>.
  */
-export function mappingToExists<T extends MappingExistItem>(
-    exists: T[],
-    newCptOptions: ComponentOption[]
-): MappingResultItem<T>[] {
-    // Mapping by the order by original option (but not order of
-    // new option) in merge mode. Because we should ensure
-    // some specified index (like xAxisIndex) is consistent with
-    // original option, which is easy to understand, espatially in
-    // media query. And in most case, merge option is used to
-    // update partial option but not be expected to change order.
-    newCptOptions = (newCptOptions || []).slice();
-
-    const result: MappingResultItem<T>[] = map(exists || [], function (obj, index) {
-        return {exist: obj};
-    });
+export function mappingToExistsInNormalMerge<T extends MappingExistItem>(
+    existings: T[],
+    newCmptOptions: ComponentOption[]
+): MappingResult<T> {
+    newCmptOptions = (newCmptOptions || []).slice();
+    existings = existings || [];
+
+    const result: MappingResultItem<T>[] = [];
+    // Do not use native `map` to in case that the array `existings`
+    // contains elided items, which will be ommited.
+    for (let index = 0; index < existings.length; index++) {
+        // Because of replaceMerge, `existing` may be null/undefined.
+        result.push({ existing: existings[index] });
+    }
 
     // Mapping by id or name if specified.
-    each(newCptOptions, function (cptOption, index) {
-        if (!isObject<ComponentOption>(cptOption)) {
+    each(newCmptOptions, function (cmptOption, index) {
+        if (!isObject<ComponentOption>(cmptOption)) {
+            newCmptOptions[index] = null;
             return;
         }
 
         // id has highest priority.
         for (let i = 0; i < result.length; i++) {
-            if (!result[i].option // Consider name: two map to one.
-                && cptOption.id != null
-                && result[i].exist.id === cptOption.id + ''
+            const existing = result[i].existing;
+            if (!result[i].newOption // Consider name: two map to one.
+                && cmptOption.id != null
+                && existing
+                && existing.id === cmptOption.id + ''
             ) {
-                result[i].option = cptOption;
-                newCptOptions[index] = null;
+                result[i].newOption = cmptOption;
+                newCmptOptions[index] = null;
                 return;
             }
         }
 
         for (let i = 0; i < result.length; i++) {
-            const exist = result[i].exist;
-            if (!result[i].option // Consider name: two map to one.
-                // Can not match when both ids exist but different.
-                && (exist.id == null || cptOption.id == null)
-                && cptOption.name != null
-                && !isIdInner(cptOption)
-                && !isIdInner(exist)
-                && exist.name === cptOption.name + ''
+            const existing = result[i].existing;
+            if (!result[i].newOption // Consider name: two map to one.
+                // Can not match when both ids existing but different.
+                && existing
+                && (existing.id == null || cmptOption.id == null)
+                && cmptOption.name != null
+                && !isIdInner(cmptOption)
+                && !isIdInner(existing)
+                && existing.name === cmptOption.name + ''
             ) {
-                result[i].option = cptOption;
-                newCptOptions[index] = null;
+                result[i].newOption = cmptOption;
+                newCmptOptions[index] = null;
                 return;
             }
         }
     });
 
-    // Otherwise mapping by index.
-    each(newCptOptions, function (cptOption, index) {
-        if (!isObject<ComponentOption>(cptOption)) {
-            return;
-        }
+    mappingByIndexFinally(newCmptOptions, result, false);
 
-        let i = 0;
-        for (; i < result.length; i++) {
-            const exist = result[i].exist;
-            if (!result[i].option
-                // Existing model that already has id should be able to
-                // mapped to (because after mapping performed model may
-                // be assigned with a id, whish should not affect next
-                // mapping), except those has inner id.
-                && !isIdInner(exist)
-                // Caution:
-                // Do not overwrite id. But name can be overwritten,
-                // because axis use name as 'show label text'.
-                // 'exist' always has id and name and we dont
-                // need to check it.
-                && cptOption.id == null
-            ) {
-                result[i].option = cptOption;
-                break;
+    return result;
+}
+
+/**
+ * Mapping to exists for merge.
+ * The mode "replaceMerge" means that:
+ * (1) Only the id mapped components will be merged.
+ * (2) Other existing components (except inner compoonets) will be removed.
+ * (3) Other new options will be used to create new component.
+ * (4) The index of the existing compoents will not be modified.
+ * That means their might be "hole" after the removal.
+ * The new components are created first at those available index.
+ *
+ * @return See the comment of <MappingResult>.
+ */
+export function mappingToExistsInReplaceMerge<T extends MappingExistItem>(
+    existings: T[],
+    newCmptOptions: ComponentOption[]
+): MappingResult<T> {
+
+    existings = existings || [];
+    newCmptOptions = (newCmptOptions || []).slice();
+    const existingIdIdxMap = createHashMap<number>();
+    const result = [] as MappingResult<T>;
+
+    // Do not use native `each` to in case that the array `existings`
+    // contains elided items, which will be ommited.
+    for (let index = 0; index < existings.length; index++) {
+        const existing = existings[index];
+        let innerExisting: T;
+        // Because of replaceMerge, `existing` may be null/undefined.
+        if (existing) {
+            if (isIdInner(existing)) {
+                // inner components should not be removed.
+                innerExisting = existing;
             }
+            // Input with inner id is allowed for convenience of some internal usage.
+            existingIdIdxMap.set(existing.id, index);
         }
+        result.push({ existing: innerExisting });
+    }
 
-        if (i >= result.length) {
-            result.push({option: cptOption});
+    // Mapping by id if specified.
+    each(newCmptOptions, function (cmptOption, index) {
+        if (!isObject<ComponentOption>(cmptOption)) {
+            newCmptOptions[index] = null;
+            return;
+        }
+        const optionId = cmptOption.id + '';
+        const existingIdx = existingIdIdxMap.get(optionId);
+        if (existingIdx != null) {
+            if (__DEV__) {
+                assert(
+                    !result[existingIdx].newOption,
+                    'Duplicated option on id "' + optionId + '".'
+                );
+            }
+            result[existingIdx].newOption = cmptOption;
+            // Mark not to be removed but to be merged.
+            // In this case the existing component will be merged with the new option if `subType` is the same,
+            // or replaced with a new created component if the `subType` is different.
+            result[existingIdx].existing = existings[existingIdx];
+            newCmptOptions[index] = null;
         }
     });
 
+    mappingByIndexFinally(newCmptOptions, result, true);
+
+    // The array `result` MUST NOT contain elided items, otherwise the
+    // forEach will ommit those items and result in incorrect result.
     return result;
 }
 
+function mappingByIndexFinally<T extends MappingExistItem>(
+    newCmptOptions: ComponentOption[],
+    mappingResult: MappingResult<T>,
+    allBrandNew: boolean
+): void {
+    let nextIdx = 0;
+    each(newCmptOptions, function (cmptOption) {
+        if (!cmptOption) {
+            return;
+        }
+
+        // Find the first place that not mapped by id and not inner component (consider the "hole").
+        let resultItem;
+        while (
+            // Be `!resultItem` only when `nextIdx >= mappingResult.length`.
+            (resultItem = mappingResult[nextIdx])
+            // (1) Existing models that already have id should be able to mapped to. Because
+            // after mapping performed, model will always be assigned with an id if user not given.
+            // After that all models have id.
+            // (2) If new option has id, it can only set to a hole or append to the last. It should
+            // not be merged to the existings with different id. Because id should not be overwritten.
+            // (3) Name can be overwritten, because axis use name as 'show label text'.
+            && (
+                (cmptOption.id != null && resultItem.existing)
+                || resultItem.newOption
+                || isIdInner(resultItem.existing)
+            )
+        ) {
+            nextIdx++;
+        }
+
+        if (resultItem) {
+            resultItem.newOption = cmptOption;
+            resultItem.brandNew = allBrandNew;
+        }
+        else {
+            mappingResult.push({ newOption: cmptOption, brandNew: allBrandNew });
+        }
+        nextIdx++;
+    });
+}
+
 /**
  * Make id and name for mapping result (result of mappingToExists)
  * into `keyInfo` field.
  */
 export function makeIdAndName(
-    mapResult: MappingResultItem<MappingExistItem>[]
+    mapResult: MappingResult<MappingExistItem>
 ): void {
     // We use this id to hash component models and view instances
     // in echarts. id can be specified by user, or auto generated.
@@ -271,13 +371,13 @@ export function makeIdAndName(
     // Ensure that each id is distinct.
     const idMap = createHashMap();
 
-    each(mapResult, function (item, index) {
-        const existCpt = item.exist;
-        existCpt && idMap.set(existCpt.id, item);
+    each(mapResult, function (item) {
+        const existing = item.existing;
+        existing && idMap.set(existing.id, item);
     });
 
-    each(mapResult, function (item, index) {
-        const opt = item.option;
+    each(mapResult, function (item) {
+        const opt = item.newOption;
 
         assert(
             !opt || opt.id == null || !idMap.get(opt.id) || idMap.get(opt.id) === item,
@@ -290,8 +390,8 @@ export function makeIdAndName(
 
     // Make name and id.
     each(mapResult, function (item, index) {
-        const existCpt = item.exist;
-        const opt = item.option;
+        const existing = item.existing;
+        const opt = item.newOption;
         const keyInfo = item.keyInfo;
 
         if (!isObject<ComponentOption>(opt)) {
@@ -304,14 +404,14 @@ export function makeIdAndName(
         // instance will be recreated, which can be accepted.
         keyInfo.name = opt.name != null
             ? opt.name + ''
-            : existCpt
-            ? existCpt.name
+            : existing
+            ? existing.name
             // Avoid diffferent series has the same name,
             // because name may be used like in color pallet.
             : DUMMY_COMPONENT_NAME_PREFIX + index;
 
-        if (existCpt) {
-            keyInfo.id = existCpt.id;
+        if (existing) {
+            keyInfo.id = existing.id;
         }
         else if (opt.id != null) {
             keyInfo.id = opt.id + '';
@@ -341,13 +441,13 @@ export function isNameSpecified(componentModel: ComponentModel): boolean {
 
 /**
  * @public
- * @param {Object} cptOption
+ * @param {Object} cmptOption
  * @return {boolean}
  */
-export function isIdInner(cptOption: ComponentOption): boolean {
-    return isObject(cptOption)
-        && cptOption.id
-        && (cptOption.id + '').indexOf('\0_ec_\0') === 0;
+export function isIdInner(cmptOption: ComponentOption): boolean {
+    return cmptOption
+        && cmptOption.id
+        && (cmptOption.id + '').indexOf('\0_ec_\0') === 0;
 }
 
 type BatchItem = {
diff --git a/src/util/types.ts b/src/util/types.ts
index 50b7f7c..4ab0b19 100644
--- a/src/util/types.ts
+++ b/src/util/types.ts
@@ -352,7 +352,7 @@ export type ECUnitOption = {
     media?: never
     timeline?: ComponentOption | ComponentOption[]
     [key: string]: ComponentOption | ComponentOption[] | Dictionary<any> | any
-} & AnimationOptionMixin;
+} & AnimationOptionMixin & ColorPaletteOptionMixin;
 
 /**
  * [ECOption]:
diff --git a/test/lib/testHelper.js b/test/lib/testHelper.js
index 5e6c6fe..f5a5f0f 100644
--- a/test/lib/testHelper.js
+++ b/test/lib/testHelper.js
@@ -261,6 +261,64 @@
         }
     };
 
+    /**
+     * @usage
+     * ```js
+     * testHelper.printAssert(chart, function (assert) {
+     *     // If any error thrown here, a "checked: Fail" will be printed on the chart;
+     *     // Otherwise, "checked: Pass" will be printed on the chart.
+     *     assert(condition1);
+     *     assert(condition2);
+     *     assert(condition3);
+     * });
+     * ```
+     * `testHelper.printAssert` can be called multiple times for one chart instance.
+     * For each call, one result (fail or pass) will be printed.
+     *
+     * @param chart {EChartsInstance}
+     * @param checkFn {Function} param: a function `assert`.
+     */
+    testHelper.printAssert = function (chart, checkerFn) {
+        var failErr;
+        function assert(cond) {
+            if (!cond) {
+                throw new Error();
+            }
+        }
+        try {
+            checkerFn(assert);
+        }
+        catch (err) {
+            console.error(err);
+            failErr = err;
+        }
+        var printAssertRecord = chart.__printAssertRecord || (chart.__printAssertRecord = []);
+
+        var resultDom = document.createElement('div');
+        resultDom.innerHTML = failErr ? 'checked: Fail' : 'checked: Pass';
+        var fontSize = 40;
+        resultDom.style.cssText = [
+            'position: absolute;',
+            'left: 20px;',
+            'font-size: ' + fontSize + 'px;',
+            'z-index: ' + (failErr ? 99999 : 88888) + ';',
+            'color: ' + (failErr ? 'red' : 'green') + ';',
+        ].join('');
+        printAssertRecord.push(resultDom);
+        chart.getDom().appendChild(resultDom);
+
+        relayoutResult();
+
+        function relayoutResult() {
+            var chartHeight = chart.getHeight();
+            var lineHeight = Math.min(fontSize + 10, (chartHeight - 20) / printAssertRecord.length);
+            for (var i = 0; i < printAssertRecord.length; i++) {
+                var record = printAssertRecord[i];
+                record.style.top = (10 + i * lineHeight) + 'px';
+            }
+        }
+    };
+
 
     var _dummyRequestAnimationFrameMounted = false;
 
diff --git a/test/option-replaceMerge.html b/test/option-replaceMerge.html
new file mode 100644
index 0000000..2513ec2
--- /dev/null
+++ b/test/option-replaceMerge.html
@@ -0,0 +1,661 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+
+<html>
+    <head>
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <script src="lib/esl.js"></script>
+        <script src="lib/config.js"></script>
+        <script src="lib/jquery.min.js"></script>
+        <script src="lib/facePrint.js"></script>
+        <script src="lib/testHelper.js"></script>
+        <!-- <script src="ut/lib/canteen.js"></script> -->
+        <link rel="stylesheet" href="lib/reset.css" />
+    </head>
+    <body>
+        <style>
+        </style>
+
+
+
+        <div id="main_normalMerge_basic"></div>
+        <div id="main_replaceMerge_basic"></div>
+        <div id="main_normalMerge_add"></div>
+        <div id="main_replaceMerge_add_no_id"></div>
+        <div id="main_replaceMerge_add_new_id"></div>
+        <div id="main_replaceMerge_add_find_hole"></div>
+        <div id="main_normalMerge_add_find_hole"></div>
+        <div id="main_replaceMerge_inner_and_other_cmpt_not_effect"></div>
+        <div id="main_replaceMerge_remove_all"></div>
+
+
+
+
+        <script>
+            function makeBasicOption(opt) {
+                return {
+                    xAxis: {
+                        type: 'category'
+                    },
+                    yAxis: {},
+                    legend: {},
+                    tooltip: {},
+                    dataZoom: [{
+                        type: 'slider'
+                    }, {
+                        type: 'inside'
+                    }],
+                    series: [{
+                        id: 'a',
+                        name: 'aa',
+                        type: 'line',
+                        data: [['a11', 22], ['a33', 44]]
+                    }, {
+                        id: 'b',
+                        name: 'bb',
+                        type: 'line',
+                        data: [['a11', 55], ['a33', 77]]
+                    }, {
+                        id: 'c',
+                        name: 'cc',
+                        type: 'line',
+                        data: [['a11', 66], ['a33', 100]]
+                    }, {
+                        id: 'd',
+                        name: 'dd',
+                        type: 'line',
+                        data: [['a11', 99], ['a33', 130]]
+                    }, {
+                        name: 'no_id',
+                        type: 'line',
+                        data: [['a11', 130], ['a33', 160]]
+                    }]
+                };
+            }
+
+        </script>
+
+
+
+        <!-- ----------------------------- -->
+        <!-- ----------------------------- -->
+        <!-- ----------------------------- -->
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option = makeBasicOption();
+
+            var chart = testHelper.create(echarts, 'main_normalMerge_basic', {
+                title: [
+                    'normalMerge: basic case',
+                    'click "setOption": "bb" become bar chart, "aa" become **rect** symbol',
+                    'other series **do not change**'
+                ],
+                option: option,
+                buttons: [{
+                    text: 'setOption',
+                    onclick: function () {
+                        chart.setOption({
+                            series: [{
+                                id: 'b',
+                                type: 'bar',
+                                data: [['a11', 55], ['a33', 77]]
+                            }, {
+                                id: 'a',
+                                symbol: 'rect'
+                            }]
+                        })
+                    }
+                }],
+                height: 300
+            });
+        });
+        </script>
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option = makeBasicOption();
+
+            var chart = testHelper.create(echarts, 'main_replaceMerge_basic', {
+                title: [
+                    'replaceMerge: basic case',
+                    'click "setOption": "bb" become bar chart, "aa" become **rect** symbol',
+                    'other series **removed**'
+                ],
+                option: option,
+                buttons: [{
+                    text: 'setOption',
+                    onclick: function () {
+                        chart.setOption({
+                            series: [{
+                                id: 'b',
+                                type: 'bar',
+                                data: [['a11', 55], ['a33', 77]]
+                            }, {
+                                id: 'a',
+                                symbol: 'rect'
+                            }]
+                        }, {replaceMerge: 'series'})
+                    }
+                }, {
+                    text: 'check after click setOption',
+                    onclick: function () {
+                        testHelper.printAssert(chart, function (assert) {
+                            var seriesModels = chart.getModel().getSeries();
+                            assert(seriesModels.length === 2);
+                            assert(chart.getModel().getSeriesCount() === 2);
+                        });
+                    }
+                }],
+                height: 300
+            });
+        });
+        </script>
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option = makeBasicOption();
+
+            var chart = testHelper.create(echarts, 'main_normalMerge_add', {
+                title: [
+                    'normalMerge: add',
+                    'click "setOption": "aa" become **rect** symbol, "no_id" become "new_no_id" and bar',
+                    'other series **do not change**'
+                ],
+                option: option,
+                buttons: [{
+                    text: 'setOption',
+                    onclick: function () {
+                        chart.setOption({
+                            series: [{
+                                id: 'a',
+                                symbol: 'rect'
+                            }, {
+                                name: 'new_no_id',
+                                type: 'bar',
+                                data: [['a11', 10], ['a33', 20]]
+                            }]
+                        })
+                    }
+                }],
+                height: 300
+            });
+        });
+        </script>
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option = makeBasicOption();
+
+            var chart = testHelper.create(echarts, 'main_replaceMerge_add_no_id', {
+                title: [
+                    'replaceMerge: add (add no id)',
+                    'click "setOption": "aa" become **rect** symbol, bar series "new_no_id" added',
+                    'other series **removed**',
+                    'click "check": should show **checked: Pass**'
+                ],
+                option: option,
+                buttons: [{
+                    text: 'setOption',
+                    onclick: function () {
+                        chart.setOption({
+                            series: [{
+                                id: 'a',
+                                symbol: 'rect'
+                            }, {
+                                name: 'new_no_id',
+                                type: 'bar',
+                                data: [['a11', 10], ['a33', 20]]
+                            }]
+                        }, {replaceMerge: ['series']});
+                    }
+                }, {
+                    text: 'check after click setOption',
+                    onclick: function () {
+                        testHelper.printAssert(chart, function (assert) {
+                            var seriesModels = chart.getModel().getSeries();
+                            assert(seriesModels.length === 2);
+                            assert(
+                                seriesModels[1].componentIndex === 1
+                                && seriesModels[1].name === 'new_no_id'
+                            );
+                            assert(chart.getModel().getSeriesCount() === 2);
+                        });
+                    }
+                }],
+                height: 300
+            });
+        });
+        </script>
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option = makeBasicOption();
+
+            var chart = testHelper.create(echarts, 'main_replaceMerge_add_new_id', {
+                title: [
+                    'replaceMerge: add (has new id)',
+                    'click "setOption": "aa" become **rect** symbol, bar series "xx" added',
+                    'other series **removed**',
+                    'click "check": should show **checked: Pass**'
+                ],
+                option: option,
+                buttons: [{
+                    text: 'setOption',
+                    onclick: function () {
+                        chart.setOption({
+                            series: [{
+                                id: 'a',
+                                symbol: 'rect'
+                            }, {
+                                id: 'x',
+                                name: 'xx',
+                                type: 'bar',
+                                data: [['a11', 10], ['a33', 20]]
+                            }]
+                        }, {replaceMerge: 'series'});
+                    }
+                }, {
+                    text: 'check after click setOption',
+                    onclick: function () {
+                        testHelper.printAssert(chart, function (assert) {
+                            var seriesModels = chart.getModel().getSeries();
+                            assert(seriesModels.length === 2);
+                            assert(
+                                seriesModels[1].componentIndex === 1
+                                && seriesModels[1].name === 'xx'
+                            );
+                            assert(chart.getModel().getSeriesCount() === 2);
+                        });
+                    }
+                }],
+                height: 300
+            });
+        });
+        </script>
+
+
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option = makeBasicOption();
+
+            var chart = testHelper.create(echarts, 'main_replaceMerge_add_find_hole', {
+                title: [
+                    '**replaceMerge**: add (find the first hole)',
+                    'click the buttons one by one from left to right',
+                    'should show **TWO checked: Pass**'
+                ],
+                option: option,
+                buttons: [{
+                    text: 'setOption_remove',
+                    onclick: function () {
+                        chart.setOption({
+                            series: [{
+                                id: 'a'
+                            }, {
+                                id: 'c'
+                            }, {
+                                id: 'd'
+                            }]
+                        }, {replaceMerge: 'series'});
+                    }
+                }, {
+                    text: 'check after click setOption_remove',
+                    onclick: function () {
+                        testHelper.printAssert(chart, function (assert) {
+                            var seriesModels = chart.getModel().getSeries();
+                            assert(seriesModels.length === 3);
+                            assert(seriesModels[0].componentIndex === 0);
+                            assert(seriesModels[1].componentIndex === 2);
+                            assert(seriesModels[2].componentIndex === 3);
+                            assert(seriesModels[0].id === 'a');
+                            assert(seriesModels[1].id === 'c');
+                            assert(seriesModels[2].id === 'd');
+
+                            assert(chart.getModel().getSeriesCount() === 3);
+
+                            var optionGotten = chart.getOption();
+                            assert(optionGotten.series.length === 4);
+                            assert(optionGotten.series[0].name === 'aa');
+                            assert(optionGotten.series[1] == null);
+                            assert(optionGotten.series[2].name === 'cc');
+                            assert(optionGotten.series[3].name === 'dd');
+
+                            assert(chart.getModel().getSeriesByIndex(1) == null);
+                            assert(chart.getModel().getComponent('series', 1) == null);
+                        });
+                    }
+                }, {
+                    text: 'setOption_replaceMerge',
+                    onclick: function () {
+                        chart.setOption({
+                            series: [{
+                                id: 'm',
+                                type: 'bar',
+                                data: [['a11', 22], ['a33', 44]]
+                            }, {
+                                id: 'n',
+                                type: 'bar',
+                                data: [['a11', 32], ['a33', 54]]
+                            }, {
+                                id: 'a'
+                            }, {
+                                id: 'c'
+                            }, {
+                                id: 'd'
+                            }]
+                        }, {replaceMerge: 'series'});
+                    }
+                }, {
+                    text: 'check after click setOption_replaceMerge',
+                    onclick: function () {
+                        testHelper.printAssert(chart, function (assert) {
+                            var seriesModels = chart.getModel().getSeries();
+                            assert(seriesModels.length === 5);
+                            assert(seriesModels[0].componentIndex === 0);
+                            assert(seriesModels[1].componentIndex === 1);
+                            assert(seriesModels[2].componentIndex === 2);
+                            assert(seriesModels[3].componentIndex === 3);
+                            assert(seriesModels[4].componentIndex === 4);
+                            assert(seriesModels[0].id === 'a');
+                            assert(seriesModels[1].id === 'm');
+                            assert(seriesModels[2].id === 'c');
+                            assert(seriesModels[3].id === 'd');
+                            assert(seriesModels[4].id === 'n');
+
+                            assert(chart.getModel().getSeriesCount() === 5);
+
+                            var optionGotten = chart.getOption();
+                            assert(optionGotten.series.length === 5);
+                            assert(optionGotten.series[0].id === 'a');
+                            assert(optionGotten.series[1].id === 'm');
+                            assert(optionGotten.series[2].id === 'c');
+                            assert(optionGotten.series[3].id === 'd');
+                            assert(optionGotten.series[4].id === 'n');
+
+                            assert(chart.getModel().getSeriesByIndex(1).id == 'm');
+                            assert(chart.getModel().getComponent('series', 1).id == 'm');
+                        });
+                    }
+                }],
+                height: 300
+            });
+        });
+        </script>
+
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option = makeBasicOption();
+
+            var chart = testHelper.create(echarts, 'main_normalMerge_add_find_hole', {
+                title: [
+                    '**normalMerge**: add (find the first hole)',
+                    'click the buttons one by one from left to right',
+                    'should show **TWO checked: Pass**'
+                ],
+                option: option,
+                buttons: [{
+                    text: 'setOption_remove',
+                    onclick: function () {
+                        chart.setOption({
+                            series: [{
+                                id: 'a'
+                            }, {
+                                id: 'c'
+                            }, {
+                                id: 'd'
+                            }]
+                        }, {replaceMerge: 'series'});
+                    }
+                }, {
+                    text: 'check after click setOption_remove',
+                    onclick: function () {
+                        testHelper.printAssert(chart, function (assert) {
+                            var seriesModels = chart.getModel().getSeries();
+                            assert(seriesModels.length === 3);
+                            assert(seriesModels[0].componentIndex === 0);
+                            assert(seriesModels[1].componentIndex === 2);
+                            assert(seriesModels[2].componentIndex === 3);
+                            assert(seriesModels[0].id === 'a');
+                            assert(seriesModels[1].id === 'c');
+                            assert(seriesModels[2].id === 'd');
+
+                            assert(chart.getModel().getSeriesCount() === 3);
+
+                            var optionGotten = chart.getOption();
+                            assert(optionGotten.series.length === 4);
+                            assert(optionGotten.series[0].name === 'aa');
+                            assert(optionGotten.series[1] == null);
+                            assert(optionGotten.series[2].name === 'cc');
+                            assert(optionGotten.series[3].name === 'dd');
+
+                            assert(chart.getModel().getSeriesByIndex(1) == null);
+                            assert(chart.getModel().getComponent('series', 1) == null);
+                        });
+                    }
+                }, {
+                    text: 'setOption_normalMerge',
+                    onclick: function () {
+                        chart.setOption({
+                            series: [{
+                                id: 'm',
+                                type: 'bar',
+                                data: [['a11', 22], ['a33', 44]]
+                            }, {
+                                id: 'n',
+                                type: 'bar',
+                                data: [['a11', 32], ['a33', 54]]
+                            }]
+                        });
+                    }
+                }, {
+                    text: 'check after click setOption_normalMerge',
+                    onclick: function () {
+                        testHelper.printAssert(chart, function (assert) {
+                            var seriesModels = chart.getModel().getSeries();
+                            assert(seriesModels.length === 5);
+                            assert(seriesModels[0].componentIndex === 0);
+                            assert(seriesModels[1].componentIndex === 1);
+                            assert(seriesModels[2].componentIndex === 2);
+                            assert(seriesModels[3].componentIndex === 3);
+                            assert(seriesModels[4].componentIndex === 4);
+                            assert(seriesModels[0].id === 'a');
+                            assert(seriesModels[1].id === 'm');
+                            assert(seriesModels[2].id === 'c');
+                            assert(seriesModels[3].id === 'd');
+                            assert(seriesModels[4].id === 'n');
+
+                            assert(chart.getModel().getSeriesCount() === 5);
+
+                            var optionGotten = chart.getOption();
+                            assert(optionGotten.series.length === 5);
+                            assert(optionGotten.series[0].id === 'a');
+                            assert(optionGotten.series[1].id === 'm');
+                            assert(optionGotten.series[2].id === 'c');
+                            assert(optionGotten.series[3].id === 'd');
+                            assert(optionGotten.series[4].id === 'n');
+
+                            assert(chart.getModel().getSeriesByIndex(1).id == 'm');
+                            assert(chart.getModel().getComponent('series', 1).id == 'm');
+                        });
+                    }
+                }],
+                height: 300
+            });
+        });
+        </script>
+
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+        var option = {
+                xAxis: {
+                    type: 'category'
+                },
+                yAxis: {},
+                legend: {},
+                tooltip: {},
+                toolbox: {
+                    feature: {
+                        dataZoom: {}
+                    }
+                },
+                dataZoom: [{
+                    id: 'inside_dz',
+                    type: 'inside'
+                }],
+                series: [{
+                    id: 'a',
+                    name: 'aa',
+                    type: 'line',
+                    data: [['a11', 22], ['a33', 44]]
+                }, {
+                    id: 'b',
+                    name: 'bb',
+                    type: 'line',
+                    data: [['a11', 55], ['a33', 77]]
+                }]
+            };
+
+            var chart = testHelper.create(echarts, 'main_replaceMerge_inner_and_other_cmpt_not_effect', {
+                title: [
+                    'replaceMerge: inner not effect',
+                    'click "setOption": a dataZoom.slider added',
+                    'check **inside dataZoom** and **select dataZoom** on toolbox still OK',
+                    'series **not change**',
+                    'click "check": should show **checked: Pass**'
+                ],
+                option: option,
+                buttons: [{
+                    text: 'setOption',
+                    onclick: function () {
+                        chart.setOption({
+                            dataZoom: [{
+                               type: 'slider'
+                            }, {
+                                id: 'inside_dz'
+                            }]
+                        }, {replaceMerge: ['dataZoom']});
+                    }
+                }, {
+                    text: 'check after click setOption',
+                    onclick: function () {
+                        testHelper.printAssert(chart, function (assert) {
+                            var insideDZ = chart.getModel().getComponent('dataZoom', 0);
+                            var selectDZX = chart.getModel().getComponent('dataZoom', 1);
+                            var selectDZY = chart.getModel().getComponent('dataZoom', 2);
+                            var sliderDZ = chart.getModel().getComponent('dataZoom', 3);
+                            assert(insideDZ.type === 'dataZoom.inside');
+                            assert(selectDZX.type === 'dataZoom.select');
+                            assert(selectDZY.type === 'dataZoom.select');
+                            assert(sliderDZ.type === 'dataZoom.slider');
+                            assert(chart.getModel().getComponent('dataZoom', 4) == null);
+                        });
+                    }
+                }],
+                height: 300
+            });
+        });
+        </script>
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option = makeBasicOption();
+
+            var chart = testHelper.create(echarts, 'main_replaceMerge_remove_all', {
+                title: [
+                    'replaceMerge: remove all',
+                    'click "setOption": "all series should be removed"',
+                    'click "check": should show **checked: Pass**'
+                ],
+                option: option,
+                buttons: [{
+                    text: 'setOption',
+                    onclick: function () {
+                        chart.setOption({
+                            series: []
+                        }, {replaceMerge: 'series'});
+                    }
+                }, {
+                    text: 'check after click setOption',
+                    onclick: function () {
+                        testHelper.printAssert(chart, function (assert) {
+                            var seriesModels = chart.getModel().getSeries();
+                            assert(seriesModels.length === 0);
+                            assert(chart.getModel().getSeriesCount() === 0);
+                        });
+                    }
+                }],
+                height: 300
+            });
+        });
+        </script>
+
+
+
+
+
+
+    </body>
+</html>
+
diff --git a/test/option-replaceMerge2.html b/test/option-replaceMerge2.html
new file mode 100644
index 0000000..cd85164
--- /dev/null
+++ b/test/option-replaceMerge2.html
@@ -0,0 +1,497 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+
+<html>
+    <head>
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <script src="lib/esl.js"></script>
+        <script src="lib/config.js"></script>
+        <script src="lib/jquery.min.js"></script>
+        <script src="lib/facePrint.js"></script>
+        <script src="lib/testHelper.js"></script>
+        <!-- <script src="ut/lib/canteen.js"></script> -->
+        <link rel="stylesheet" href="lib/reset.css" />
+    </head>
+    <body>
+        <style>
+        </style>
+
+
+
+        <div id="transition_facet_cartesian"></div>
+        <div id="notMerge_transition_replaceMerge_newView"></div>
+        <div id="main_replaceMerge_keep_update"></div>
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var optionBase = {
+                color: ['#eb6134', '#eb9934', '#348feb', '#36b6d9'],
+                dataset: {
+                    source: [
+                        [null, 'sweet zongzi', 'salty zongzi', 'sweet milk tea', 'salty milk tea'],
+                        ['2012-01', 32, 65, 71, 31],
+                        ['2012-02', 41, 67, 89, 23],
+                        ['2012-03', 58, 61, 97, 12],
+                        ['2012-04', 67, 73, 105, 9],
+                        ['2012-05', 72, 67, 122, 18],
+                        ['2012-06', 94, 79, 118, 32],
+                        ['2012-07', 79, 89, 131, 37],
+                        ['2012-08', 65, 76, 103, 41],
+                        ['2012-09', 69, 81, 84, 48],
+                        ['2012-10', 74, 64, 104, 38],
+                        ['2012-11', 91, 76, 111, 51],
+                        ['2012-12', 64, 68, 121, 61]
+                    ]
+                },
+                legend: {},
+                tooltip: {
+                    trigger: 'axis',
+                    axisPointer: {
+                        type: 'shadow'
+                    }
+                },
+                dataZoom: [{
+                    type: 'slider',
+                    height: 15
+                }, {
+                    type: 'inside'
+                }],
+                series: [{
+                    type: 'bar',
+                    encode: { x: 0, y: 1, seriesName: 1 }
+                }, {
+                    type: 'bar',
+                    encode: { x: 0, y: 3, seriesName: 3 }
+                }, {
+                    type: 'bar',
+                    encode: { x: 0, y: 2, seriesName: 2 }
+                }, {
+                    type: 'bar',
+                    encode: { x: 0, y: 4, seriesName: 4 }
+                }]
+            };
+
+            var optionSingle = makeSingleCartesianOption();
+            var option0 = mergeOption(echarts, optionBase, optionSingle);
+
+            function mergeOption(echarts, target, source) {
+                echarts.util.each(source, function (srcCmpts, mainType) {
+                    var tarCmpts = target[mainType] = toArray(target[mainType]);
+                    echarts.util.each(toArray(srcCmpts), function (srcCmpt, index) {
+                        tarCmpts[index] = echarts.util.merge(tarCmpts[index], srcCmpt, true);
+                    });
+                });
+                function toArray(some) {
+                    return echarts.util.isArray(some) ? some : some ? [some] : [];
+                }
+                return target;
+            }
+
+            function makeSingleCartesianOption() {
+                return {
+                    grid: {
+                    },
+                    xAxis: {
+                        type: 'category'
+                    },
+                    yAxis: {
+                        max: 150,
+                        axisLine: { show: false },
+                        axisTick: { show: false }
+                    },
+                    axisPointer: {
+                        link: [{xAxisIndex: 0}]
+                    },
+                    dataZoom: [{
+                        xAxisIndex: 0
+                    }, {
+                        xAxisIndex: 0
+                    }],
+                    series: [{
+                        xAxisIndex: 0,
+                        yAxisIndex: 0
+                    }, {
+                        xAxisIndex: 0,
+                        yAxisIndex: 0
+                    }, {
+                        xAxisIndex: 0,
+                        yAxisIndex: 0
+                    }, {
+                        xAxisIndex: 0,
+                        yAxisIndex: 0
+                    }]
+                };
+            }
+            function makeDoubleCartesianOption() {
+                return {
+                    grid: [{
+                        bottom: '52%'
+                    }, {
+                        top: '52%'
+                    }],
+                    dataZoom: [{
+                        xAxisIndex: [0, 1]
+                    }, {
+                        xAxisIndex: [0, 1]
+                    }],
+                    axisPointer: {
+                        link: [{xAxisIndex: [0, 1]}]
+                    },
+                    xAxis: [{
+                        type: 'category',
+                        axisLabel: { show: false },
+                        axisTick: { show: false },
+                        axisLine: { show: false },
+                        gridIndex: 0
+                    }, {
+                        type: 'category',
+                        // axisTick: { show: false },
+                        axisLine: { onZero: false },
+                        gridIndex: 1
+                    }],
+                    yAxis: [{
+                        name: 'sweet',
+                        max: 150,
+                        nameLocation: 'center',
+                        nameGap: 40,
+                        axisLine: { show: false },
+                        axisTick: { show: false },
+                        axisLabel: { color: '#000' },
+                        gridIndex: 0
+                    }, {
+                        name: 'salty',
+                        max: 150,
+                        nameLocation: 'center',
+                        nameGap: 40,
+                        inverse: true,
+                        axisLine: { show: false },
+                        axisTick: { show: false },
+                        axisLabel: { color: '#000' },
+                        gridIndex: 1
+                    }],
+                    series: [{
+                        xAxisIndex: 0,
+                        yAxisIndex: 0
+                    }, {
+                        xAxisIndex: 0,
+                        yAxisIndex: 0
+                    }, {
+                        xAxisIndex: 1,
+                        yAxisIndex: 1
+                    }, {
+                        xAxisIndex: 1,
+                        yAxisIndex: 1
+                    }]
+                };
+            }
+
+            var chart = testHelper.create(echarts, 'transition_facet_cartesian', {
+                title: [
+                    '<1> Click "double cartesian", should become **double** grid',
+                    'Click "single cartesian", should become **single** grid',
+                    'Check transition animation existing',
+                    '<2> **downplay some legend item**, then click "doulbe"/"single" btns again',
+                    'transition should be OK, legend state should be kept',
+                    '<3> **shrink dataZoom**, then click "doulbe"/"single" btns again',
+                    'transition should be OK, legend state should be kept',
+                ],
+                option: option0,
+                buttons: [{
+                    text: 'double cartesian',
+                    onclick: function () {
+                        chart.setOption(makeDoubleCartesianOption(), {
+                            replaceMerge: ['xAxis', 'yAxis', 'grid']
+                        })
+                    }
+                }, {
+                    text: 'single cartesian',
+                    onclick: function () {
+                        chart.setOption(makeSingleCartesianOption(), {
+                            replaceMerge: ['xAxis', 'yAxis', 'grid']
+                        })
+                    }
+                }]
+            });
+        });
+        </script>
+
+
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            function makeOption(extOption) {
+                return {
+                    animationDurationUpdate: 2000,
+                    dataset: {
+                        source: [
+                            [null, 'sweet zongzi', 'salty zongzi', 'sweet milk tea', 'salty milk tea', 'NewNew'],
+                            ['2012-01', 32, 65, 71, 31, 99],
+                            ['2012-02', 41, 67, 99, 23, 199],
+                            ['2012-03', 58, 61, 97, 12, 99],
+                            ['2012-04', 67, 73, 105, 9, 199],
+                            ['2012-05', 72, 67, 122, 18, 99],
+                            ['2012-06', 94, 79, 118, 32, 199],
+                        ]
+                    },
+                    legend: {},
+                    tooltip: {
+                    },
+                    xAxis: {
+                        type: 'category'
+                    },
+                    yAxis: {},
+                    series: extOption.series
+                };
+            }
+
+            var option_base = makeOption({
+                series: [{
+                    type: 'scatter',
+                    encode: { x: 0, y: 1, seriesName: 1 }
+                }, {
+                    type: 'scatter',
+                    encode: { x: 0, y: 3, seriesName: 3 }
+                }]
+            });
+
+            var option_notMerge = makeOption({
+                series: [{
+                    type: 'scatter',
+                    encode: { x: 0, y: 2, seriesName: 2 }
+                }, {
+                    type: 'scatter',
+                    encode: { x: 0, y: 4, seriesName: 4 }
+                }]
+            });
+
+            var option_replaceMerge = {
+                series: [{
+                    type: 'scatter',
+                    encode: { x: 0, y: 5, seriesName: 5 }
+                }]
+            };
+
+            var seriesModels_base;
+            var seriesModels_notMerge;
+            var seriesModels_replaceMerge;
+            var view0_notMerge;
+            var view0_replaceMerge;
+
+            var chart = testHelper.create(echarts, 'notMerge_transition_replaceMerge_newView', {
+                title: [
+                    'Click btns from left to right:',
+                    'Click "setOption_notMerge", should **has trans animation**',
+                    'Click "check", should print **checked: Pass**',
+                    'Click "setOption_replaceMerge", should only "NewNew" and **no trans animation**',
+                    'Click "check", should print **checked: Pass**',
+                ],
+                option: option_base,
+                buttons: [{
+                    text: 'setOption_notMerge',
+                    onclick: function () {
+                        seriesModels_base = chart.getModel().getSeries('series');
+                        chart.setOption(option_notMerge, true);
+                    }
+                }, {
+                    text: 'then check',
+                    onclick: function () {
+                        testHelper.printAssert(chart, function (assert) {
+                            seriesModels_notMerge = chart.getModel().getSeries();
+                            assert(seriesModels_base.length === 2);
+                            assert(seriesModels_notMerge.length === 2);
+
+                            assert(seriesModels_base[0] !== seriesModels_notMerge[0]);
+                            assert(seriesModels_base[1] !== seriesModels_notMerge[1]);
+                            assert(seriesModels_base[0] !== seriesModels_notMerge[1]);
+                            assert(seriesModels_base[1] !== seriesModels_notMerge[0]);
+                        });
+                    }
+                }, {
+                    text: 'setOption_replaceMerge',
+                    onclick: function () {
+                        seriesModels_base = chart.getModel().getSeries('series');
+                        view0_notMerge = chart.getViewOfSeriesModel(seriesModels_notMerge[0]);
+                        chart.setOption(option_replaceMerge, { replaceMerge: 'series' });
+                    }
+                }, {
+                    text: 'then check',
+                    onclick: function () {
+                        testHelper.printAssert(chart, function (assert) {
+                            seriesModels_replaceMerge = chart.getModel().getSeries();
+                            assert(seriesModels_replaceMerge.length === 1);
+                            assert(seriesModels_notMerge[0] !== seriesModels_replaceMerge[0]);
+                            view0_replaceMerge = chart.getViewOfSeriesModel(seriesModels_replaceMerge[0]);
+                            assert(view0_notMerge != null);
+                            assert(view0_notMerge !== view0_replaceMerge);
+                        });
+                    }
+                }]
+            });
+        });
+        </script>
+
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var currentRound = 0;
+            var nameMap = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n'];
+            var categories = ['Mon', 'Tue', 'Wed'];
+
+            function createUpdatableSeriesAndDataset(seriesCount) {
+                var series = [];
+                for (var i = 0; i < seriesCount; i++) {
+                    series.push({
+                        name: nameMap[i] + '_round_' + currentRound,
+                        type: 'bar',
+                        barWidth: 40,
+                        encode: {
+                            x: 0,
+                            y: i + 1
+                        },
+                        seriesLayoutBy: 'row'
+                    });
+                }
+                var dataset = {
+                    source: [categories.slice()]
+                };
+                var yVal = 22 + 100 * currentRound;
+                for (var i = 0; i < seriesCount; i++, yVal += 10) {
+                    dataset.source.push([yVal, yVal * 2, yVal * 2.5]);
+                }
+
+                currentRound++;
+
+                return {
+                    series: series,
+                    dataset: dataset
+                };
+            }
+
+            var sInfo = createUpdatableSeriesAndDataset(2);
+            var series = sInfo.series;
+            var dataset = sInfo.dataset;
+
+            series.unshift({
+                id: 'I_never_change',
+                name: 'I_never_change',
+                type: 'pie',
+                selectedMode: 'single',
+                lineStyle: {
+                    color: '#881100',
+                    width: 5
+                },
+                center: ['20%', 80],
+                radius: 40,
+                data: [
+                    {name: 'Mon', value: 100},
+                    {name: 'Tue', value: 200},
+                    {name: 'Wed', value: 150}
+                ]
+            });
+
+            var option = {
+                dataset: dataset,
+                brush: {
+                    toolbox: ['polygon', 'rect', 'lineX', 'lineY', 'keep', 'clear'],
+                    xAxisIndex: 'all',
+                },
+                xAxis: {
+                    type: 'category'
+                },
+                yAxis: {},
+                legend: {},
+                tooltip: {},
+                dataZoom: [{
+                    type: 'slider'
+                }, {
+                    type: 'inside'
+                }],
+                series: series
+            };
+
+            var chart = testHelper.create(echarts, 'main_replaceMerge_keep_update', {
+                title: [
+                    'replaceMerge: keep update',
+                    '<1> click "replace to new 4 series": bar totally replaced to new 4 different bars',
+                    'click "replace to new 2 series": bar totally replaced to new 2 different bars',
+                    'series "I_never_change" **never change color and data**',
+                    'click "check": should show **checked: Pass**',
+                    '<2> click pie legend to hide a sector',
+                    'click pie to select a sector',
+                    'click buttons again, **pie state should not changed**',
+                    '<3> use brush',
+                    'click buttons again, **brush selected should be correct**',
+                ],
+                option: option,
+                height: 400,
+                buttons: [{
+                    text: 'replace to new 4 series',
+                    onclick: function () {
+                        var sInfo = createUpdatableSeriesAndDataset(4);
+                        sInfo.series.push({id: 'I_never_change'});
+                        chart.setOption({
+                            dataset: sInfo.dataset,
+                            series: sInfo.series
+                        }, {replaceMerge: ['series', 'dataset']});
+                    }
+                }, {
+                    text: 'replace to new 2 series',
+                    onclick: function () {
+                        var sInfo = createUpdatableSeriesAndDataset(2);
+                        sInfo.series.push({id: 'I_never_change'});
+                        chart.setOption({
+                            dataset: sInfo.dataset,
+                            series: sInfo.series
+                        }, {replaceMerge: ['series', 'dataset']});
+                    }
+                }, {
+                    text: 'check after click setOption',
+                    onclick: function () {
+                        testHelper.printAssert(chart, function (assert) {
+                            var seriesModels = chart.getModel().getSeries();
+                            assert(seriesModels.length <= 6);
+                            assert(chart.getModel().getSeriesCount() <= 6);
+                        });
+                    }
+                }]
+            });
+        });
+        </script>
+
+
+
+
+    </body>
+</html>
+
diff --git a/test/timeline-dynamic-series.html b/test/timeline-dynamic-series.html
index 1c786ac..7db7bdf 100644
--- a/test/timeline-dynamic-series.html
+++ b/test/timeline-dynamic-series.html
@@ -21,130 +21,109 @@ under the License.
 <html>
     <head>
         <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
         <script src="lib/esl.js"></script>
         <script src="lib/config.js"></script>
-        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <script src="lib/jquery.min.js"></script>
+        <script src="lib/facePrint.js"></script>
+        <script src="lib/testHelper.js"></script>
+        <link rel="stylesheet" href="lib/reset.css" />
     </head>
     <body>
         <style>
-            html, body, #main {
-                width: 100%;
-                height: 100%;
-                margin: 0;
-            }
-            #main {
-                background: #fff;
-            }
         </style>
-        <div id="main"></div>
-
-        <script>
-
-// markLine: {
-//     symbol: ['arrow','none'],
-//     symbolSize: [4, 2],
-//     itemStyle: {
-//         normal: {
-//             lineStyle: {color:'orange'},
-//             barBorderColor:'orange',
-//             label: {
-//                 position:'left',
-//                 formatter:function(params){
-//                     return Math.round(params.value);
-//                 },
-//                 textStyle:{color:'orange'}
-//             }
-//         }
-//     },
-//     data: [{type: 'average', name: '平均值'}]
-// }
 
-            require([
-                'echarts'
-                // 'echarts/chart/bar',
-                // 'echarts/chart/pie',
-                // 'echarts/component/title',
-                // 'echarts/component/legend',
-                // 'echarts/component/grid',
-                // 'echarts/component/tooltip',
-                // 'echarts/component/timeline'
-            ], function (echarts) {
+        <div id="main0"></div>
 
-                var chart = echarts.init(document.getElementById('main'), null, {
+        <script>
 
-                });
+        require(['echarts'], function (echarts) {
 
-var option = {
-    baseOption: {
-        timeline: {
-            // y: 0,
-            axisType: 'category',
-            // realtime: false,
-            // loop: false,
-            autoPlay: false,
-            // currentIndex: 2,
-            playInterval: 1000,
-            // controlStyle: {
-            //     position: 'left'
-            // },
-            data: [
-                '2002-01-01','2003-01-01'
-            ],
-            label: {
-                formatter : function(s) {
-                    return (new Date(s)).getFullYear();
-                }
-            }
-        },
-        title: {
-            subtext: '数据来自国家统计局'
-        },
-        tooltip: {
-            trigger:'axis',
-            axisPointer: {
-                type: 'shadow'
-            }
-        },
-        calculable: true,
-        grid: {
-            top:80, bottom: 100
-        },
-        xAxis: {
-            'type':'category',
-            'axisLabel':{'interval':0},
-            'data':[
-                '北京','\n天津','河北','\n山西'
-            ],
-            splitLine: {show: false}
-        },
-        yAxis: [
-            {
-                type: 'value',
-                name: 'GDP(亿元)'
-            }
-        ],
-        series: [
-        ]
-    },
-    options: [
-        {
-            series: [
-                {id: 'a', type: 'bar', data: [12, 33, 44, 55]}
-            ]
-        },
-        {
-            series : [
-                {id: 'a', type: 'bar', data: [22, 33, 44, 55]},
-                {id: 'b', type: 'bar', data: [55, 66, 77, 88]}
-            ]
-        }
-    ]
-};
+            var option = {
+                baseOption: {
+                    timeline: {
+                        // y: 0,
+                        axisType: 'category',
+                        // realtime: false,
+                        // loop: false,
+                        autoPlay: false,
+                        // currentIndex: 2,
+                        playInterval: 1000,
+                        // controlStyle: {
+                        //     position: 'left'
+                        // },
+                        replaceMerge: 'series',
+                        data: [
+                            '2 series', '3 series', '1 series'
+                        ],
+                    },
+                    tooltip: {
+                        trigger:'axis',
+                        axisPointer: {
+                            type: 'shadow'
+                        }
+                    },
+                    legend: {},
+                    calculable: true,
+                    grid: {
+                        top:80, bottom: 100
+                    },
+                    toolbox: {
+                        left: 'center',
+                        top: 30,
+                        feature: {
+                            dataZoom: {}
+                        }
+                    },
+                    xAxis: {
+                        type: 'category',
+                        data: ['CityB', 'CityT', 'CityH', 'CityS'],
+                        splitLine: {show: false}
+                    },
+                    yAxis: [
+                        {
+                            type: 'value',
+                            name: 'GDP'
+                        }
+                    ],
+                    series: [
+                    ]
+                },
+                options: [
+                    {
+                        series: [
+                            {name: 'a', type: 'bar', data: [12, 33, 44, 55]},
+                            {name: 'b', type: 'bar', data: [55, 66, 77, 88]},
+                        ]
+                    },
+                    {
+                        series : [
+                            {name: 'a', type: 'bar', data: [22, 33, 44, 55]},
+                            {name: 'b', type: 'bar', data: [55, 66, 77, 88]},
+                            {name: 'c', type: 'bar', data: [55, 66, 77, 88]}
+                        ]
+                    },
+                    {
+                        series : [
+                            {name: 'b', type: 'bar', data: [55, 66, 77, 88]}
+                        ]
+                    }
+                ]
+            };
 
-                chart.setOption(option);
 
-                window.onresize = chart.resize;
+            var chart = testHelper.create(echarts, 'main0', {
+                title: [
+                    'Click "right arrow" of the timeline',
+                    'check it should be **2 bar** => **3 bar** => **1 bar**',
+                    'use toolbox "区域缩放"',
+                    'timeline change',
+                    'use toolbox "区域缩放还原"',
+                    'Should be able to return correctly.'
+                ],
+                option: option,
             });
+        });
         </script>
     </body>
 </html>
\ No newline at end of file
diff --git a/test/timeline-life.html b/test/timeline-life.html
new file mode 100755
index 0000000..a9e8e4c
--- /dev/null
+++ b/test/timeline-life.html
@@ -0,0 +1,279 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+
+<html>
+    <head>
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <script src="lib/esl.js"></script>
+        <script src="lib/config.js"></script>
+        <script src="lib/jquery.min.js"></script>
+        <script src="lib/facePrint.js"></script>
+        <script src="lib/testHelper.js"></script>
+        <link rel="stylesheet" href="lib/reset.css">
+    </head>
+    <body>
+        <style>
+            .test-title {
+                background: #146402;
+                color: #fff;
+            }
+            #main0 {
+                height: 500px;
+            }
+        </style>
+
+
+        <div id="main0"></div>
+
+
+        <script>
+
+            var chart;
+            var myChart;
+            var option;
+
+            require(['echarts'], function (echarts) {
+
+                var myChart = echarts.init(document.getElementById('main0'));
+
+                $.get('data/life-expectancy.json', function (data) {
+                    myChart.hideLoading();
+
+                    var itemStyle = {
+                        normal: {
+                            opacity: 0.8,
+                            shadowBlur: 10,
+                            shadowOffsetX: 0,
+                            shadowOffsetY: 0,
+                            shadowColor: 'rgba(0, 0, 0, 0.5)'
+                        }
+                    };
+
+                    var sizeFunction = function (x) {
+                        var y = Math.sqrt(x / 5e8) + 0.1;
+                        return y * 80;
+                    };
+                    // Schema:
+                    var schema = [
+                        {name: 'Income', index: 0, text: '人均收入', unit: '美元'},
+                        {name: 'LifeExpectancy', index: 1, text: '人均寿命', unit: '岁'},
+                        {name: 'Population', index: 2, text: '总人口', unit: ''},
+                        {name: 'Country', index: 3, text: '国家', unit: ''}
+                    ];
+
+                    option = {
+                        baseOption: {
+                            timeline: {
+                                axisType: 'category',
+                                orient: 'vertical',
+                                autoPlay: false,
+                                inverse: true,
+                                playInterval: 1000,
+                                left: null,
+                                right: 0,
+                                top: 20,
+                                bottom: 20,
+                                width: 55,
+                                height: null,
+                                label: {
+                                    normal: {
+                                        textStyle: {
+                                            color: '#999'
+                                        }
+                                    },
+                                    emphasis: {
+                                        textStyle: {
+                                            color: '#fff'
+                                        }
+                                    }
+                                },
+                                symbol: 'none',
+                                lineStyle: {
+                                    color: '#555'
+                                },
+                                checkpointStyle: {
+                                    color: '#bbb',
+                                    borderColor: '#777',
+                                    borderWidth: 2
+                                },
+                                controlStyle: {
+                                    showNextBtn: false,
+                                    showPrevBtn: false,
+                                    normal: {
+                                        color: '#666',
+                                        borderColor: '#666'
+                                    },
+                                    emphasis: {
+                                        color: '#aaa',
+                                        borderColor: '#aaa'
+                                    }
+                                },
+                                data: []
+                            },
+                            backgroundColor: '#404a59',
+                            title: [{
+                                text: data.timeline[0],
+                                textAlign: 'center',
+                                left: '63%',
+                                top: '55%',
+                                textStyle: {
+                                    fontSize: 100,
+                                    color: 'rgba(255, 255, 255, 0.7)'
+                                }
+                            }, {
+                                text: '各国人均寿命与GDP关系演变',
+                                left: 'center',
+                                top: 10,
+                                textStyle: {
+                                    color: '#aaa',
+                                    fontWeight: 'normal',
+                                    fontSize: 20
+                                }
+                            }],
+                            tooltip: {
+                                padding: 5,
+                                backgroundColor: '#222',
+                                borderColor: '#777',
+                                borderWidth: 1,
+                                formatter: function (obj) {
+                                    var value = obj.value;
+                                    return schema[3].text + ':' + value[3] + '<br>'
+                                            + schema[1].text + ':' + value[1] + schema[1].unit + '<br>'
+                                            + schema[0].text + ':' + value[0] + schema[0].unit + '<br>'
+                                            + schema[2].text + ':' + value[2] + '<br>';
+                                }
+                            },
+                            grid: {
+                                top: 100,
+                                containLabel: true,
+                                left: 30,
+                                right: '110'
+                            },
+                            xAxis: {
+                                type: 'log',
+                                name: '人均收入',
+                                max: 100000,
+                                min: 300,
+                                nameGap: 25,
+                                nameLocation: 'middle',
+                                nameTextStyle: {
+                                    fontSize: 18
+                                },
+                                splitLine: {
+                                    show: false
+                                },
+                                axisLine: {
+                                    lineStyle: {
+                                        color: '#ccc'
+                                    }
+                                },
+                                axisLabel: {
+                                    formatter: '{value} $'
+                                }
+                            },
+                            yAxis: {
+                                type: 'value',
+                                name: '平均寿命',
+                                max: 100,
+                                nameTextStyle: {
+                                    color: '#ccc',
+                                    fontSize: 18
+                                },
+                                axisLine: {
+                                    lineStyle: {
+                                        color: '#ccc'
+                                    }
+                                },
+                                splitLine: {
+                                    show: false
+                                },
+                                axisLabel: {
+                                    formatter: '{value} 岁'
+                                }
+                            },
+                            visualMap: [
+                                {
+                                    show: false,
+                                    type: 'piecewise',
+                                    dimension: 3,
+                                    categories: echarts.util.map(data.countries, function (item) {
+                                        return item[2];
+                                    }),
+                                    calculable: true,
+                                    precision: 0.1,
+                                    textGap: 30,
+                                    textStyle: {
+                                        color: '#ccc'
+                                    },
+                                    inRange: {
+                                        color: (function () {
+                                            var colors = ['#bcd3bb', '#e88f70', '#edc1a5', '#9dc5c8', '#e1e8c8', '#7b7c68', '#e5b5b5', '#f0b489', '#928ea8', '#bda29a'];
+                                            return colors.concat(colors);
+                                        })()
+                                    }
+                                }
+                            ],
+                            series: [
+                                {
+                                    type: 'scatter',
+                                    itemStyle: itemStyle,
+                                    // progressive: false,
+                                    data: data.series[0],
+                                    symbolSize: function(val) {
+                                        return sizeFunction(val[2]);
+                                    }
+                                }
+                            ],
+                            animationDurationUpdate: 1000,
+                            animationEasingUpdate: 'quinticInOut'
+                        },
+                        options: []
+                    };
+
+                    for (var n = 0; n < data.timeline.length; n++) {
+                        option.baseOption.timeline.data.push(data.timeline[n]);
+                        option.options.push({
+                            title: {
+                                show: true,
+                                'text': data.timeline[n] + ''
+                            },
+                            series: {
+                                name: data.timeline[n],
+                                type: 'scatter',
+                                itemStyle: itemStyle,
+                                data: data.series[n],
+                                symbolSize: function(val) {
+                                    return sizeFunction(val[2]);
+                                }
+                            }
+                        });
+                    }
+
+                    myChart.setOption(option);
+
+                });
+
+            });
+
+        </script>
+    </body>
+</html>
\ No newline at end of file


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


[incubator-echarts] 15/16: add test case.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 548bf37d8e7a598f17ec7dae026e3ec041a4892a
Author: 100pah <su...@gmail.com>
AuthorDate: Fri Jul 17 18:35:17 2020 +0800

    add test case.
---
 test/option-replaceMerge2.html | 427 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 424 insertions(+), 3 deletions(-)

diff --git a/test/option-replaceMerge2.html b/test/option-replaceMerge2.html
index cd85164..7b7cf7f 100644
--- a/test/option-replaceMerge2.html
+++ b/test/option-replaceMerge2.html
@@ -40,6 +40,7 @@ under the License.
         <div id="transition_facet_cartesian"></div>
         <div id="notMerge_transition_replaceMerge_newView"></div>
         <div id="main_replaceMerge_keep_update"></div>
+        <div id="transition_dots"></div>
 
 
 
@@ -411,12 +412,14 @@ under the License.
                     color: '#881100',
                     width: 5
                 },
-                center: ['20%', 80],
+                center: ['20%', 120],
                 radius: 40,
                 data: [
                     {name: 'Mon', value: 100},
                     {name: 'Tue', value: 200},
-                    {name: 'Wed', value: 150}
+                    {name: 'Wed', value: 150},
+                    {name: 'Thu', value: 350},
+                    {name: 'Fri', value: 180}
                 ]
             });
 
@@ -430,7 +433,9 @@ under the License.
                     type: 'category'
                 },
                 yAxis: {},
-                legend: {},
+                legend: {
+                    left: 20
+                },
                 tooltip: {},
                 dataZoom: [{
                     type: 'slider'
@@ -492,6 +497,422 @@ under the License.
 
 
 
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var rawData = [[161.2,51.6,0],[174,65.6,1],[167.5,59,0],[175.3,71.8,1],[159.5,49.2,0],[193.5,80.7,1],[157,63,0],[186.5,72.6,1],[155.8,53.6,0],[187.2,78.8,1],[170,59,0],[181.5,74.8,1],[159.1,47.6,0],[184,86.4,1],[166,69.8,0],[184.5,78.4,1],[176.2,66.8,0],[175,62,1],[160.2,75.2,0],[184,81.6,1],[172.5,55.2,0],[180,76.6,1],[170.9,54.2,0],[177.8,83.6,1],[172.9,62.5,0],[192,90,1],[153.4,42,0],[176,74.6,1],[160,50,0],[174,71,1],[147.2,49.8,0],[184,79.6,1],[168.2,49.2,0],[192.7,93.8, [...]
+            // var rawData = [[157.5,76.8,0]];
+
+            var FEMALE = 0;
+            var MALE = 1;
+            var DIMENSION_HEIGHT = 0;
+            var DIMENSION_WEIGHT = 1;
+            var DIMENSION_SEX = 2;
+            var SYMBOL_RADIUS = 6;
+
+            var HEIGHT_RANGES = [
+                { min: 140, max: 160 },
+                { min: 160, max: 180 },
+                { min: 180, max: 200 }
+            ];
+
+            var State = {
+                NONE: 0,
+                HEIGHT_WEIGHT_SINGLE: 1,
+                HEIGHT_WEIGHT_SEX_SEP: 2,
+                COUNT_BY_HEIGHT_RANGE: 3
+            };
+
+            var currentState = State.NONE
+            var countBySexHeightRangeMax = 0;
+
+            function renderItem(seriesInfo, params, api) {
+                if (currentState === State.NONE) {
+                    return;
+                }
+
+                var sex = api.value(DIMENSION_SEX)
+                var height = api.value(DIMENSION_HEIGHT);
+                var weight = api.value(DIMENSION_WEIGHT);
+
+                if (seriesInfo.sex !== sex) {
+                    return;
+                }
+                if (height < seriesInfo.heightRange.min || height >= seriesInfo.heightRange.max) {
+                    return;
+                }
+
+                var posX;
+                var posY;
+                if (currentState === State.COUNT_BY_HEIGHT_RANGE) {
+                    var position = api.coord([sex, 0]);
+                    posX = position[0];
+                    var yStart = position[1];
+                    position = api.coord([sex, seriesInfo.countBySexHeightRange]);
+                    var yEnd = position[1];
+                    if (params.context.displayIdx == null) {
+                        params.context.displayIdx = 0;
+                    }
+                    else {
+                        params.context.displayIdx++;
+                    }
+                    if (seriesInfo.countBySexHeightRange === 1) {
+                        posY = yEnd;
+                    }
+                    else {
+                        posY = yStart + (yEnd - yStart) / (seriesInfo.countBySexHeightRange - 1) * params.context.displayIdx;
+                    }
+                }
+                else {
+                    var position = api.coord([height, weight]);
+                    posX = position[0];
+                    posY = position[1];
+                }
+
+                var color = sex === FEMALE ? '#eb6134' : '#348feb'; // ? '#eb9934' : '#36b6d9';
+
+                return {
+                    type: 'circle',
+                    x: posX,
+                    y: posY,
+                    shape: { cx: 0, cy: 0, r: SYMBOL_RADIUS },
+                    style: {
+                        fill: color,
+                        opacity: 0.8
+                        // stroke: '#555',
+                        // lineWidth: 1
+                    }
+                };
+            }
+
+            function animationDelayUpdate(idx) {
+                return idx;
+            }
+
+            var baseOption = {
+                dataset: {
+                    source: rawData
+                },
+                title: {
+                    text: 'Height and Weight'
+                },
+                tooltip: {
+                },
+                toolbox: {
+                    feature: {
+                        dataZoom: {},
+                        brush: {
+                            type: ['rect', 'polygon', 'clear']
+                        }
+                    }
+                },
+                legend: {
+                },
+                brush: {
+                },
+                dataZoom: [{
+                    type: 'slider',
+                    left: 50,
+                    right: 50,
+                    height: 20,
+                    xAxisIndex: 'all'
+                }, {
+                    type: 'inside',
+                    xAxisIndex: 'all'
+                }],
+                series: []
+            };
+
+            var seriesInfoList = [];
+
+            for (var i = 0, seriesInfo; i < HEIGHT_RANGES.length; i++) {
+                seriesInfoList.push(seriesInfo = {
+                    sex: FEMALE,
+                    heightRange: HEIGHT_RANGES[i]
+                });
+                baseOption.series.push({
+                    type: 'custom',
+                    name: 'Female',
+                    coordinateSystem: 'none',
+                    animationDelayUpdate: animationDelayUpdate,
+                    renderItem: echarts.util.curry(renderItem, seriesInfo)
+                });
+                seriesInfoList.push(seriesInfo = {
+                    sex: MALE,
+                    heightRange: HEIGHT_RANGES[i]
+                });
+                baseOption.series.push({
+                    type: 'custom',
+                    name: 'Male',
+                    coordinateSystem: 'none',
+                    animationDelayUpdate: animationDelayUpdate,
+                    renderItem: echarts.util.curry(renderItem, seriesInfo)
+                });
+            }
+
+            makeCountByHeightRange(rawData, seriesInfoList);
+
+            function makeCountByHeightRange(rawData, seriesInfoList) {
+                for (var i = 0; i < rawData.length; i++) {
+                    var rawDataItem = rawData[i];
+                    var val = rawDataItem[DIMENSION_HEIGHT];
+
+                    for(var j = 0; j < seriesInfoList.length; j++) {
+                        var seriesInfo = seriesInfoList[j];
+
+                        if (seriesInfo.sex === rawDataItem[DIMENSION_SEX]
+                            && seriesInfo.heightRange.min <= val
+                            && val < seriesInfo.heightRange.max
+                        ) {
+                            if (seriesInfo.countBySexHeightRange == null) {
+                                seriesInfo.countBySexHeightRange = 0;
+                            }
+                            seriesInfo.countBySexHeightRange++;
+                            countBySexHeightRangeMax = Math.max(
+                                countBySexHeightRangeMax, seriesInfo.countBySexHeightRange
+                            );
+                        }
+                    }
+                }
+            }
+
+            var makeStateOption = {};
+
+            makeStateOption[State.HEIGHT_WEIGHT_SINGLE] = function () {
+                var option = {
+                    dataZoom: [{
+                        xAxisIndex: 'all'
+                    }, {
+                        xAxisIndex: 'all'
+                    }],
+                    grid: [{
+                        bottom: 80
+                    }],
+                    xAxis: [{
+                        scale: true,
+                        axisLabel: { formatter: '{value} cm' },
+                        splitLine: { show: false }
+                    }],
+                    yAxis: [{
+                        scale: true,
+                        axisLabel: { formatter: '{value} kg' },
+                        splitLine: { show: false }
+                    }],
+                    series: []
+                };
+                for (var i = 0; i < baseOption.series.length; i++) {
+                    option.series.push({
+                        coordinateSystem: 'cartesian2d',
+                        encode: {
+                            x: DIMENSION_HEIGHT,
+                            y: DIMENSION_WEIGHT,
+                            label: [DIMENSION_HEIGHT, DIMENSION_WEIGHT]
+                        },
+                        xAxisIndex: 0,
+                        yAxisIndex: 0
+                    });
+                }
+                return option;
+            };
+
+            makeStateOption[State.HEIGHT_WEIGHT_SEX_SEP] = function () {
+                var option = {
+                    dataZoom: [{
+                        xAxisIndex: 'all'
+                    }, {
+                        xAxisIndex: 'all'
+                    }],
+                    grid: [{
+                        right: '55%',
+                        bottom: 80,
+                    }, {
+                        left: '55%',
+                        bottom: 80,
+                    }],
+                    xAxis: [{
+                        scale: true,
+                        gridIndex: 0,
+                        axisLabel: { formatter: '{value} cm' },
+                        splitLine: { show: false }
+                    }, {
+                        scale: true,
+                        gridIndex: 1,
+                        axisLabel: { formatter: '{value} cm' },
+                        splitLine: { show: false }
+                    }],
+                    yAxis: [{
+                        scale: true,
+                        gridIndex: 0,
+                        axisLabel: { formatter: '{value} kg' },
+                        splitLine: { show: false }
+                    }, {
+                        scale: true,
+                        gridIndex: 1,
+                        axisLabel: { formatter: '{value} kg' },
+                        splitLine: { show: false }
+                    }],
+                    series: []
+                };
+                for (var i = 0; i < baseOption.series.length; i++) {
+                    var axisIndex = seriesInfoList[i].sex === FEMALE ? 0 : 1;
+                    console.log(axisIndex, seriesInfoList[i]);
+                    option.series.push({
+                        coordinateSystem: 'cartesian2d',
+                        encode: {
+                            x: DIMENSION_HEIGHT,
+                            y: DIMENSION_WEIGHT,
+                            label: [DIMENSION_HEIGHT, DIMENSION_WEIGHT]
+                        },
+                        xAxisIndex: axisIndex,
+                        yAxisIndex: axisIndex
+                    });
+                }
+                return option;
+            };
+
+            makeStateOption[State.COUNT_BY_HEIGHT_RANGE] = function () {
+                var yMax = Math.round(countBySexHeightRangeMax * 1.2);
+
+                var option = {
+                    dataZoom: [{
+                        xAxisIndex: 'none'
+                    }, {
+                        xAxisIndex: 'none'
+                    }],
+                    grid: [{
+                        left: '10%',
+                        width: '20%',
+                        top: 90,
+                        bottom: 80,
+                    }, {
+                        left: '40%',
+                        width: '20%',
+                        top: 90,
+                        bottom: 80,
+                    }, {
+                        left: '75%',
+                        width: '20%',
+                        top: 90,
+                        bottom: 80,
+                    }],
+                    xAxis: [{
+                        type: 'category',
+                        name: HEIGHT_RANGES[0].min + ' cm ~ ' + HEIGHT_RANGES[0].max + ' cm',
+                        nameLocation: 'center',
+                        nameGap: 20,
+                        axisLabel: { show: false },
+                        axisTick: { show: false },
+                        gridIndex: 0,
+                        splitLine: { show: false }
+                    }, {
+                        type: 'category',
+                        name: HEIGHT_RANGES[1].min + ' cm ~ ' + HEIGHT_RANGES[1].max + ' cm',
+                        nameLocation: 'center',
+                        nameGap: 20,
+                        axisLabel: { show: false },
+                        axisTick: { show: false },
+                        gridIndex: 1,
+                        splitLine: { show: false }
+                    }, {
+                        type: 'category',
+                        name: HEIGHT_RANGES[2].min + ' cm ~ ' + HEIGHT_RANGES[2].max + ' cm',
+                        nameLocation: 'center',
+                        nameGap: 20,
+                        axisLabel: { show: false },
+                        axisTick: { show: false },
+                        gridIndex: 2,
+                        splitLine: { show: false }
+                    }],
+                    yAxis: [{
+                        gridIndex: 0,
+                        name: 'persons',
+                        axisLabel: { formatter: '{value}' },
+                        max: yMax,
+                        min: 0,
+                        splitLine: { show: false }
+                    }, {
+                        gridIndex: 1,
+                        name: 'persons',
+                        axisLabel: { formatter: '{value}' },
+                        max: yMax,
+                        min: 0,
+                        splitLine: { show: false }
+                    }, {
+                        gridIndex: 2,
+                        name: 'persons',
+                        axisLabel: { formatter: '{value}' },
+                        max: yMax,
+                        min: 0,
+                        splitLine: { show: false }
+                    }],
+                    series: []
+                };
+
+                for (var i = 0; i < baseOption.series.length; i++) {
+                    var axisIndex = HEIGHT_RANGES.indexOf(seriesInfoList[i].heightRange);
+                    option.series.push({
+                        coordinateSystem: 'cartesian2d',
+                        encode: {
+                            x: DIMENSION_SEX,
+                            y: -1,
+                            label: [DIMENSION_HEIGHT, DIMENSION_WEIGHT]
+                        },
+                        xAxisIndex: axisIndex,
+                        yAxisIndex: axisIndex
+                    });
+                }
+
+                return option;
+            };
+
+            function changeState(state) {
+                currentState = state;
+                var option = makeStateOption[state]();
+                chart.setOption(option, {
+                    replaceMerge: ['grid', 'xAxis', 'yAxis']
+                });
+            }
+
+            var chart = testHelper.create(echarts, 'transition_dots', {
+                title: [
+                    'Transition case:',
+                    'click btns one by one',
+                ],
+                option: baseOption,
+                lazyUpdate: true,
+                height: 600,
+                buttons: [{
+                    text: 'scatter by sex',
+                    onclick: function () {
+                        changeState(State.HEIGHT_WEIGHT_SEX_SEP);
+                    }
+                }, {
+                    text: 'count by height range',
+                    onclick: function () {
+                        changeState(State.COUNT_BY_HEIGHT_RANGE);
+                    }
+                }, {
+                    text: 'one grid',
+                    onclick: function () {
+                        changeState(State.HEIGHT_WEIGHT_SINGLE);
+                    }
+                }]
+            });
+
+            changeState(State.HEIGHT_WEIGHT_SINGLE);
+
+        });
+        </script>
+
+
+
+
+
+
     </body>
 </html>
 


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


[incubator-echarts] 09/16: feature: add test case for id duplicated.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit a3d29cf60002869679d58259b9f9d2b2758bbcec
Author: 100pah <su...@gmail.com>
AuthorDate: Tue Jul 14 18:09:46 2020 +0800

    feature: add test case for id duplicated.
---
 src/model/Global.ts           |  2 +-
 src/util/model.ts             | 32 ++++++++++++++----------
 test/lib/testHelper.js        | 19 ++++++++++----
 test/option-replaceMerge.html | 58 +++++++++++++++++++++++++++++++++++++++++--
 4 files changed, 90 insertions(+), 21 deletions(-)

diff --git a/src/model/Global.ts b/src/model/Global.ts
index ad02e72..5fd20d9 100644
--- a/src/model/Global.ts
+++ b/src/model/Global.ts
@@ -384,7 +384,7 @@ class GlobalModel extends Model<ECUnitOption> {
                 let metNonInner = false;
                 for (let i = realLen - 1; i >= 0; i--) {
                     // Remove options with inner id.
-                    if (opts[i] && !modelUtil.isIdInner(opts[i])) {
+                    if (opts[i] && !modelUtil.isComponentIdInternal(opts[i])) {
                         metNonInner = true;
                     }
                     else {
diff --git a/src/util/model.ts b/src/util/model.ts
index 7e21a2e..e0939a9 100644
--- a/src/util/model.ts
+++ b/src/util/model.ts
@@ -56,6 +56,8 @@ import { isNumeric } from './number';
  */
 const DUMMY_COMPONENT_NAME_PREFIX = 'series\0';
 
+const INTERNAL_COMPONENT_ID_PREFIX = '\0_ec_\0';
+
 /**
  * If value is not array, then translate it to array.
  * @param  {*} value
@@ -242,8 +244,8 @@ function mappingToExistsInNormalMerge<T extends MappingExistingItem>(
                 // Can not match when both ids existing but different.
                 && existing
                 && (existing.id == null || cmptOption.id == null)
-                && !isIdInner(cmptOption)
-                && !isIdInner(existing)
+                && !isComponentIdInternal(cmptOption)
+                && !isComponentIdInternal(existing)
                 && keyExistAndEqual('name', existing, cmptOption)
             ) {
                 result[i].newOption = cmptOption;
@@ -264,7 +266,7 @@ function mappingToExistsInNormalMerge<T extends MappingExistingItem>(
  * Mapping to exists for merge.
  * The mode "replaceMerge" means that:
  * (1) Only the id mapped components will be merged.
- * (2) Other existing components (except inner compoonets) will be removed.
+ * (2) Other existing components (except internal compoonets) will be removed.
  * (3) Other new options will be used to create new component.
  * (4) The index of the existing compoents will not be modified.
  * That means their might be "hole" after the removal.
@@ -286,20 +288,20 @@ function mappingToExistsInReplaceMerge<T extends MappingExistingItem>(
     // contains elided items, which will be ommited.
     for (let index = 0; index < existings.length; index++) {
         const existing = existings[index];
-        let innerExisting: T;
+        let internalExisting: T;
         // Because of replaceMerge, `existing` may be null/undefined.
         if (existing) {
-            if (isIdInner(existing)) {
-                // inner components should not be removed.
-                innerExisting = existing;
+            if (isComponentIdInternal(existing)) {
+                // internal components should not be removed.
+                internalExisting = existing;
             }
-            // Input with inner id is allowed for convenience of some internal usage.
+            // Input with internal id is allowed for convenience of some internal usage.
             // When `existing` is rawOption (see `OptionManager`#`mergeOption`), id might be empty.
             if (existing.id != null) {
                 existingIdIdxMap.set(existing.id, index);
             }
         }
-        result.push({ existing: innerExisting });
+        result.push({ existing: internalExisting });
     }
 
     // Mapping by id if specified.
@@ -350,7 +352,7 @@ function mappingByIndexFinally<T extends MappingExistingItem>(
             return;
         }
 
-        // Find the first place that not mapped by id and not inner component (consider the "hole").
+        // Find the first place that not mapped by id and not internal component (consider the "hole").
         let resultItem;
         while (
             // Be `!resultItem` only when `nextIdx >= mappingResult.length`.
@@ -368,7 +370,7 @@ function mappingByIndexFinally<T extends MappingExistingItem>(
                     && !keyExistAndEqual('id', cmptOption, resultItem.existing)
                 )
                 || resultItem.newOption
-                || isIdInner(resultItem.existing)
+                || isComponentIdInternal(resultItem.existing)
             )
         ) {
             nextIdx++;
@@ -414,6 +416,7 @@ function makeIdAndName(
     each(mapResult, function (item) {
         const opt = item.newOption;
 
+        // Force ensure id not duplicated.
         assert(
             !opt || opt.id == null || !idMap.get(opt.id) || idMap.get(opt.id) === item,
             'id duplicates: ' + (opt && opt.id)
@@ -511,12 +514,15 @@ export function isNameSpecified(componentModel: ComponentModel): boolean {
  * @param {Object} cmptOption
  * @return {boolean}
  */
-export function isIdInner(cmptOption: MappingExistingItem): boolean {
+export function isComponentIdInternal(cmptOption: MappingExistingItem): boolean {
     return cmptOption
         && cmptOption.id != null
-        && makeComparableKey(cmptOption.id).indexOf('\0_ec_\0') === 0;
+        && makeComparableKey(cmptOption.id).indexOf(INTERNAL_COMPONENT_ID_PREFIX) === 0;
 }
 
+export function makeInternalComponentId(idSuffix: string) {
+    return INTERNAL_COMPONENT_ID_PREFIX + idSuffix;
+}
 
 export function setComponentTypeToKeyInfo(
     mappingResult: MappingResult<MappingExistingItem & { subType?: ComponentSubType }>,
diff --git a/test/lib/testHelper.js b/test/lib/testHelper.js
index f5a5f0f..88dc879 100644
--- a/test/lib/testHelper.js
+++ b/test/lib/testHelper.js
@@ -275,10 +275,19 @@
      * `testHelper.printAssert` can be called multiple times for one chart instance.
      * For each call, one result (fail or pass) will be printed.
      *
-     * @param chart {EChartsInstance}
+     * @param chartOrDomId {EChartsInstance | string}
      * @param checkFn {Function} param: a function `assert`.
      */
-    testHelper.printAssert = function (chart, checkerFn) {
+    testHelper.printAssert = function (chartOrDomId, checkerFn) {
+        var hostDOMEl;
+        var chart;
+        if (typeof chartOrDomId === 'string') {
+            hostDOMEl = document.getElementById(chartOrDomId);
+        }
+        else {
+            chart = chartOrDomId;
+            hostDOMEl = chartOrDomId.getDom();
+        }
         var failErr;
         function assert(cond) {
             if (!cond) {
@@ -292,7 +301,7 @@
             console.error(err);
             failErr = err;
         }
-        var printAssertRecord = chart.__printAssertRecord || (chart.__printAssertRecord = []);
+        var printAssertRecord = hostDOMEl.__printAssertRecord || (hostDOMEl.__printAssertRecord = []);
 
         var resultDom = document.createElement('div');
         resultDom.innerHTML = failErr ? 'checked: Fail' : 'checked: Pass';
@@ -305,12 +314,12 @@
             'color: ' + (failErr ? 'red' : 'green') + ';',
         ].join('');
         printAssertRecord.push(resultDom);
-        chart.getDom().appendChild(resultDom);
+        hostDOMEl.appendChild(resultDom);
 
         relayoutResult();
 
         function relayoutResult() {
-            var chartHeight = chart.getHeight();
+            var chartHeight = chart ? chart.getHeight() : hostDOMEl.offsetHeight;
             var lineHeight = Math.min(fontSize + 10, (chartHeight - 20) / printAssertRecord.length);
             for (var i = 0; i < printAssertRecord.length; i++) {
                 var record = printAssertRecord[i];
diff --git a/test/option-replaceMerge.html b/test/option-replaceMerge.html
index e525e7a..1c8ff5f 100644
--- a/test/option-replaceMerge.html
+++ b/test/option-replaceMerge.html
@@ -44,11 +44,12 @@ under the License.
         <div id="main_replaceMerge_add_new_id"></div>
         <div id="main_replaceMerge_add_find_hole"></div>
         <div id="main_normalMerge_add_find_hole"></div>
-        <div id="main_replaceMerge_inner_and_other_cmpt_not_effect"></div>
+        <div id="main_replaceMerge_internal_and_other_cmpt_not_effect"></div>
         <div id="main_replaceMerge_remove_all"></div>
         <div id="main_replaceMerge_reproduce_by_getOption_src"></div>
         <div id="main_replaceMerge_reproduce_by_getOption_tar"></div>
         <div id="main_replaceMerge_if_not_declared_in_option"></div>
+        <div id="main_throw_error_when_id_duplicated"></div>
 
 
 
@@ -627,7 +628,7 @@ under the License.
                 }]
             };
 
-            var chart = testHelper.create(echarts, 'main_replaceMerge_inner_and_other_cmpt_not_effect', {
+            var chart = testHelper.create(echarts, 'main_replaceMerge_internal_and_other_cmpt_not_effect', {
                 title: [
                     'replaceMerge: inner not effect',
                     'click "setOption": a dataZoom.slider added',
@@ -1004,6 +1005,59 @@ under the License.
 
 
 
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option;
+
+            option = {
+                xAxis: [{
+                    type: 'category'
+                }],
+                yAxis: [{
+                }],
+                series: [{
+                    id: 'sb0',
+                    type: 'line',
+                    data: [[11, 22], [33, 44]]
+                }, {
+                    type: 'line',
+                    data: [[11111, 52], [21133, 74]]
+                }, {
+                    id: 'sb0',
+                    type: 'line',
+                    data: [[222233, 1432], [111154, 1552]]
+                }]
+            };
+
+            testHelper.printAssert('main_throw_error_when_id_duplicated', function (assert) {
+                try {
+                    var chart = testHelper.create(echarts, 'main_throw_error_when_id_duplicated', {
+                        title: [
+                            'Check throw error when id duplicated',
+                            'Should show **checked: Pass**'
+                        ],
+                        option: option,
+                        height: 100,
+                    });
+                    assert(false);
+                }
+                catch (err) {
+                    assert(true);
+                }
+            });
+
+        });
+        </script>
+
+
+
+
+
     </body>
 </html>
 


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


[incubator-echarts] 08/16: feature: (1) support axis id reference in dataZoom. (except toolbox dataZoom) (2) enhance dataZoom auto choose axis logic to support multiple setOption. (except toolbox dataZoom) (3) enhance dataZoom to enable axis and dataZoom removal. (except toolbox dataZoom) (4) simplify the code of dataZoom.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 32a4b6626858207865a555874e774e3941c14883
Author: 100pah <su...@gmail.com>
AuthorDate: Tue Jul 14 17:38:24 2020 +0800

    feature:
    (1) support axis id reference in dataZoom. (except toolbox dataZoom)
    (2) enhance dataZoom auto choose axis logic to support multiple setOption. (except toolbox dataZoom)
    (3) enhance dataZoom to enable axis and dataZoom removal. (except toolbox dataZoom)
    (4) simplify the code of dataZoom.
---
 src/component/dataZoom/AxisProxy.ts         |  13 +-
 src/component/dataZoom/DataZoomModel.ts     | 432 +++++++++++-----------------
 src/component/dataZoom/DataZoomView.ts      |   5 +-
 src/component/dataZoom/InsideZoomView.ts    |   2 +-
 src/component/dataZoom/SliderZoomView.ts    |  27 +-
 src/component/dataZoom/dataZoomAction.ts    |  25 +-
 src/component/dataZoom/dataZoomProcessor.ts |  75 +++--
 src/component/dataZoom/helper.ts            | 209 ++++++--------
 test/dataZoom-feature.html                  | 256 +++++++++++++++++
 9 files changed, 594 insertions(+), 450 deletions(-)

diff --git a/src/component/dataZoom/AxisProxy.ts b/src/component/dataZoom/AxisProxy.ts
index b322d91..196c71d 100644
--- a/src/component/dataZoom/AxisProxy.ts
+++ b/src/component/dataZoom/AxisProxy.ts
@@ -105,17 +105,12 @@ class AxisProxy {
 
     getTargetSeriesModels() {
         const seriesModels: SeriesModel[] = [];
-        const ecModel = this.ecModel;
 
-        ecModel.eachSeries(function (seriesModel) {
+        this.ecModel.eachSeries(function (seriesModel) {
             if (helper.isCoordSupported(seriesModel.get('coordinateSystem'))) {
-                const dimName = this._dimName;
-                const axisModel = ecModel.queryComponents({
-                    mainType: dimName + 'Axis',
-                    index: seriesModel.get(dimName + 'AxisIndex' as any),
-                    id: seriesModel.get(dimName + 'AxisId' as any)
-                })[0];
-                if (this._axisIndex === (axisModel && axisModel.componentIndex)) {
+                const axisMainType = helper.getAxisMainType(this._dimName);
+                const axisModel = seriesModel.getReferringComponents(axisMainType, true).models[0];
+                if (axisModel && this._axisIndex === axisModel.componentIndex) {
                     seriesModels.push(seriesModel);
                 }
             }
diff --git a/src/component/dataZoom/DataZoomModel.ts b/src/component/dataZoom/DataZoomModel.ts
index 48ec294..d1fadbe 100644
--- a/src/component/dataZoom/DataZoomModel.ts
+++ b/src/component/dataZoom/DataZoomModel.ts
@@ -17,30 +17,24 @@
 * under the License.
 */
 
-import {__DEV__} from '../../config';
-import * as zrUtil from 'zrender/src/core/util';
+import { each, createHashMap, merge, HashMap, assert } from 'zrender/src/core/util';
 import env from 'zrender/src/core/env';
-import * as modelUtil from '../../util/model';
 import AxisProxy from './AxisProxy';
 import ComponentModel from '../../model/Component';
 import {
     LayoutOrient,
     ComponentOption,
-    Dictionary,
-    LabelOption,
-    SeriesOption,
-    SeriesOnCartesianOptionMixin,
-    SeriesOnPolarOptionMixin,
-    SeriesOnSingleOptionMixin
+    LabelOption
 } from '../../util/types';
 import Model from '../../model/Model';
 import GlobalModel from '../../model/Global';
-import SeriesModel from '../../model/Series';
 import { AxisBaseModel } from '../../coord/AxisBaseModel';
-import { OptionAxisType, AxisBaseOption } from '../../coord/axisCommonTypes';
-import { eachAxisDim } from './helper';
+import {
+    getAxisMainType, DATA_ZOOM_AXIS_DIMENSIONS, DataZoomAxisDimension
+} from './helper';
+import SingleAxisModel from '../../coord/single/AxisModel';
+import { __DEV__ } from '../../config';
 
-const each = zrUtil.each;
 
 export interface DataZoomOption extends ComponentOption {
 
@@ -53,16 +47,21 @@ export interface DataZoomOption extends ComponentOption {
      * Default the first horizontal category axis.
      */
     xAxisIndex?: number | number[]
+    xAxisId?: string | string[]
 
     /**
      * Default the first vertical category axis.
      */
     yAxisIndex?: number | number[]
+    yAxisId?: string | string[]
 
     radiusAxisIndex?: number | number[]
+    radiusAxisId?: string | string[]
     angleAxisIndex?: number | number[]
+    angleAxisId?: string | string[]
 
     singleAxisIndex?: number | number[]
+    singleAxisId?: string | string[]
 
     /**
      * Possible values: 'filter' or 'empty' or 'weakFilter'.
@@ -131,20 +130,27 @@ export interface DataZoomOption extends ComponentOption {
 
 type RangeOption = Pick<DataZoomOption, 'start' | 'end' | 'startValue' | 'endValue'>;
 
-type ExtendedAxisBaseModel = AxisBaseModel & {
+export type DataZoomExtendedAxisBaseModel = AxisBaseModel & {
     __dzAxisProxy: AxisProxy
 };
 
-interface SeriesModelOnAxis extends SeriesModel<
-    SeriesOption & SeriesOnCartesianOptionMixin & SeriesOnPolarOptionMixin & SeriesOnSingleOptionMixin
-> {}
+class DataZoomAxisInfo {
+    indexList: number[] = [];
+    indexMap: boolean[] = [];
+
+    add(axisCmptIdx: number) {
+        this.indexList.push(axisCmptIdx);
+        this.indexMap[axisCmptIdx] = true;
+    }
+}
+export type DataZoomTargetAxisInfoMap = HashMap<DataZoomAxisInfo, DataZoomAxisDimension>;
 
 class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends ComponentModel<Opts> {
     static type = 'dataZoom';
     type = DataZoomModel.type;
 
     static dependencies = [
-        'xAxis', 'yAxis', 'zAxis', 'radiusAxis', 'angleAxis', 'singleAxis', 'series'
+        'xAxis', 'yAxis', 'radiusAxis', 'angleAxis', 'singleAxis', 'series'
     ];
 
 
@@ -158,16 +164,11 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
         end: 100
     };
 
-    /**
-     * key like x_0, y_1
-     */
-    private _dataIntervalByAxis: Dictionary<[number, number]> = {};
-
-    private _dataInfo = {};
+    private _autoThrottle = true;
 
-    private _axisProxies: Dictionary<AxisProxy> = {};
+    private _orient: LayoutOrient;
 
-    private _autoThrottle = true;
+    private _targetAxisInfoMap: DataZoomTargetAxisInfoMap;
 
     /**
      * It is `[rangeModeForMin, rangeModeForMax]`.
@@ -187,11 +188,12 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
      */
     private _rangePropMode: DataZoomOption['rangeMode'] = ['percent', 'percent'];
 
-    textStyleModel: Model<DataZoomOption['textStyle']>;
-
+    /**
+     * @readonly
+     */
     settledOption: Opts;
 
-    init(option: Opts, parentModel: Model, ecModel: GlobalModel) {
+    init(option: Opts, parentModel: Model, ecModel: GlobalModel): void {
 
         const inputRawOption = retrieveRawOption(option);
 
@@ -215,33 +217,25 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
          * (Step)2) click the legend to hide and show a series,
          *     where the new range is calculated by the earsed `startValue` and `endValue`,
          *     which brings incorrect result.
-         *
-         * @readOnly
          */
         this.settledOption = inputRawOption;
 
         this.mergeDefaultAndTheme(option, ecModel);
 
-        this.doInit(inputRawOption);
+        this._doInit(inputRawOption);
     }
 
-    /**
-     * @override
-     */
-    mergeOption(newOption: Opts) {
+    mergeOption(newOption: Opts): void {
         const inputRawOption = retrieveRawOption(newOption);
 
         //FIX #2591
-        zrUtil.merge(this.option, newOption, true);
-        zrUtil.merge(this.settledOption, inputRawOption, true);
+        merge(this.option, newOption, true);
+        merge(this.settledOption, inputRawOption, true);
 
-        this.doInit(inputRawOption);
+        this._doInit(inputRawOption);
     }
 
-    /**
-     * @protected
-     */
-    doInit(inputRawOption: Opts) {
+    private _doInit(inputRawOption: Opts): void {
         const thisOption = this.option;
 
         // Disable realtime view update if canvas is not supported.
@@ -264,201 +258,121 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
             // Otherwise do nothing and use the merge result.
         }, this);
 
-        this.textStyleModel = this.getModel('textStyle');
-
         this._resetTarget();
-
-        this._prepareAxisProxies();
-    }
-
-    private _prepareAxisProxies() {
-        const axisProxies = this._axisProxies;
-
-        this.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel, ecModel) {
-            const axisModel = this.ecModel.getComponent(dimNames.axis, axisIndex);
-
-            // If exists, share axisProxy with other dataZoomModels.
-            const axisProxy = (axisModel as ExtendedAxisBaseModel).__dzAxisProxy || (
-                // Use the first dataZoomModel as the main model of axisProxy.
-                (axisModel as ExtendedAxisBaseModel).__dzAxisProxy = new AxisProxy(
-                    dimNames.name, axisIndex, this, ecModel
-                )
-            );
-            // FIXME
-            // dispose __dzAxisProxy
-
-            axisProxies[dimNames.name + '_' + axisIndex] = axisProxy;
-        }, this);
     }
 
     private _resetTarget() {
-        const thisOption = this.option;
+        const optionOrient = this.get('orient', true);
+        const targetAxisIndexMap = this._targetAxisInfoMap = createHashMap<DataZoomAxisInfo, DataZoomAxisDimension>();
 
-        const autoMode = this._judgeAutoMode();
-
-        eachAxisDim(function (dimNames) {
-            const axisIndexName = dimNames.axisIndex;
-            thisOption[axisIndexName] = modelUtil.normalizeToArray(
-                thisOption[axisIndexName]
-            );
-        }, this);
+        const hasAxisSpecified = this._fillSpecifiedTargetAxis(targetAxisIndexMap);
 
-        if (autoMode === 'axisIndex') {
-            this._autoSetAxisIndex();
+        if (hasAxisSpecified) {
+            this._orient = optionOrient || this._makeAutoOrientByTargetAxis();
         }
-        else if (autoMode === 'orient') {
-            this._autoSetOrient();
+        else {
+            this._orient = optionOrient || 'horizontal';
+            this._fillAutoTargetAxisByOrient(targetAxisIndexMap, this._orient);
         }
     }
 
-    private _judgeAutoMode() {
-        // Auto set only works for setOption at the first time.
-        // The following is user's reponsibility. So using merged
-        // option is OK.
-        const thisOption = this.option;
+    private _fillSpecifiedTargetAxis(targetAxisIndexMap: DataZoomTargetAxisInfoMap): boolean {
+        let hasAxisSpecified = false;
 
-        let hasIndexSpecified = false;
-        eachAxisDim(function (dimNames) {
+        each(DATA_ZOOM_AXIS_DIMENSIONS, function (axisDim) {
+            const refering = this.getReferringComponents(getAxisMainType(axisDim), false);
             // When user set axisIndex as a empty array, we think that user specify axisIndex
             // but do not want use auto mode. Because empty array may be encountered when
             // some error occured.
-            if (thisOption[dimNames.axisIndex] != null) {
-                hasIndexSpecified = true;
+            if (!refering.specified) {
+                return;
             }
+            hasAxisSpecified = true;
+            const axisInfo = new DataZoomAxisInfo();
+            each(refering.models, function (axisModel) {
+                axisInfo.add(axisModel.componentIndex);
+            });
+            targetAxisIndexMap.set(axisDim, axisInfo);
         }, this);
 
-        const orient = thisOption.orient;
-
-        if (orient == null && hasIndexSpecified) {
-            return 'orient';
-        }
-        else if (!hasIndexSpecified) {
-            if (orient == null) {
-                thisOption.orient = 'horizontal';
-            }
-            return 'axisIndex';
-        }
+        return hasAxisSpecified;
     }
 
-    private _autoSetAxisIndex() {
-        let autoAxisIndex = true;
-        const orient = this.get('orient', true);
-        const thisOption = this.option;
+    private _fillAutoTargetAxisByOrient(targetAxisIndexMap: DataZoomTargetAxisInfoMap, orient: LayoutOrient): void {
+        const ecModel = this.ecModel;
+        let needAuto = true;
 
-        if (autoAxisIndex) {
-            // Find axis that parallel to dataZoom as default.
-            const dimName = orient === 'vertical' ? 'y' : 'x';
+        // Find axis that parallel to dataZoom as default.
+        if (needAuto) {
+            const axisDim = orient === 'vertical' ? 'y' : 'x';
+            const axisModels = ecModel.findComponents({ mainType: axisDim + 'Axis' });
+            setParallelAxis(axisModels, axisDim);
+        }
+        // Find axis that parallel to dataZoom as default.
+        if (needAuto) {
+            const axisModels = ecModel.findComponents({
+                mainType: 'singleAxis',
+                filter: (axisModel: SingleAxisModel) => axisModel.get('orient', true) === orient
+            });
+            setParallelAxis(axisModels, 'single');
+        }
 
-            if (this.ecModel.queryComponents({ mainType: dimName + 'Axis' }).length) {
-                thisOption[dimName + 'AxisIndex' as 'xAxisIndex' | 'yAxisIndex'] = [0];
-                autoAxisIndex = false;
+        function setParallelAxis(axisModels: ComponentModel[], axisDim: DataZoomAxisDimension): void {
+            // At least use the first parallel axis as the target axis.
+            const axisModel = axisModels[0];
+            if (!axisModel) {
+                return;
             }
-            else {
-                each(this.ecModel.queryComponents({ mainType: 'singleAxis' }), function (
-                    singleAxisModel: AxisBaseModel<{'orient': LayoutOrient} & AxisBaseOption>
-                ) {
-                    if (autoAxisIndex && singleAxisModel.get('orient', true) === orient) {
-                        thisOption.singleAxisIndex = [singleAxisModel.componentIndex];
-                        autoAxisIndex = false;
+
+            const axisInfo = new DataZoomAxisInfo();
+            axisInfo.add(axisModel.componentIndex);
+            targetAxisIndexMap.set(axisDim, axisInfo);
+            needAuto = false;
+
+            // Find parallel axes in the same grid.
+            if (axisDim === 'x' || axisDim === 'y') {
+                const gridModel = axisModel.getReferringComponents('grid', true).models[0];
+                gridModel && each(axisModels, function (axModel) {
+                    if (axisModel.componentIndex !== axModel.componentIndex
+                        && gridModel === axModel.getReferringComponents('grid', true).models[0]
+                    ) {
+                        axisInfo.add(axModel.componentIndex);
                     }
                 });
             }
         }
 
-        if (autoAxisIndex) {
-            // Find the first category axis as default. (consider polar)
-            eachAxisDim(function (dimNames) {
-                if (!autoAxisIndex) {
+        if (needAuto) {
+            // If no parallel axis, find the first category axis as default. (Also consider polar).
+            each(DATA_ZOOM_AXIS_DIMENSIONS, function (axisDim) {
+                if (needAuto) {
                     return;
                 }
-                const axisIndices = [];
-                const axisModels = this.ecModel.queryComponents({ mainType: dimNames.axis });
-                if (axisModels.length && !axisIndices.length) {
-                    for (let i = 0, len = axisModels.length; i < len; i++) {
-                        if (axisModels[i].get('type') === 'category') {
-                            axisIndices.push(i);
-                        }
-                    }
-                }
-                thisOption[dimNames.axisIndex] = axisIndices;
-                if (axisIndices.length) {
-                    autoAxisIndex = false;
-                }
-            }, this);
-        }
-
-        if (autoAxisIndex) {
-            // FIXME
-            // 这里是兼容ec2的写法(没指定xAxisIndex和yAxisIndex时把scatter和双数值轴折柱纳入dataZoom控制),
-            // 但是实际是否需要Grid.js#getScaleByOption来判断(考虑time,log等axis type)?
-
-            // If both dataZoom.xAxisIndex and dataZoom.yAxisIndex is not specified,
-            // dataZoom component auto adopts series that reference to
-            // both xAxis and yAxis which type is 'value'.
-            this.ecModel.eachSeries(function (seriesModel: SeriesModelOnAxis) {
-                if (this._isSeriesHasAllAxesTypeOf(seriesModel, 'value')) {
-                    eachAxisDim(function (dimNames) {
-                        const axisIndices = thisOption[dimNames.axisIndex] as number[]; // Has been normalized to array
-
-                        let axisIndex = seriesModel.get(dimNames.axisIndex);
-                        const axisId = seriesModel.get(dimNames.axisId);
-
-                        const axisModel = seriesModel.ecModel.queryComponents({
-                            mainType: dimNames.axis,
-                            index: axisIndex,
-                            id: axisId
-                        })[0];
-
-                        if (__DEV__) {
-                            if (!axisModel) {
-                                throw new Error(
-                                    dimNames.axis + ' "' + zrUtil.retrieve<number | string>(
-                                        axisIndex,
-                                        axisId,
-                                        0
-                                    ) + '" not found'
-                                );
-                            }
-                        }
-                        axisIndex = axisModel.componentIndex;
-
-                        if (zrUtil.indexOf(axisIndices, axisIndex) < 0) {
-                            axisIndices.push(axisIndex);
-                        }
-                    });
+                const axisModels = ecModel.findComponents({
+                    mainType: getAxisMainType(axisDim),
+                    filter: (axisModel: SingleAxisModel) => axisModel.get('type', true) === 'category'
+                });
+                if (axisModels[0]) {
+                    const axisInfo = new DataZoomAxisInfo();
+                    axisInfo.add(axisModels[0].componentIndex);
+                    targetAxisIndexMap.set(axisDim, axisInfo);
                 }
             }, this);
         }
     }
 
-    private _autoSetOrient() {
+    private _makeAutoOrientByTargetAxis(): LayoutOrient {
         let dim: string;
 
         // Find the first axis
-        this.eachTargetAxis(function (dimNames) {
-            !dim && (dim = dimNames.name);
+        this.eachTargetAxis(function (axisDim) {
+            !dim && (dim = axisDim);
         }, this);
 
-        this.option.orient = dim === 'y' ? 'vertical' : 'horizontal';
+        return dim === 'y' ? 'vertical' : 'horizontal';
     }
 
-    private _isSeriesHasAllAxesTypeOf(seriesModel: SeriesModelOnAxis, axisType: OptionAxisType) {
-        // FIXME
-        // Depends on that series.xAxisIndex series.yAxisIndex are specified.
-
-        let is = true;
-        eachAxisDim(function (dimNames) {
-            const seriesAxisIndex = seriesModel.get(dimNames.axisIndex);
-            const axisModel = this.ecModel.getComponent(dimNames.axis, seriesAxisIndex);
-
-            if (!axisModel || axisModel.get('type') !== axisType) {
-                is = false;
-            }
-        }, this);
-        return is;
-    }
-
-    private _setDefaultThrottle(inputRawOption: DataZoomOption) {
+    private _setDefaultThrottle(inputRawOption: DataZoomOption): void {
         // When first time user set throttle, auto throttle ends.
         if (inputRawOption.hasOwnProperty('throttle')) {
             this._autoThrottle = false;
@@ -471,7 +385,7 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
         }
     }
 
-    private _updateRangeUse(inputRawOption: RangeOption) {
+    private _updateRangeUse(inputRawOption: RangeOption): void {
         const rangePropMode = this._rangePropMode;
         const rangeModeInOption = this.get('rangeMode');
 
@@ -494,17 +408,13 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
         });
     }
 
-    /**
-     * @public
-     */
-    getFirstTargetAxisModel() {
+    getFirstTargetAxisModel(): AxisBaseModel {
         let firstAxisModel: AxisBaseModel;
-        eachAxisDim(function (dimNames) {
+        this.eachTargetAxis(function (axisDim, axisIndex) {
             if (firstAxisModel == null) {
-                const indices = this.get(dimNames.axisIndex) as number[]; // Has been normalized to array
-                if (indices.length) {
-                    firstAxisModel = this.ecModel.getComponent(dimNames.axis, indices[0]) as AxisBaseModel;
-                }
+                firstAxisModel = this.ecModel.getComponent(
+                    getAxisMainType(axisDim), axisIndex
+                ) as AxisBaseModel;
             }
         }, this);
 
@@ -512,50 +422,50 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
     }
 
     /**
-     * @public
      * @param {Function} callback param: axisModel, dimNames, axisIndex, dataZoomModel, ecModel
      */
     eachTargetAxis<Ctx>(
         callback: (
             this: Ctx,
-            dimNames: Parameters<Parameters<typeof eachAxisDim>[0]>[0],
-            axisIndex: number,
-            dataZoomModel: this,
-            ecModel: GlobalModel
+            axisDim: DataZoomAxisDimension,
+            axisIndex: number
         ) => void,
         context?: Ctx
-    ) {
-        const ecModel = this.ecModel;
-        eachAxisDim(function (dimNames) {
-            each(
-                this.get(dimNames.axisIndex) as number[],   // Has been normalized to array
-                function (axisIndex) {
-                    callback.call(context, dimNames, axisIndex, this, ecModel);
-                },
-                this
-            );
-        }, this);
+    ): void {
+        this._targetAxisInfoMap.each(function (axisInfo, axisDim) {
+            each(axisInfo.indexList, function (axisIndex) {
+                callback.call(context, axisDim, axisIndex);
+            });
+        });
     }
 
     /**
      * @return If not found, return null/undefined.
      */
-    getAxisProxy(dimName: string, axisIndex: number): AxisProxy {
-        return this._axisProxies[dimName + '_' + axisIndex];
+    getAxisProxy(axisDim: DataZoomAxisDimension, axisIndex: number): AxisProxy {
+        const axisModel = this.getAxisModel(axisDim, axisIndex);
+        if (axisModel) {
+            return (axisModel as DataZoomExtendedAxisBaseModel).__dzAxisProxy;
+        }
     }
 
     /**
      * @return If not found, return null/undefined.
      */
-    getAxisModel(dimName: string, axisIndex: number): AxisBaseModel {
-        const axisProxy = this.getAxisProxy(dimName, axisIndex);
-        return axisProxy && axisProxy.getAxisModel();
+    getAxisModel(axisDim: DataZoomAxisDimension, axisIndex: number): AxisBaseModel {
+        if (__DEV__) {
+            assert(axisDim && axisIndex != null);
+        }
+        const axisInfo = this._targetAxisInfoMap.get(axisDim);
+        if (axisInfo && axisInfo.indexMap[axisIndex]) {
+            return this.ecModel.getComponent(getAxisMainType(axisDim), axisIndex) as AxisBaseModel;
+        }
     }
 
     /**
      * If not specified, set to undefined.
      */
-    setRawRange(opt: RangeOption) {
+    setRawRange(opt: RangeOption): void {
         const thisOption = this.option;
         const settledOption = this.settledOption;
         each([['start', 'startValue'], ['end', 'endValue']] as const, function (names) {
@@ -577,18 +487,14 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
         this._updateRangeUse(opt);
     }
 
-    setCalculatedRange(opt: RangeOption) {
+    setCalculatedRange(opt: RangeOption): void {
         const option = this.option;
         each(['start', 'startValue', 'end', 'endValue'] as const, function (name) {
             option[name] = opt[name];
         });
     }
 
-    /**
-     * @public
-     * @return {Array.<number>} [startPercent, endPercent]
-     */
-    getPercentRange() {
+    getPercentRange(): number[] {
         const axisProxy = this.findRepresentativeAxisProxy();
         if (axisProxy) {
             return axisProxy.getDataPercentWindow();
@@ -596,58 +502,64 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
     }
 
     /**
-     * @public
      * For example, chart.getModel().getComponent('dataZoom').getValueRange('y', 0);
      *
      * @return [startValue, endValue] value can only be '-' or finite number.
      */
-    getValueRange(axisDimName: string, axisIndex: number): [number, number] {
-        if (axisDimName == null && axisIndex == null) {
+    getValueRange(axisDim: DataZoomAxisDimension, axisIndex: number): number[] {
+        if (axisDim == null && axisIndex == null) {
             const axisProxy = this.findRepresentativeAxisProxy();
             if (axisProxy) {
                 return axisProxy.getDataValueWindow();
             }
         }
         else {
-            return this.getAxisProxy(axisDimName, axisIndex).getDataValueWindow();
+            return this.getAxisProxy(axisDim, axisIndex).getDataValueWindow();
         }
     }
 
     /**
-     * @public
      * @param axisModel If axisModel given, find axisProxy
      *      corresponding to the axisModel
      */
     findRepresentativeAxisProxy(axisModel?: AxisBaseModel): AxisProxy {
         if (axisModel) {
-            return (axisModel as ExtendedAxisBaseModel).__dzAxisProxy;
+            return (axisModel as DataZoomExtendedAxisBaseModel).__dzAxisProxy;
         }
 
         // Find the first hosted axisProxy
-        const axisProxies = this._axisProxies;
-        for (const key in axisProxies) {
-            if (axisProxies.hasOwnProperty(key) && axisProxies[key].hostedBy(this)) {
-                return axisProxies[key];
+        let firstProxy;
+        const axisDimList = this._targetAxisInfoMap.keys();
+        for (let i = 0; i < axisDimList.length; i++) {
+            const axisDim = axisDimList[i];
+            const axisInfo = this._targetAxisInfoMap.get(axisDim);
+            for (let j = 0; j < axisInfo.indexList.length; j++) {
+                const proxy = this.getAxisProxy(axisDim, axisInfo.indexList[j]);
+                if (proxy.hostedBy(this)) {
+                    return proxy;
+                }
+                if (!firstProxy) {
+                    firstProxy = proxy;
+                }
             }
         }
 
-        // If no hosted axis find not hosted axisProxy.
-        // Consider this case: dataZoomModel1 and dataZoomModel2 control the same axis,
-        // and the option.start or option.end settings are different. The percentRange
-        // should follow axisProxy.
-        // (We encounter this problem in toolbox data zoom.)
-        for (const key in axisProxies) {
-            if (axisProxies.hasOwnProperty(key) && !axisProxies[key].hostedBy(this)) {
-                return axisProxies[key];
-            }
-        }
+        // If no hosted proxy found, still need to return a proxy.
+        // This case always happens in toolbox dataZoom, where axes are all hosted by
+        // other dataZooms.
+        return firstProxy;
     }
 
-    /**
-     * @return {Array.<string>}
-     */
-    getRangePropMode() {
-        return this._rangePropMode.slice();
+    getRangePropMode(): DataZoomModel['_rangePropMode'] {
+        return this._rangePropMode.slice() as DataZoomModel['_rangePropMode'];
+    }
+
+    getOrient(): LayoutOrient {
+        if (__DEV__) {
+            // Should not be called before initialized.
+            assert(this._orient);
+        }
+        return this._orient;
     }
 
 }
diff --git a/src/component/dataZoom/DataZoomView.ts b/src/component/dataZoom/DataZoomView.ts
index 85d291f..8f9b285 100644
--- a/src/component/dataZoom/DataZoomView.ts
+++ b/src/component/dataZoom/DataZoomView.ts
@@ -24,6 +24,7 @@ import ExtensionAPI from '../../ExtensionAPI';
 import { AxisBaseModel } from '../../coord/AxisBaseModel';
 import { Dictionary } from '../../util/types';
 import { CoordinateSystemHostModel } from '../../coord/CoordinateSystem';
+import { getAxisMainType } from './helper';
 
 export interface CoordInfo {
     model: CoordinateSystemHostModel
@@ -68,8 +69,8 @@ class DataZoomView extends ComponentView {
         const ecModel = this.ecModel;
         const coordSysLists: Dictionary<CoordInfo[]> = {};
 
-        dataZoomModel.eachTargetAxis(function (dimNames, axisIndex) {
-            const axisModel = ecModel.getComponent(dimNames.axis, axisIndex) as AxisBaseModel;
+        dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
+            const axisModel = ecModel.getComponent(getAxisMainType(axisDim), axisIndex) as AxisBaseModel;
             if (axisModel) {
                 const coordModel = axisModel.getCoordSysModel();
                 coordModel && save(
diff --git a/src/component/dataZoom/InsideZoomView.ts b/src/component/dataZoom/InsideZoomView.ts
index 634ccbf..41f99e7 100644
--- a/src/component/dataZoom/InsideZoomView.ts
+++ b/src/component/dataZoom/InsideZoomView.ts
@@ -41,7 +41,7 @@ class InsideZoomView extends DataZoomView {
      * 'throttle' is used in this.dispatchAction, so we save range
      * to avoid missing some 'pan' info.
      */
-    range: [number, number];
+    range: number[];
 
     /**
      * @override
diff --git a/src/component/dataZoom/SliderZoomView.ts b/src/component/dataZoom/SliderZoomView.ts
index bfd3d7f..1b47172 100644
--- a/src/component/dataZoom/SliderZoomView.ts
+++ b/src/component/dataZoom/SliderZoomView.ts
@@ -27,13 +27,16 @@ import * as layout from '../../util/layout';
 import sliderMove from '../helper/sliderMove';
 import GlobalModel from '../../model/Global';
 import ExtensionAPI from '../../ExtensionAPI';
-import { LayoutOrient, Payload, ZRTextVerticalAlign, ZRTextAlign, ZRElementEvent, ParsedValue } from '../../util/types';
+import {
+    LayoutOrient, Payload, ZRTextVerticalAlign, ZRTextAlign, ZRElementEvent, ParsedValue
+} from '../../util/types';
 import SliderZoomModel from './SliderZoomModel';
 import ComponentView from '../../view/Component';
 import { RectLike } from 'zrender/src/core/BoundingRect';
 import Axis from '../../coord/Axis';
 import SeriesModel from '../../model/Series';
 import { AxisBaseModel } from '../../coord/AxisBaseModel';
+import { getAxisMainType } from './helper';
 
 const Rect = graphic.Rect;
 
@@ -64,17 +67,17 @@ class SliderZoomView extends DataZoomView {
 
     private _orient: LayoutOrient;
 
-    private _range: [number, number];
+    private _range: number[];
 
     /**
      * [coord of the first handle, coord of the second handle]
      */
-    private _handleEnds: [number, number];
+    private _handleEnds: number[];
 
     /**
      * [length, thick]
      */
-    private _size: [number, number];
+    private _size: number[];
 
     private _handleWidth: number;
 
@@ -118,7 +121,7 @@ class SliderZoomView extends DataZoomView {
             'fixRate'
         );
 
-        this._orient = dataZoomModel.get('orient');
+        this._orient = dataZoomModel.getOrient();
 
         if (this.dataZoomModel.get('show') === false) {
             this.group.removeAll();
@@ -392,9 +395,9 @@ class SliderZoomView extends DataZoomView {
         let result: SliderZoomView['_dataShadowInfo'];
         const ecModel = this.ecModel;
 
-        dataZoomModel.eachTargetAxis(function (dimNames, axisIndex) {
+        dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
             const seriesModels = dataZoomModel
-                .getAxisProxy(dimNames.name, axisIndex)
+                .getAxisProxy(axisDim, axisIndex)
                 .getTargetSeriesModels();
 
             each(seriesModels, function (seriesModel) {
@@ -409,8 +412,10 @@ class SliderZoomView extends DataZoomView {
                     return;
                 }
 
-                const thisAxis = (ecModel.getComponent(dimNames.axis, axisIndex) as AxisBaseModel).axis;
-                let otherDim = getOtherDim(dimNames.name);
+                const thisAxis = (
+                    ecModel.getComponent(getAxisMainType(axisDim), axisIndex) as AxisBaseModel
+                ).axis;
+                let otherDim = getOtherDim(axisDim);
                 let otherAxisInverse;
                 const coordSys = seriesModel.coordinateSystem;
 
@@ -423,7 +428,7 @@ class SliderZoomView extends DataZoomView {
                 result = {
                     thisAxis: thisAxis,
                     series: seriesModel,
-                    thisDim: dimNames.name,
+                    thisDim: axisDim,
                     otherDim: otherDim,
                     otherAxisInverse: otherAxisInverse
                 };
@@ -504,7 +509,7 @@ class SliderZoomView extends DataZoomView {
 
             barGroup.add(handles[handleIndex] = path);
 
-            const textStyleModel = dataZoomModel.textStyleModel;
+            const textStyleModel = dataZoomModel.getModel('textStyle');
 
             this.group.add(
                 handleLabels[handleIndex] = new graphic.Text({
diff --git a/src/component/dataZoom/dataZoomAction.ts b/src/component/dataZoom/dataZoomAction.ts
index 5223b0d..11c50f2 100644
--- a/src/component/dataZoom/dataZoomAction.ts
+++ b/src/component/dataZoom/dataZoomAction.ts
@@ -20,33 +20,14 @@
 import * as echarts from '../../echarts';
 import * as zrUtil from 'zrender/src/core/util';
 import GlobalModel from '../../model/Global';
-import { createLinkedNodesFinder, eachAxisDim } from './helper';
-import DataZoomModel from './DataZoomModel';
+import { findEffectedDataZooms } from './helper';
 
 
 echarts.registerAction('dataZoom', function (payload, ecModel: GlobalModel) {
 
-    const linkedNodesFinder = createLinkedNodesFinder(
-        zrUtil.bind(ecModel.eachComponent, ecModel, 'dataZoom' as any),
-        eachAxisDim,
-        function (model: DataZoomModel, dimNames) {
-            // Has been normalized to array
-            return model.get(dimNames.axisIndex) as number[];
-        }
-    );
+    const effectedModels = findEffectedDataZooms(ecModel, payload);
 
-    const effectedModels: DataZoomModel[] = [];
-
-    ecModel.eachComponent(
-        {mainType: 'dataZoom', query: payload},
-        function (model: DataZoomModel, index: number) {
-            effectedModels.push.apply(
-                effectedModels, linkedNodesFinder(model).nodes
-            );
-        }
-    );
-
-    zrUtil.each(effectedModels, function (dataZoomModel, index) {
+    zrUtil.each(effectedModels, function (dataZoomModel) {
         dataZoomModel.setRawRange({
             start: payload.start,
             end: payload.end,
diff --git a/src/component/dataZoom/dataZoomProcessor.ts b/src/component/dataZoom/dataZoomProcessor.ts
index 960d62c..7404abf 100644
--- a/src/component/dataZoom/dataZoomProcessor.ts
+++ b/src/component/dataZoom/dataZoomProcessor.ts
@@ -20,7 +20,10 @@
 import * as echarts from '../../echarts';
 import {createHashMap, each} from 'zrender/src/core/util';
 import SeriesModel from '../../model/Series';
-import DataZoomModel from './DataZoomModel';
+import DataZoomModel, { DataZoomExtendedAxisBaseModel } from './DataZoomModel';
+import { getAxisMainType, DataZoomAxisDimension } from './helper';
+import AxisProxy from './AxisProxy';
+
 
 echarts.registerProcessor(echarts.PRIORITY.PROCESSOR.FILTER, {
 
@@ -28,23 +31,49 @@ echarts.registerProcessor(echarts.PRIORITY.PROCESSOR.FILTER, {
     // there is a line series and a pie series, it is better not to update the
     // line series if only pie series is needed to be updated.
     getTargetSeries: function (ecModel) {
-        const seriesModelMap = createHashMap<SeriesModel>();
 
-        ecModel.eachComponent('dataZoom', function (dataZoomModel: DataZoomModel) {
-            dataZoomModel.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel) {
-                const axisProxy = dataZoomModel.getAxisProxy(dimNames.name, axisIndex);
-                each(axisProxy.getTargetSeriesModels(), function (seriesModel) {
-                    seriesModelMap.set(seriesModel.uid, seriesModel);
+        function eachAxisModel(
+            cb: (
+                axisDim: DataZoomAxisDimension,
+                axisIndex: number,
+                axisModel: DataZoomExtendedAxisBaseModel,
+                dataZoomModel: DataZoomModel
+            ) => void
+        ) {
+            ecModel.eachComponent('dataZoom', function (dataZoomModel: DataZoomModel) {
+                dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
+                    const axisModel = ecModel.getComponent(getAxisMainType(axisDim), axisIndex);
+                    cb(axisDim, axisIndex, axisModel as DataZoomExtendedAxisBaseModel, dataZoomModel);
                 });
             });
+        }
+        // FIXME: it brings side-effect to `getTargetSeries`.
+        // Prepare axis proxies.
+        eachAxisModel(function (axisDim, axisIndex, axisModel, dataZoomModel) {
+            // dispose all last axis proxy, in case that some axis are deleted.
+            axisModel.__dzAxisProxy = null;
+        });
+        const proxyList: AxisProxy[] = [];
+        eachAxisModel(function (axisDim, axisIndex, axisModel, dataZoomModel) {
+            // Different dataZooms may constrol the same axis. In that case,
+            // an axisProxy serves both of them.
+            if (!axisModel.__dzAxisProxy) {
+                // Use the first dataZoomModel as the main model of axisProxy.
+                axisModel.__dzAxisProxy = new AxisProxy(axisDim, axisIndex, dataZoomModel, ecModel);
+                proxyList.push(axisModel.__dzAxisProxy);
+            }
+        });
+
+        const seriesModelMap = createHashMap<SeriesModel>();
+        each(proxyList, function (axisProxy) {
+            each(axisProxy.getTargetSeriesModels(), function (seriesModel) {
+                seriesModelMap.set(seriesModel.uid, seriesModel);
+            });
         });
 
         return seriesModelMap;
     },
 
-    // FIXME:TS never used, so comment it
-    // modifyOutputEnd: true,
-
     // Consider appendData, where filter should be performed. Because data process is
     // in block mode currently, it is not need to worry about that the overallProgress
     // execute every frame.
@@ -54,8 +83,8 @@ echarts.registerProcessor(echarts.PRIORITY.PROCESSOR.FILTER, {
             // We calculate window and reset axis here but not in model
             // init stage and not after action dispatch handler, because
             // reset should be called after seriesData.restoreData.
-            dataZoomModel.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel) {
-                dataZoomModel.getAxisProxy(dimNames.name, axisIndex).reset(dataZoomModel);
+            dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
+                dataZoomModel.getAxisProxy(axisDim, axisIndex).reset(dataZoomModel);
             });
 
             // Caution: data zoom filtering is order sensitive when using
@@ -72,8 +101,8 @@ echarts.registerProcessor(echarts.PRIORITY.PROCESSOR.FILTER, {
             // while sliding y-dataZoom will only change the range of yAxis.
             // So we should filter x-axis after reset x-axis immediately,
             // and then reset y-axis and filter y-axis.
-            dataZoomModel.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel) {
-                dataZoomModel.getAxisProxy(dimNames.name, axisIndex).filterData(dataZoomModel, api);
+            dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
+                dataZoomModel.getAxisProxy(axisDim, axisIndex).filterData(dataZoomModel, api);
             });
         });
 
@@ -81,15 +110,17 @@ echarts.registerProcessor(echarts.PRIORITY.PROCESSOR.FILTER, {
             // Fullfill all of the range props so that user
             // is able to get them from chart.getOption().
             const axisProxy = dataZoomModel.findRepresentativeAxisProxy();
-            const percentRange = axisProxy.getDataPercentWindow();
-            const valueRange = axisProxy.getDataValueWindow();
+            if (axisProxy) {
+                const percentRange = axisProxy.getDataPercentWindow();
+                const valueRange = axisProxy.getDataValueWindow();
 
-            dataZoomModel.setCalculatedRange({
-                start: percentRange[0],
-                end: percentRange[1],
-                startValue: valueRange[0],
-                endValue: valueRange[1]
-            });
+                dataZoomModel.setCalculatedRange({
+                    start: percentRange[0],
+                    end: percentRange[1],
+                    startValue: valueRange[0],
+                    endValue: valueRange[1]
+                });
+            }
         });
     }
 });
diff --git a/src/component/dataZoom/helper.ts b/src/component/dataZoom/helper.ts
index ef364b1..d36a783 100644
--- a/src/component/dataZoom/helper.ts
+++ b/src/component/dataZoom/helper.ts
@@ -17,9 +17,11 @@
 * under the License.
 */
 
-import * as zrUtil from 'zrender/src/core/util';
-import * as formatUtil from '../../util/format';
-import { Dictionary } from '../../util/types';
+import { Payload, DimensionName } from '../../util/types';
+import GlobalModel from '../../model/Global';
+import DataZoomModel from './DataZoomModel';
+import { indexOf, createHashMap, assert } from 'zrender/src/core/util';
+import { __DEV__ } from '../../config';
 
 
 export interface DataZoomPayloadBatchItem {
@@ -30,148 +32,109 @@ export interface DataZoomPayloadBatchItem {
     endValue?: number
 }
 
-const AXIS_DIMS = ['x', 'y', 'z', 'radius', 'angle', 'single'] as const;
+export const DATA_ZOOM_AXIS_DIMENSIONS = [
+    'x', 'y', 'radius', 'angle', 'single'
+] as const;
+export type DataZoomAxisDimension =
+    'x' | 'y' | 'radius' | 'angle' | 'single';
+type DataZoomAxisMainType =
+    'xAxis' | 'yAxis' | 'radiusAxis' | 'angleAxis' | 'singleAxis';
+type DataZoomAxisIndexPropName =
+    'xAxisIndex' | 'yAxisIndex' | 'radiusAxisIndex' | 'angleAxisIndex' | 'singleAxisIndex';
+type DataZoomAxisIdPropName =
+    'xAxisId' | 'yAxisId' | 'radiusAxisId' | 'angleAxisId' | 'singleAxisId';
+
 // Supported coords.
 // FIXME: polar has been broken (but rarely used).
 const COORDS = ['cartesian2d', 'polar', 'singleAxis'] as const;
 
 export function isCoordSupported(coordType: string) {
-    return zrUtil.indexOf(COORDS, coordType) >= 0;
+    return indexOf(COORDS, coordType) >= 0;
 }
 
-type AxisAttrMap = {
-    // TODO zAxis ?
-    'axisIndex': 'xAxisIndex' | 'yAxisIndex' | 'radiusAxisIndex' | 'angleAxisIndex' | 'singleAxisIndex'
-    'axis': 'xAxis' | 'yAxis' | 'radiusAxis' | 'angleAxis' | 'singleAxis'
-    'axisId': 'xAxisId' | 'yAxisId' | 'radiusAxisId' | 'angleAxisId' | 'singleAxisId'
-};
-
-/**
- * Create "each" method to iterate names.
- */
-function createNameEach<T extends string>(names: readonly T[]) {
-    const attrs = ['axisIndex', 'axis', 'axisId'] as const;
-    const capitalNames = zrUtil.map(names.slice(), formatUtil.capitalFirst);
-    const capitalAttrs = zrUtil.map((attrs || []).slice(), formatUtil.capitalFirst);
-
-    type NameObj = {
-        name: T,
-        capital: string,
-        axisIndex: AxisAttrMap['axisIndex']
-        axis: AxisAttrMap['axis']
-        axisId: AxisAttrMap['axisId']
-    };
-    return function<Ctx> (
-        callback: (this: Ctx, nameObj: NameObj) => void,
-        context?: Ctx
-    ) {
-        zrUtil.each(names, function (name, index) {
-            const nameObj = {
-                name: name,
-                capital: capitalNames[index]
-            } as NameObj;
-
-            for (let j = 0; j < attrs.length; j++) {
-                nameObj[attrs[j]] = (name + capitalAttrs[j]) as never;
-            }
+export function getAxisMainType(axisDim: DimensionName): DataZoomAxisMainType {
+    if (__DEV__) {
+        assert(axisDim);
+    }
+    return axisDim + 'Axis' as DataZoomAxisMainType;
+}
 
-            callback.call(context, nameObj);
-        });
-    };
+export function getAxisIndexPropName(axisDim: DimensionName): DataZoomAxisIndexPropName {
+    if (__DEV__) {
+        assert(axisDim);
+    }
+    return axisDim + 'AxisIndex' as DataZoomAxisIndexPropName;
 }
 
-/**
- * Iterate each dimension name.
- *
- * @public
- * @param callback The parameter is like:
- *                            {
- *                                name: 'angle',
- *                                capital: 'Angle',
- *                                axis: 'angleAxis',
- *                                axisIndex: 'angleAxisIndex',
- *                                index: 'angleIndex'
- *                            }
- * @param context
- */
-export const eachAxisDim = createNameEach(AXIS_DIMS);
+export function getAxisIdPropName(axisDim: DimensionName): DataZoomAxisIdPropName {
+    if (__DEV__) {
+        assert(axisDim);
+    }
+    return axisDim + 'AxisId' as DataZoomAxisIdPropName;
+}
 
 /**
- * If tow dataZoomModels has the same axis controlled, we say that they are 'linked'.
- * dataZoomModels and 'links' make up one or more graphics.
- * This function finds the graphic where the source dataZoomModel is in.
- *
- * @public
- * @param forEachNode Node iterator.
- * @param forEachEdgeType edgeType iterator
- * @param edgeIdGetter Giving node and edgeType, return an array of edge id.
- * @return Input: sourceNode, Output: Like {nodes: [], dims: {}}
+ * If two dataZoomModels has the same axis controlled, we say that they are 'linked'.
+ * This function finds all linked dataZoomModels start from the given payload.
  */
-export function createLinkedNodesFinder<N, E extends {name: string}>(
-    forEachNode: (cb: (node: N) => void) => void,
-    forEachEdgeType: (cb: (edge: E) => void) => void,
-    edgeIdGetter: (node: N, edge: E) => number[]
-) {
-
-    type Result = {
-        nodes: N[]
-        // key: edgeType.name, value: Object (key: edge id, value: boolean).
-        records: Dictionary<Dictionary<boolean>>
-    };
-
-    return function (sourceNode: N) {
-        const result: Result = {
-            nodes: [],
-            records: {}
-        };
-
-        forEachEdgeType(function (edgeType) {
-            result.records[edgeType.name] = {};
-        });
-
-        if (!sourceNode) {
-            return result;
-        }
-
-        absorb(sourceNode, result);
-
-        let existsLink;
-        do {
-            existsLink = false;
-            forEachNode(processSingleNode);
-        }
-        while (existsLink);
-
-        function processSingleNode(node: N) {
-            if (!isNodeAbsorded(node, result) && isLinked(node, result)) {
-                absorb(node, result);
-                existsLink = true;
+export function findEffectedDataZooms(ecModel: GlobalModel, payload: Payload): DataZoomModel[] {
+
+    // Key: `DataZoomAxisDimension`
+    const axisRecords = createHashMap<boolean[], DataZoomAxisDimension>();
+    const effectedModels: DataZoomModel[] = [];
+    // Key: uid of dataZoomModel
+    const effectedModelMap = createHashMap<boolean>();
+
+    // Find the dataZooms specified by payload.
+    ecModel.eachComponent(
+        { mainType: 'dataZoom', query: payload },
+        function (dataZoomModel: DataZoomModel) {
+            if (!effectedModelMap.get(dataZoomModel.uid)) {
+                addToEffected(dataZoomModel);
             }
         }
+    );
+
+    // Start from the given dataZoomModels, travel the graph to find
+    // all of the linked dataZoom models.
+    let foundNewLink;
+    do {
+        foundNewLink = false;
+        ecModel.eachComponent('dataZoom', processSingle);
+    }
+    while (foundNewLink);
 
-        return result;
-    };
+    function processSingle(dataZoomModel: DataZoomModel): void {
+        if (!effectedModelMap.get(dataZoomModel.uid) && isLinked(dataZoomModel)) {
+            addToEffected(dataZoomModel);
+            foundNewLink = true;
+        }
+    }
 
-    function isNodeAbsorded(node: N, result: Result) {
-        return zrUtil.indexOf(result.nodes, node) >= 0;
+    function addToEffected(dataZoom: DataZoomModel): void {
+        effectedModelMap.set(dataZoom.uid, true);
+        effectedModels.push(dataZoom);
+        markAxisControlled(dataZoom);
     }
 
-    function isLinked(node: N, result: Result) {
-        let hasLink = false;
-        forEachEdgeType(function (edgeType: E) {
-            zrUtil.each(edgeIdGetter(node, edgeType) || [], function (edgeId) {
-                result.records[edgeType.name][edgeId] && (hasLink = true);
-            });
+    function isLinked(dataZoomModel: DataZoomModel): boolean {
+        let isLink = false;
+        dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
+            const axisIdxArr = axisRecords.get(axisDim);
+            if (axisIdxArr && axisIdxArr[axisIndex]) {
+                isLink = true;
+            }
         });
-        return hasLink;
+        return isLink;
     }
 
-    function absorb(node: N, result: Result) {
-        result.nodes.push(node);
-        forEachEdgeType(function (edgeType: E) {
-            zrUtil.each(edgeIdGetter(node, edgeType) || [], function (edgeId) {
-                result.records[edgeType.name][edgeId] = true;
-            });
+    function markAxisControlled(dataZoomModel: DataZoomModel) {
+        dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
+            (
+                axisRecords.get(axisDim) || axisRecords.set(axisDim, [])
+            )[axisIndex] = true;
         });
     }
+
+    return effectedModels;
 }
diff --git a/test/dataZoom-feature.html b/test/dataZoom-feature.html
new file mode 100644
index 0000000..f93fc4b
--- /dev/null
+++ b/test/dataZoom-feature.html
@@ -0,0 +1,256 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+
+<html>
+    <head>
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <script src="lib/esl.js"></script>
+        <script src="lib/config.js"></script>
+        <script src="lib/jquery.min.js"></script>
+        <script src="lib/facePrint.js"></script>
+        <script src="lib/testHelper.js"></script>
+        <!-- <script src="ut/lib/canteen.js"></script> -->
+        <link rel="stylesheet" href="lib/reset.css" />
+    </head>
+    <body>
+        <style>
+        </style>
+
+
+
+        <div id="refer_by_id"></div>
+        <div id="auto_axis_second_setOption"></div>
+
+
+
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option;
+
+            option = {
+                grid: [{
+                    bottom: '55%'
+                }, {
+                    id: 'g1',
+                    top: '55%'
+                }],
+                xAxis: [{
+
+                }, {
+                    id: 'x1',
+                    gridId: 'g1'
+                }],
+                yAxis: [{
+
+                }, {
+                    id: 'y1',
+                    gridId: 'g1'
+                }],
+                dataZoom: [{
+                    xAxisId: 'x1',
+                    type: 'slider'
+                }, {
+                    xAxisId: 'x1',
+                    type: 'inside'
+                }],
+                series: [{
+                    type: 'line',
+                    data: [[11, 22], [33, 44]]
+                }, {
+                    type: 'line',
+                    xAxisId: 'x1',
+                    yAxisId: 'y1',
+                    data: [[233, 432], [154, 552]]
+                }]
+            };
+
+            var chart = testHelper.create(echarts, 'refer_by_id', {
+                title: [
+                    'refer axis by id',
+                    'Two grids, dataZoom should refer to the second grid',
+                ],
+                option: option,
+                height: 300,
+            });
+        });
+        </script>
+
+
+
+
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option;
+
+            option = {
+                toolbox: {
+                    left: 'center',
+                    feature: {
+                        dataZoom: {}
+                    }
+                },
+                grid: [{
+                    bottom: '60%'
+                }, {
+                    id: 'gb',
+                    top: '60%'
+                }],
+                xAxis: [{
+                    type: 'category'
+                }, {
+                    type: 'category'
+                }, {
+                    id: 'xb0',
+                    type: 'category',
+                    gridIndex: 1
+                }, {
+                    id: 'xb1',
+                    type: 'category',
+                    gridIndex: 1
+                }],
+                yAxis: [{
+
+                }, {
+                    id: 'yb',
+                    gridIndex: 1
+                }],
+                dataZoom: [{
+                    type: 'slider'
+                }, {
+                    type: 'inside'
+                }],
+                series: [{
+                    type: 'line',
+                    data: [[11, 22], [33, 44]]
+                }, {
+                    type: 'line',
+                    xAxisIndex: 1,
+                    data: [[11111, 52], [21133, 74]]
+                }, {
+                    id: 'sb0',
+                    type: 'line',
+                    xAxisIndex: 2,
+                    yAxisIndex: 1,
+                    data: [[23, 432], [54, 552]]
+                }, {
+                    id: 'sb1',
+                    type: 'line',
+                    xAxisIndex: 3,
+                    yAxisIndex: 1,
+                    data: [[222233, 1432], [111154, 1552]]
+                }]
+            };
+
+            var chart = testHelper.create(echarts, 'auto_axis_second_setOption', {
+                title: [
+                    'two grids, each has two xAxis.',
+                    'dataZoom should auto **control all of the two xAxis of the first** grid.',
+                    'Click btn "remove the first grid".',
+                    'dataZoom should auto **control all of the two xAxis of the second** grid.',
+                    '**inside dataZoom** on the first grid area **should be removed**.',
+                    '**toolbox zoom** should only control the second grid.',
+                    'Click btn "addback the first grid".',
+                    'dataZoom should auto **control all of the two xAxis of the first** grid.',
+                    '**inside dataZoom** on the second grid area **should be removed**.',
+                    'Click btn "remove all grids".',
+                    'Should no error.',
+                    'Click btn "addback the first grid".',
+                    'Functionalities should be OK.'
+                ],
+                option: option,
+                height: 350,
+                buttons: [{
+                    text: 'remove the first grid',
+                    onclick: function () {
+                        chart.setOption({
+                            grid: [{
+                                id: 'gb',
+                            }],
+                            xAxis: [{
+                                id: 'xb0',
+                            }, {
+                                id: 'xb1',
+                            }],
+                            yAxis: [{
+                                id: 'yb'
+                            }],
+                            series: [{
+                                id: 'sb0',
+                            }, {
+                                id: 'sb1',
+                            }]
+                        }, { replaceMerge: ['grid', 'xAxis', 'yAxis', 'series'] });
+                    }
+                }, {
+                    text: 'addback the first grid',
+                    onclick: function () {
+                        chart.setOption({
+                            grid: [{
+                                bottom: '60%'
+                            }],
+                            xAxis: [{
+                            }, {
+                            }],
+                            yAxis: [{
+                            }],
+                            series: [{
+                                type: 'line',
+                                data: [[11, 22], [33, 44]]
+                            }, {
+                                type: 'line',
+                                xAxisIndex: 1,
+                                data: [[11111, 52], [21133, 74]]
+                            }]
+                        });
+                    }
+                }, {
+                    text: 'remove all grids',
+                    onclick: function () {
+                        chart.setOption({
+                            grid: [],
+                            xAxis: [],
+                            yAxis: [],
+                            series: []
+                        }, { replaceMerge: ['grid', 'xAxis', 'yAxis', 'series'] });
+                    }
+                }]
+            });
+        });
+        </script>
+
+
+
+    </body>
+</html>
+


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


[incubator-echarts] 14/16: fix: fix feature reproduce and complete test case.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 7606cb72b155a994584c7d0ff704541f27ff244c
Author: 100pah <su...@gmail.com>
AuthorDate: Fri Jul 17 02:06:47 2020 +0800

    fix: fix feature reproduce and complete test case.
---
 src/model/Global.ts                |   7 +-
 src/model/OptionManager.ts         |  12 +-
 src/preprocessor/backwardCompat.ts |   2 +-
 src/util/model.ts                  |  90 ++++--
 test/dataZoom-feature.html         | 564 ++++++++++++++++++++++++++++---------
 test/option-replaceMerge.html      | 101 ++-----
 6 files changed, 539 insertions(+), 237 deletions(-)

diff --git a/src/model/Global.ts b/src/model/Global.ts
index 93b674d..6328032 100644
--- a/src/model/Global.ts
+++ b/src/model/Global.ts
@@ -272,8 +272,11 @@ class GlobalModel extends Model<ECUnitOption> {
             );
 
             const oldCmptList = componentsMap.get(mainType);
-            const mergeMode = (replaceMergeMainTypeMap && replaceMergeMainTypeMap.get(mainType))
-                ? 'replaceMerge' : 'normalMerge';
+            const mergeMode =
+                // `!oldCmptList` means init. See the comment in `mappingToExists`
+                  !oldCmptList ? 'replaceAll'
+                : (replaceMergeMainTypeMap && replaceMergeMainTypeMap.get(mainType)) ? 'replaceMerge'
+                : 'normalMerge';
             const mappingResult = modelUtil.mappingToExists(oldCmptList, newCmptOptionList, mergeMode);
 
             // Set mainType and complete subType.
diff --git a/src/model/OptionManager.ts b/src/model/OptionManager.ts
index 17d3767..33f0742 100644
--- a/src/model/OptionManager.ts
+++ b/src/model/OptionManager.ts
@@ -22,17 +22,19 @@
  */
 
 
-import ComponentModel, { ComponentModelConstructor } from './Component';
+// import ComponentModel, { ComponentModelConstructor } from './Component';
 import ExtensionAPI from '../ExtensionAPI';
 import {
-    OptionPreprocessor, MediaQuery, ECUnitOption, MediaUnit, ECOption, SeriesOption, ComponentOption
+    OptionPreprocessor, MediaQuery, ECUnitOption, MediaUnit, ECOption, SeriesOption
 } from '../util/types';
 import GlobalModel, { InnerSetOptionOpts } from './Global';
 import {
-    MappingExistingItem, normalizeToArray, setComponentTypeToKeyInfo, mappingToExists
+    normalizeToArray
+    // , MappingExistingItem, setComponentTypeToKeyInfo, mappingToExists
 } from '../util/model';
 import {
-    each, clone, map, merge, isTypedArray, setAsPrimitive, HashMap, createHashMap, extend
+    each, clone, map, isTypedArray, setAsPrimitive
+    // , HashMap , createHashMap, extend, merge,
 } from 'zrender/src/core/util';
 
 const QUERY_REG = /^(min|max)?(.+)$/;
@@ -45,7 +47,7 @@ interface ParsedRawOption {
 }
 
 // Key: mainType
-type FakeComponentsMap = HashMap<(MappingExistingItem & { subType: string })[]>;
+// type FakeComponentsMap = HashMap<(MappingExistingItem & { subType: string })[]>;
 
 /**
  * TERM EXPLANATIONS:
diff --git a/src/preprocessor/backwardCompat.ts b/src/preprocessor/backwardCompat.ts
index f710dcc..5e112f6 100644
--- a/src/preprocessor/backwardCompat.ts
+++ b/src/preprocessor/backwardCompat.ts
@@ -58,7 +58,7 @@ function set(opt: Dictionary<any>, path: string, val: any, overwrite?: boolean)
 }
 
 function compatLayoutProperties(option: Dictionary<any>) {
-    each(LAYOUT_PROPERTIES, function (prop) {
+    option && each(LAYOUT_PROPERTIES, function (prop) {
         if (prop[0] in option && !(prop[1] in option)) {
             option[prop[1]] = option[prop[0]];
         }
diff --git a/src/util/model.ts b/src/util/model.ts
index 6225339..5cf3568 100644
--- a/src/util/model.ts
+++ b/src/util/model.ts
@@ -162,17 +162,15 @@ export interface MappingExistingItem {
 type MappingResult<T> = MappingResultItem<T>[];
 interface MappingResultItem<T extends MappingExistingItem = MappingExistingItem> {
     // Existing component instance.
-    existing?: T;
+    existing: T;
     // The mapped new component option.
-    newOption?: ComponentOption;
+    newOption: ComponentOption;
     // Mark that the new component has nothing to do with any of the old components.
     // So they won't share view. Also see `__requireNewView`.
-    brandNew?: boolean;
-    // id?: string;
-    // name?: string;
+    brandNew: boolean;
     // keyInfo for new component.
     // All of them will be assigned to a created component instance.
-    keyInfo?: {
+    keyInfo: {
         name: string,
         id: string,
         mainType: ComponentMainType,
@@ -180,6 +178,8 @@ interface MappingResultItem<T extends MappingExistingItem = MappingExistingItem>
     };
 }
 
+type MappingToExistsMode = 'normalMerge' | 'replaceMerge' | 'replaceAll';
+
 /**
  * Mapping to existings for merge.
  *
@@ -198,22 +198,31 @@ interface MappingResultItem<T extends MappingExistingItem = MappingExistingItem>
  *     That means their might be "hole" after the removal.
  *     The new components are created first at those available index.
  *
+ * Mode "replaceAll":
+ *     This mode try to support that reproduce an echarts instance from another
+ *     echarts instance (via `getOption`) in some simple cases.
+ *     In this senario, the `result` index are exactly the consistent with the `newCmptOptions`,
+ *     which ensures the compoennt index referring (like `xAxisIndex: ?`) corrent. That is,
+ *     the "hole" in `newCmptOptions` will also be kept.
+ *     On the contrary, other modes try best to eliminate holes.
+ *     PENDING: This is an experimental mode yet.
+ *
  * @return See the comment of <MappingResult>.
  */
 export function mappingToExists<T extends MappingExistingItem>(
     existings: T[],
     newCmptOptions: ComponentOption[],
-    mode: 'normalMerge' | 'replaceMerge'
+    mode: MappingToExistsMode
 ): MappingResult<T> {
 
-    const result: MappingResultItem<T>[] = [];
+    const isNormalMergeMode = mode === 'normalMerge';
     const isReplaceMergeMode = mode === 'replaceMerge';
+    const isReplaceAllMode = mode === 'replaceAll';
     existings = existings || [];
     newCmptOptions = (newCmptOptions || []).slice();
     const existingIdIdxMap = createHashMap<number>();
 
-    prepareResult(result, existings, existingIdIdxMap, isReplaceMergeMode);
-
+    // Validate id and name on user input option.
     each(newCmptOptions, function (cmptOption, index) {
         if (!isObject<ComponentOption>(cmptOption)) {
             newCmptOptions[index] = null;
@@ -223,13 +232,22 @@ export function mappingToExists<T extends MappingExistingItem>(
         cmptOption.name == null || validateIdOrName(cmptOption.name);
     });
 
-    mappingById(result, existings, existingIdIdxMap, newCmptOptions);
+    const result = prepareResult(existings, existingIdIdxMap, mode);
+
+    if (isNormalMergeMode || isReplaceMergeMode) {
+        mappingById(result, existings, existingIdIdxMap, newCmptOptions);
+    }
 
-    if (!isReplaceMergeMode) {
+    if (isNormalMergeMode) {
         mappingByName(result, newCmptOptions);
     }
 
-    mappingByIndex(result, newCmptOptions, isReplaceMergeMode);
+    if (isNormalMergeMode || isReplaceMergeMode) {
+        mappingByIndex(result, newCmptOptions, isReplaceMergeMode);
+    }
+    else if (isReplaceAllMode) {
+        mappingInReplaceAllMode(result, newCmptOptions);
+    }
 
     makeIdAndName(result);
 
@@ -239,11 +257,16 @@ export function mappingToExists<T extends MappingExistingItem>(
 }
 
 function prepareResult<T extends MappingExistingItem>(
-    result: MappingResult<T>,
     existings: T[],
     existingIdIdxMap: HashMap<number>,
-    isReplaceMergeMode: boolean
-) {
+    mode: MappingToExistsMode
+): MappingResultItem<T>[] {
+    const result: MappingResultItem<T>[] = [];
+
+    if (mode === 'replaceAll') {
+        return result;
+    }
+
     // Do not use native `map` to in case that the array `existings`
     // contains elided items, which will be ommited.
     for (let index = 0; index < existings.length; index++) {
@@ -258,11 +281,15 @@ function prepareResult<T extends MappingExistingItem>(
         // For internal-components:
         //     go with "replaceMerge" approach in both mode.
         result.push({
-            existing: (isReplaceMergeMode || isComponentIdInternal(existing))
+            existing: (mode === 'replaceMerge' || isComponentIdInternal(existing))
                 ? null
-                : existing
+                : existing,
+            newOption: null,
+            keyInfo: null,
+            brandNew: null
         });
     }
+    return result;
 }
 
 function mappingById<T extends MappingExistingItem>(
@@ -323,7 +350,7 @@ function mappingByName<T extends MappingExistingItem>(
 function mappingByIndex<T extends MappingExistingItem>(
     result: MappingResult<T>,
     newCmptOptions: ComponentOption[],
-    isReplaceMergeMode: boolean
+    brandNew: boolean
 ): void {
     let nextIdx = 0;
     each(newCmptOptions, function (cmptOption) {
@@ -358,15 +385,36 @@ function mappingByIndex<T extends MappingExistingItem>(
 
         if (resultItem) {
             resultItem.newOption = cmptOption;
-            resultItem.brandNew = isReplaceMergeMode;
+            resultItem.brandNew = brandNew;
         }
         else {
-            result.push({ newOption: cmptOption, brandNew: isReplaceMergeMode });
+            result.push({
+                newOption: cmptOption,
+                brandNew: brandNew,
+                existing: null,
+                keyInfo: null
+            });
         }
         nextIdx++;
     });
 }
 
+function mappingInReplaceAllMode<T extends MappingExistingItem>(
+    result: MappingResult<T>,
+    newCmptOptions: ComponentOption[]
+): void {
+    each(newCmptOptions, function (cmptOption) {
+        // The feature "reproduce" requires "hole" will also reproduced
+        // in case that compoennt index referring are broken.
+        result.push({
+            newOption: cmptOption,
+            brandNew: true,
+            existing: null,
+            keyInfo: null
+        });
+    });
+}
+
 /**
  * Make id and name for mapping result (result of mappingToExists)
  * into `keyInfo` field.
diff --git a/test/dataZoom-feature.html b/test/dataZoom-feature.html
index 9d42ccc..fd9e788 100644
--- a/test/dataZoom-feature.html
+++ b/test/dataZoom-feature.html
@@ -38,8 +38,10 @@ under the License.
 
 
         <div id="refer_by_id"></div>
-        <div id="auto_axis_second_setOption_normal_dz"></div>
         <div id="auto_axis_second_setOption_only_toolbox_dz"></div>
+        <div id="auto_axis_second_setOption_normal_dz"></div>
+        <div id="specified_axis_second_setOption_normal_dz"></div>
+        <div id="remove_dz"></div>
 
 
 
@@ -93,7 +95,7 @@ under the License.
             var chart = testHelper.create(echarts, 'refer_by_id', {
                 title: [
                     'refer axis by id',
-                    'Two grids, dataZoom should **only refer to the bottom grid**',
+                    'Two grids, dataZoom should **only refer to bottom grid**',
                 ],
                 option: option,
                 height: 300,
@@ -106,32 +108,181 @@ under the License.
 
 
 
+        <script>
+        require(['echarts'], function (echarts) {
+            function makeFirstGridOption() {
+                return {
+                    grid: [{
+                        bottom: '60%'
+                    }],
+                    xAxis: [{
+                    }, {
+                    }],
+                    yAxis: [{
+                    }],
+                    series: [{
+                        type: 'line',
+                        data: [[333, 22], [666, 44]]
+                    }, {
+                        type: 'line',
+                        xAxisIndex: 1,
+                        data: [[88888, 52], [99999, 74]]
+                    }]
+                };
+            }
+
+            var option = makeFirstGridOption();
+
+            option.toolbox = {
+                left: 'center',
+                feature: {
+                    dataZoom: {}
+                }
+            };
+            option.grid.push(
+                {
+                    id: 'gb',
+                    top: '60%'
+                }
+            );
+            option.xAxis.push(
+                {
+                    id: 'xb0',
+                    type: 'category',
+                    gridIndex: 1
+                }, {
+                    id: 'xb1',
+                    type: 'category',
+                    gridIndex: 1
+                }
+            );
+            option.yAxis.push(
+                {
+                    id: 'yb',
+                    gridIndex: 1
+                }
+            );
+            option.series.push(
+                {
+                    id: 'sb0',
+                    type: 'line',
+                    xAxisIndex: 2,
+                    yAxisIndex: 1,
+                    data: [[63, 432], [98, 552]]
+                }, {
+                    id: 'sb1',
+                    type: 'line',
+                    xAxisIndex: 3,
+                    yAxisIndex: 1,
+                    data: [[87654, 1432], [56789, 1552]]
+                }
+            );
+
+            var chart = testHelper.create(echarts, 'auto_axis_second_setOption_only_toolbox_dz', {
+                title: [
+                    '[Only toolbox dataZoom] two grids, each has two xAxis.',
+                    'toolbox zoom should work on **all grids**.',
+                    'Click btn "remove top grid".',
+                    'toolbox zoom should work only on **bottom grids**.',
+                    'Click btn "addback top grid".',
+                    'toolbox zoom should work on **all grids**.',
+                    'Click btn "remove all grids".',
+                    'Should **no error**.',
+                    'Check toolbox zoom should **not work on the original area**.',
+                    'Click btn "addback top grid".',
+                    'toolbox zoom should work only on **top grids**.',
+                ],
+                option: option,
+                height: 350,
+                buttons: [{
+                    text: 'remove top grid',
+                    onclick: function () {
+                        chart.setOption({
+                            grid: [{
+                                id: 'gb',
+                            }],
+                            xAxis: [{
+                                id: 'xb0',
+                            }, {
+                                id: 'xb1',
+                            }],
+                            yAxis: [{
+                                id: 'yb'
+                            }],
+                            series: [{
+                                id: 'sb0',
+                            }, {
+                                id: 'sb1',
+                            }]
+                        }, { replaceMerge: ['grid', 'xAxis', 'yAxis', 'series'] });
+                    }
+                }, {
+                    text: 'addback top grid',
+                    onclick: function () {
+                        chart.setOption(makeFirstGridOption());
+                    }
+                }, {
+                    text: 'remove all grids',
+                    onclick: function () {
+                        chart.setOption({
+                            grid: [],
+                            xAxis: [],
+                            yAxis: [],
+                            series: []
+                        }, { replaceMerge: ['grid', 'xAxis', 'yAxis', 'series'] });
+                    }
+                }]
+            });
+        });
+        </script>
+
+
 
 
 
 
         <script>
         require(['echarts'], function (echarts) {
-            var option;
 
-            option = {
-                toolbox: {
-                    left: 'center',
-                    feature: {
-                        dataZoom: {}
-                    }
-                },
-                grid: [{
-                    bottom: '60%'
-                }, {
+            function makeFirstGridOption() {
+                return {
+                    grid: [{
+                        bottom: '60%'
+                    }],
+                    xAxis: [{
+                    }, {
+                    }],
+                    yAxis: [{
+                    }],
+                    series: [{
+                        type: 'line',
+                        symbolSize: 20,
+                        data: [[11, 22], [33, 44]]
+                    }, {
+                        type: 'line',
+                        xAxisIndex: 1,
+                        symbolSize: 20,
+                        data: [[11111, 52], [21133, 74]]
+                    }]
+                };
+            }
+
+            var option = makeFirstGridOption();
+
+            option.toolbox = {
+                left: 'center',
+                feature: {
+                    dataZoom: {}
+                }
+            };
+            option.grid.push(
+                {
                     id: 'gb',
                     top: '60%'
-                }],
-                xAxis: [{
-                    type: 'category'
-                }, {
-                    type: 'category'
-                }, {
+                }
+            );
+            option.xAxis.push(
+                {
                     id: 'xb0',
                     type: 'category',
                     gridIndex: 1
@@ -139,60 +290,60 @@ under the License.
                     id: 'xb1',
                     type: 'category',
                     gridIndex: 1
-                }],
-                yAxis: [{
-
-                }, {
+                }
+            );
+            option.yAxis.push(
+                {
                     id: 'yb',
                     gridIndex: 1
-                }],
-                dataZoom: [{
+                }
+            );
+            option.dataZoom = [
+                {
                     type: 'slider'
                 }, {
                     type: 'inside'
-                }],
-                series: [{
-                    type: 'line',
-                    data: [[11, 22], [33, 44]]
-                }, {
-                    type: 'line',
-                    xAxisIndex: 1,
-                    data: [[11111, 52], [21133, 74]]
-                }, {
+                }
+            ];
+            option.series.push(
+                {
                     id: 'sb0',
                     type: 'line',
+                    symbolSize: 20,
                     xAxisIndex: 2,
                     yAxisIndex: 1,
                     data: [[23, 432], [54, 552]]
                 }, {
                     id: 'sb1',
                     type: 'line',
+                    symbolSize: 20,
                     xAxisIndex: 3,
                     yAxisIndex: 1,
                     data: [[222233, 1432], [111154, 1552]]
-                }]
-            };
+                }
+            );
 
             var chart = testHelper.create(echarts, 'auto_axis_second_setOption_normal_dz', {
                 title: [
+                    'Test: **Auto axis** remove coord sys (dataZoom not remove)',
                     'two grids, each has two xAxis.',
-                    'dataZoom should auto **control all of the two xAxis of the first** grid.',
-                    'Click btn "remove the first grid".',
-                    'dataZoom should auto **control all of the two xAxis of the second** grid.',
-                    '**inside dataZoom** on the first grid area **should be removed**.',
-                    '**toolbox zoom** should only control the second grid.',
-                    'Click btn "addback the first grid".',
-                    'dataZoom should auto **control all of the two xAxis of the first** grid.',
-                    '**inside dataZoom** on the second grid area **should be removed**.',
+                    'dataZoom should **only control all 2 xAxis of top** grid.',
+                    'Click btn "remove top grid".',
+                    'dataZoom should **only control all 2 xAxis of bottom** grid.',
+                    '**inside dataZoom** on top grid area **should be removed**.',
+                    '**toolbox zoom** should only control bottom grid.',
+                    'Click btn "addback top grid".',
+                    'dataZoom should **only control all 2 xAxis of top** grid.',
+                    '**inside dataZoom** on bottom grid area **should be removed**.',
                     'Click btn "remove all grids".',
                     'Should no error.',
-                    'Click btn "addback the first grid".',
+                    'Click btn "addback top grid".',
                     'Functionalities should be OK.'
                 ],
                 option: option,
                 height: 350,
                 buttons: [{
-                    text: 'remove the first grid',
+                    text: 'remove top grid',
                     onclick: function () {
                         chart.setOption({
                             grid: [{
@@ -214,26 +365,163 @@ under the License.
                         }, { replaceMerge: ['grid', 'xAxis', 'yAxis', 'series'] });
                     }
                 }, {
-                    text: 'addback the first grid',
+                    text: 'addback top grid',
+                    onclick: function () {
+                        chart.setOption(makeFirstGridOption());
+                    }
+                }, {
+                    text: 'remove all grids',
+                    onclick: function () {
+                        chart.setOption({
+                            grid: [],
+                            xAxis: [],
+                            yAxis: [],
+                            series: []
+                        }, { replaceMerge: ['grid', 'xAxis', 'yAxis', 'series'] });
+                    }
+                }]
+            });
+        });
+        </script>
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+
+            function makeFirstGridOption() {
+                return {
+                    grid: [{
+                        bottom: '60%'
+                    }],
+                    xAxis: [{
+                    }, {
+                    }],
+                    yAxis: [{
+                    }],
+                    series: [{
+                        type: 'line',
+                        symbol: 'emptyTriangle',
+                        symbolSize: 20,
+                        data: [[11, 22], [33, 44]]
+                    }, {
+                        type: 'line',
+                        symbol: 'emptyTriangle',
+                        symbolSize: 20,
+                        xAxisIndex: 1,
+                        data: [[11111, 52], [21133, 74]]
+                    }]
+                };
+            }
+
+            var option = makeFirstGridOption();
+
+            option.toolbox = {
+                left: 'center',
+                feature: {
+                    dataZoom: {}
+                }
+            };
+            option.grid.push(
+                {
+                    id: 'gb',
+                    top: '60%'
+                }
+            );
+            option.xAxis.push(
+                {
+                    id: 'xb0',
+                    type: 'category',
+                    gridIndex: 1
+                }, {
+                    id: 'xb1',
+                    type: 'category',
+                    gridIndex: 1
+                }
+            );
+            option.yAxis.push(
+                {
+                    id: 'yb',
+                    gridIndex: 1
+                }
+            );
+            option.dataZoom = [
+                {
+                    type: 'slider',
+                    xAxisIndex: 'all'
+                }, {
+                    type: 'inside',
+                    xAxisIndex: 'all'
+                }
+            ];
+            option.series.push(
+                {
+                    id: 'sb0',
+                    type: 'line',
+                    symbol: 'emptyTriangle',
+                    symbolSize: 20,
+                    xAxisIndex: 2,
+                    yAxisIndex: 1,
+                    data: [[23, 432], [54, 552], [124, 324], [341, 134], [888, 213]]
+                }, {
+                    id: 'sb1',
+                    type: 'line',
+                    symbol: 'emptyTriangle',
+                    symbolSize: 20,
+                    xAxisIndex: 3,
+                    yAxisIndex: 1,
+                    data: [[222233, 1432], [111154, 1552], [222223, 1231], [131313, 3322], [717171, 5512]]
+                }
+            );
+
+            var chart = testHelper.create(echarts, 'specified_axis_second_setOption_normal_dz', {
+                title: [
+                    'Test: **xAxisIndex: "all"** remove coord sys (dataZoom not remove)',
+                    'two grids, each has two xAxis.',
+                    'dataZoom should **only control all 4 xAxis**.',
+                    'Click btn "remove top grid".',
+                    'dataZoom should **only control all 2 xAxis of bottom** grid.',
+                    '**inside dataZoom** on top grid area **should be removed**.',
+                    '**toolbox zoom** should only control bottom grid.',
+                    'Click btn "addback top grid".',
+                    'dataZoom should **only control all 4 xAxis**.',
+                    '**inside dataZoom** on bottom grid area **should be removed**.',
+                    'Click btn "remove all grids".',
+                    'Should no error.',
+                    'Click btn "addback top grid".',
+                    'Functionalities should be OK.'
+                ],
+                option: option,
+                height: 350,
+                buttons: [{
+                    text: 'remove top grid',
                     onclick: function () {
                         chart.setOption({
                             grid: [{
-                                bottom: '60%'
+                                id: 'gb',
                             }],
                             xAxis: [{
+                                id: 'xb0',
                             }, {
+                                id: 'xb1',
                             }],
                             yAxis: [{
+                                id: 'yb'
                             }],
                             series: [{
-                                type: 'line',
-                                data: [[11, 22], [33, 44]]
+                                id: 'sb0',
                             }, {
-                                type: 'line',
-                                xAxisIndex: 1,
-                                data: [[11111, 52], [21133, 74]]
+                                id: 'sb1',
                             }]
-                        });
+                        }, { replaceMerge: ['grid', 'xAxis', 'yAxis', 'series'] });
+                    }
+                }, {
+                    text: 'addback top grid',
+                    onclick: function () {
+                        chart.setOption(makeFirstGridOption());
                     }
                 }, {
                     text: 'remove all grids',
@@ -255,11 +543,40 @@ under the License.
 
 
 
+
+
+
         <script>
         require(['echarts'], function (echarts) {
-            var option;
 
-            option = {
+            function makeFirstGridOption() {
+                return {
+                    grid: [{
+                        bottom: '60%'
+                    }],
+                    xAxis: [{
+                    }, {
+                    }],
+                    yAxis: [{
+                    }],
+                    series: [{
+                        type: 'line',
+                        symbol: 'emptyTriangle',
+                        symbolSize: 20,
+                        data: [[11, 22], [33, 44]]
+                    }, {
+                        type: 'line',
+                        symbol: 'emptyTriangle',
+                        symbolSize: 20,
+                        xAxisIndex: 1,
+                        data: [[11111, 52], [21133, 74]]
+                    }]
+                };
+            }
+
+            var option = makeFirstGridOption();
+
+            var option = {
                 toolbox: {
                     left: 'center',
                     feature: {
@@ -267,121 +584,116 @@ under the License.
                     }
                 },
                 grid: [{
-                    bottom: '60%'
+                    right: '60%'
                 }, {
                     id: 'gb',
-                    top: '60%'
+                    left: '60%'
                 }],
                 xAxis: [{
-                    type: 'category'
                 }, {
-                    type: 'category'
+                    id: 'xb',
+                    gridIndex: 1
+                }],
+                yAxis: [{
                 }, {
-                    id: 'xb0',
+                }, {
+                    id: 'yb0',
                     type: 'category',
                     gridIndex: 1
                 }, {
-                    id: 'xb1',
+                    id: 'yb1',
                     type: 'category',
                     gridIndex: 1
                 }],
-                yAxis: [{
-
+                dataZoom: [{
+                    type: 'slider',
+                    left: 10,
+                    yAxisIndex: [0, 1]
                 }, {
-                    id: 'yb',
-                    gridIndex: 1
+                    type: 'inside',
+                    yAxisIndex: [0, 1]
                 }],
                 series: [{
                     type: 'line',
-                    data: [[333, 22], [666, 44]]
+                    symbol: 'emptyTriangle',
+                    symbolSize: 20,
+                    data: [[22, 11], [44, 33]]
                 }, {
                     type: 'line',
-                    xAxisIndex: 1,
-                    data: [[88888, 52], [99999, 74]]
+                    symbol: 'emptyTriangle',
+                    symbolSize: 20,
+                    yAxisIndex: 1,
+                    data: [[52, 11111], [74, 21133]]
                 }, {
                     id: 'sb0',
                     type: 'line',
-                    xAxisIndex: 2,
-                    yAxisIndex: 1,
-                    data: [[63, 432], [98, 552]]
+                    symbol: 'emptyTriangle',
+                    symbolSize: 20,
+                    xAxisIndex: 1,
+                    yAxisIndex: 2,
+                    data: [[432, 23], [552, 54], [324, 124], [134, 341], [213, 888]]
                 }, {
                     id: 'sb1',
                     type: 'line',
-                    xAxisIndex: 3,
-                    yAxisIndex: 1,
-                    data: [[87654, 1432], [56789, 1552]]
+                    symbol: 'emptyTriangle',
+                    symbolSize: 20,
+                    xAxisIndex: 1,
+                    yAxisIndex: 3,
+                    data: [[1432, 222233], [1552, 111154], [1231, 222223], [3322, 131313], [5512, 717171]]
                 }]
             };
 
-            var chart = testHelper.create(echarts, 'auto_axis_second_setOption_only_toolbox_dz', {
+            var chart = testHelper.create(echarts, 'remove_dz', {
                 title: [
-                    '[Only toolbox dataZoom] two grids, each has two xAxis.',
-                    'toolbox zoom should work on **all grids**.',
-                    'Click btn "remove the first grid".',
-                    'toolbox zoom should work only on **the second grids**.',
-                    'Click btn "addback the first grid".',
-                    'toolbox zoom should work on **all grids**.',
-                    'Click btn "remove all grids".',
-                    'Should **no error**.',
-                    'Check toolbox zoom should **not work on the original area**.',
-                    'Click btn "addback the first grid".',
-                    'toolbox zoom should work only on the **the first grids**.',
+                    'Test: remove dataZoom',
+                    'two grids, each has two yAxis.',
+                    'dataZoom should **only control all 2 yAxis of left** grid.',
+                    'Click btn "remove left dataZoom".',
+                    'dz disappear and insideZoom should **no longer work**.',
+                    '**toolbox zoom** should still work.',
+                    'Click btn "add right dataZoom".',
+                    'dataZoom should **only control all 2 yAxis of right** grid.',
+                    'Click btn "dataZoom control x".',
+                    'dataZoom should lay full of bottom and **only control all xAxis**.',
                 ],
                 option: option,
                 height: 350,
                 buttons: [{
-                    text: 'remove the first grid',
+                    text: 'remove left dataZoom',
                     onclick: function () {
                         chart.setOption({
-                            grid: [{
-                                id: 'gb',
-                            }],
-                            xAxis: [{
-                                id: 'xb0',
-                            }, {
-                                id: 'xb1',
-                            }],
-                            yAxis: [{
-                                id: 'yb'
-                            }],
-                            series: [{
-                                id: 'sb0',
-                            }, {
-                                id: 'sb1',
-                            }]
-                        }, { replaceMerge: ['grid', 'xAxis', 'yAxis', 'series'] });
+                        }, { replaceMerge: ['dataZoom'] });
                     }
                 }, {
-                    text: 'addback the first grid',
+                    text: 'add right dataZoom',
                     onclick: function () {
                         chart.setOption({
-                            grid: [{
-                                bottom: '60%'
-                            }],
-                            xAxis: [{
-                            }, {
-                            }],
-                            yAxis: [{
-                            }],
-                            series: [{
-                                type: 'line',
-                                data: [[333, 22], [666, 44]]
+                            dataZoom: [{
+                                type: 'slider',
+                                right: 10,
+                                yAxisIndex: [2, 3]
                             }, {
-                                type: 'line',
-                                xAxisIndex: 1,
-                                data: [[88888, 52], [99999, 74]]
+                                type: 'inside',
+                                yAxisIndex: [2, 3]
                             }]
                         });
                     }
                 }, {
-                    text: 'remove all grids',
+                    text: 'dataZoom control x',
                     onclick: function () {
                         chart.setOption({
-                            grid: [],
-                            xAxis: [],
-                            yAxis: [],
-                            series: []
-                        }, { replaceMerge: ['grid', 'xAxis', 'yAxis', 'series'] });
+                            dataZoom: [{
+                                bottom: 10,
+                                left: 20,
+                                right: 20,
+                                width: null,
+                                xAxisIndex: 'all',
+                                yAxisIndex: 'none'
+                            }, {
+                                xAxisIndex: 'all',
+                                yAxisIndex: 'none'
+                            }]
+                        });
                     }
                 }]
             });
diff --git a/test/option-replaceMerge.html b/test/option-replaceMerge.html
index 0ed8388..a56d375 100644
--- a/test/option-replaceMerge.html
+++ b/test/option-replaceMerge.html
@@ -717,11 +717,11 @@ under the License.
                 var option = {
                     grid: [{
                         right: '55%',
-                        bottom: 30
+                        bottom: 70
                     }, {
                         id: 'grid-r',
                         left: '55%',
-                        bottom: 30
+                        bottom: 70
                     }],
                     xAxis: [{
                         type: 'category',
@@ -739,11 +739,11 @@ under the License.
                     }],
                     legend: {},
                     tooltip: {},
-                    // dataZoom: [{
-                    //     type: 'slider'
-                    // }, {
-                    //     type: 'inside'
-                    // }],
+                    dataZoom: [{
+                        type: 'slider'
+                    }, {
+                        type: 'inside'
+                    }],
                     toolbox: {
                         left: 'center',
                         top: 25,
@@ -786,8 +786,11 @@ under the License.
 
             var chartSrc = testHelper.create(echarts, 'main_replaceMerge_reproduce_by_getOption_src', {
                 title: [
-                    '**replaceMerge**: reproduce via getOption',
-                    'click the buttons one by one from left to right',
+                    '**replaceMerge**: test reproduce via **getOption**',
+                    'Click "remove left grid"',
+                    'left grid removed and dataZoom go to right grid',
+                    'use insideZoom',
+                    'Click "reproduce1"',
                     'should show **TWO checked: Pass**',
                     'The chart reproduced below should be **the same**'
                 ],
@@ -815,84 +818,18 @@ under the License.
                     onclick: function () {
                         testHelper.printAssert(chartSrc, function (assert) {
                             var seriesModels = chartSrc.getModel().getSeries();
-                            assert(seriesModels.length === 3);
-                            assert(seriesModels[0].componentIndex === 0);
-                            assert(seriesModels[1].componentIndex === 2);
-                            assert(seriesModels[2].componentIndex === 3);
-                            assert(seriesModels[0].id === 'a');
-                            assert(seriesModels[1].id === 'c');
-                            assert(seriesModels[2].id === 'd');
-
-                            assert(chartSrc.getModel().getSeriesCount() === 3);
-
-                            var optionGotten = chartSrc.getOption();
-                            assert(optionGotten.series.length === 4);
-                            assert(optionGotten.series[0].name === 'aa');
-                            assert(optionGotten.series[1] == null);
-                            assert(optionGotten.series[2].name === 'cc');
-                            assert(optionGotten.series[3].name === 'dd');
-
-                            assert(chartSrc.getModel().getSeriesByIndex(1) == null);
-                            assert(chartSrc.getModel().getComponent('series', 1) == null);
-                        });
-                    }
-                }, {
-                    text: 'replaceMerge',
-                    onclick: function () {
-                        chartSrc.setOption({
-                            series: [{
-                                id: 'm',
-                                type: 'bar',
-                                data: [['a11', 22], ['a33', 44]]
-                            }, {
-                                id: 'n',
-                                type: 'bar',
-                                data: [['a11', 32], ['a33', 54]]
-                            }, {
-                                id: 'a'
-                            }, {
-                                id: 'c'
-                            }, {
-                                id: 'd'
-                            }]
-                        }, {replaceMerge: 'series'});
-                    }
-                }, {
-                    text: 'then check',
-                    onclick: function () {
-                        testHelper.printAssert(chartSrc, function (assert) {
-                            var seriesModels = chartSrc.getModel().getSeries();
-                            assert(seriesModels.length === 5);
-                            assert(seriesModels[0].componentIndex === 0);
-                            assert(seriesModels[1].componentIndex === 1);
-                            assert(seriesModels[2].componentIndex === 2);
-                            assert(seriesModels[3].componentIndex === 3);
-                            assert(seriesModels[4].componentIndex === 4);
-                            assert(seriesModels[0].id === 'a');
-                            assert(seriesModels[1].id === 'm');
-                            assert(seriesModels[2].id === 'c');
-                            assert(seriesModels[3].id === 'd');
-                            assert(seriesModels[4].id === 'n');
-
-                            assert(chartSrc.getModel().getSeriesCount() === 5);
+                            assert(seriesModels.length === 1);
+                            assert(seriesModels[0].componentIndex === 1);
 
                             var optionGotten = chartSrc.getOption();
-                            assert(optionGotten.series.length === 5);
-                            assert(optionGotten.series[0].id === 'a');
-                            assert(optionGotten.series[1].id === 'm');
-                            assert(optionGotten.series[2].id === 'c');
-                            assert(optionGotten.series[3].id === 'd');
-                            assert(optionGotten.series[4].id === 'n');
+                            assert(optionGotten.series.length === 2);
+                            assert(optionGotten.series[0] === null);
+                            assert(optionGotten.series[1].id === 'b');
 
-                            assert(chartSrc.getModel().getSeriesByIndex(1).id == 'm');
-                            assert(chartSrc.getModel().getComponent('series', 1).id == 'm');
+                            console.log(optionGotten);
+                            chartTar.setOption(optionGotten);
                         });
                     }
-                }, {
-                    text: 'reset all',
-                    onclick: function () {
-                        chartSrc.setOption(makeInitOption(), true);
-                    }
                 }],
                 height: 200
             });


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


[incubator-echarts] 03/16: Merge branch 'next' into remove-component

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 39891c0cb2f1f4e810ee97f14ca31e25223229c0
Merge: 065cf80 4c3b1b2
Author: 100pah <su...@gmail.com>
AuthorDate: Tue Jul 7 23:03:11 2020 +0800

    Merge branch 'next' into remove-component

 src/ExtensionAPI.ts                       |  45 +-
 src/chart/bar/BarView.ts                  |  69 ++-
 src/chart/bar/PictorialBarView.ts         |   5 +-
 src/chart/candlestick/CandlestickView.ts  |   1 -
 src/chart/custom.ts                       | 280 +++++++-----
 src/chart/funnel/FunnelSeries.ts          |  13 +-
 src/chart/funnel/FunnelView.ts            |  57 ++-
 src/chart/gauge/GaugeView.ts              |   7 +-
 src/chart/graph/GraphView.ts              | 141 +++---
 src/chart/helper/Line.ts                  |   6 +-
 src/chart/helper/LineDraw.ts              |   1 -
 src/chart/helper/Symbol.ts                |  95 ++--
 src/chart/helper/SymbolDraw.ts            |   2 -
 src/chart/line/LineSeries.ts              |   7 +-
 src/chart/line/LineView.ts                |   1 -
 src/chart/map/MapView.ts                  |   2 +-
 src/chart/parallel/ParallelView.ts        |   1 -
 src/chart/pie/PieSeries.ts                |  44 +-
 src/chart/pie/PieView.ts                  | 363 +++++----------
 src/chart/pie/labelLayout.ts              | 396 ++++++++--------
 src/chart/pie/pieLayout.ts                |  11 +-
 src/chart/radar/RadarView.ts              |   4 +-
 src/chart/sankey/SankeyView.ts            | 126 ++----
 src/chart/sunburst.ts                     |   2 +
 src/chart/sunburst/SunburstPiece.ts       | 285 ++++++------
 src/chart/sunburst/SunburstSeries.ts      |   8 +-
 src/chart/sunburst/SunburstView.ts        |  12 +-
 src/chart/sunburst/sunburstAction.ts      |   2 +-
 src/chart/sunburst/sunburstVisual.ts      |   4 +-
 src/chart/tree/TreeView.ts                |  13 +-
 src/chart/treemap/TreemapSeries.ts        |   1 +
 src/chart/treemap/TreemapView.ts          |  24 +-
 src/component/dataZoom/SliderZoomView.ts  |   1 +
 src/component/geo.ts                      |   4 +-
 src/component/geo/GeoView.ts              |   2 +-
 src/component/helper/MapDraw.ts           |  28 +-
 src/component/helper/roamHelper.ts        |  35 +-
 src/component/legend/LegendView.ts        |   1 +
 src/component/marker/MarkAreaView.ts      |   3 +-
 src/component/parallel.ts                 |   4 +-
 src/component/title.ts                    |   1 +
 src/component/toolbox/ToolboxView.ts      |  62 +--
 src/component/visualMap/VisualMapModel.ts |   5 +-
 src/coord/geo/GeoModel.ts                 |   2 +
 src/echarts.ts                            | 184 ++++++--
 src/export.ts                             |   1 +
 src/label/LabelManager.ts                 | 554 +++++++++++++++++++++++
 src/label/labelGuideHelper.ts             | 649 ++++++++++++++++++++++++++
 src/label/labelLayoutHelper.ts            | 352 +++++++++++++++
 src/model/Model.ts                        |  64 ++-
 src/model/Series.ts                       |   7 +-
 src/model/globalDefault.ts                |  14 +-
 src/model/mixin/dataFormat.ts             |  47 +-
 src/preprocessor/backwardCompat.ts        |  65 ++-
 src/preprocessor/helper/compatStyle.ts    |   2 +-
 src/stream/Scheduler.ts                   |  21 +-
 src/theme/dark.ts                         |   2 +
 src/util/graphic.ts                       | 364 +++++++++------
 src/util/layout.ts                        |   1 +
 src/util/types.ts                         |  93 +++-
 src/view/Chart.ts                         |   6 +
 src/visual/style.ts                       |   2 +-
 test/animation-additive.html              | 162 +++++++
 test/aria-pie.html                        |   1 +
 test/bar-stack.html                       |  31 +-
 test/custom-transition.html               |  94 ++--
 test/custom-transition2.html              | 620 +++++++++++++++++++++++--
 test/graph-label-rotate.html              |  14 +-
 test/label-layout.html                    | 695 ++++++++++++++++++++++++++++
 test/pie-alignTo.html                     |  94 ++--
 test/pie-label-extreme.html               | 728 ++++++++++++++++++++++++++++++
 test/pie-label-mobile.html                | 201 +++++++++
 test/pie-label.html                       | 271 ++++++++++-
 test/sunburst-canvas.html                 |   2 -
 test/sunburst-drink.html                  |   3 +
 75 files changed, 5996 insertions(+), 1524 deletions(-)



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


[incubator-echarts] 05/16: fix: fix replaceMerge in feature "restore" and add test cases.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit fa7cd46ab3ee1f421d418d07caf6587c0b51c344
Author: 100pah <su...@gmail.com>
AuthorDate: Wed Jul 8 22:57:10 2020 +0800

    fix: fix replaceMerge in feature "restore" and add test cases.
---
 src/component/graphic.ts      |   3 +-
 src/model/Global.ts           |  58 ++++-----
 src/model/OptionManager.ts    | 138 ++++++++++++++-------
 src/util/model.ts             | 137 ++++++++++++++++-----
 test/option-replaceMerge.html | 271 ++++++++++++++++++++++++++++++++++++++++--
 5 files changed, 491 insertions(+), 116 deletions(-)

diff --git a/src/component/graphic.ts b/src/component/graphic.ts
index 1996918..4079ed3 100644
--- a/src/component/graphic.ts
+++ b/src/component/graphic.ts
@@ -142,8 +142,7 @@ const GraphicModel = echarts.extendComponentModel({
         const flattenedList = [];
         this._flatten(newList, flattenedList);
 
-        const mappingResult = modelUtil.mappingToExistsInNormalMerge(existList, flattenedList);
-        modelUtil.makeIdAndName(mappingResult);
+        const mappingResult = modelUtil.mappingToExists(existList, flattenedList, 'normalMerge');
 
         // Clear elOptionsToUpdate
         const elOptionsToUpdate = this._elOptionsToUpdate = [];
diff --git a/src/model/Global.ts b/src/model/Global.ts
index fe9edae..5f5555e 100644
--- a/src/model/Global.ts
+++ b/src/model/Global.ts
@@ -176,6 +176,15 @@ class GlobalModel extends Model<ECUnitOption> {
             this.restoreData();
         }
 
+        // By design, if `setOption(option2)` at the second time, and `option2` is a `ECUnitOption`,
+        // it should better not have the same props with `MediaUnit['option']`.
+        // Becuase either `option2` or `MediaUnit['option']` will be always merged to "current option"
+        // rather than original "baseOption". If they both override a prop, the result might be
+        // unexpected when media state changed after `setOption` called.
+        // If we really need to modify a props in each `MediaUnit['option']`, use the full version
+        // (`{baseOption, media}`) in `setOption`.
+        // For `timeline`, the case is the same.
+
         if (!type || type === 'recreate' || type === 'timeline') {
             const timelineOption = optionManager.getTimelineOption(this);
             if (timelineOption) {
@@ -220,7 +229,7 @@ class GlobalModel extends Model<ECUnitOption> {
                 return;
             }
 
-            if (!(ComponentModel as ComponentModelConstructor).hasClass(mainType)) {
+            if (!ComponentModel.hasClass(mainType)) {
                 // globalSettingTask.dirty();
                 option[mainType] = option[mainType] == null
                     ? clone(componentOption)
@@ -247,20 +256,12 @@ class GlobalModel extends Model<ECUnitOption> {
             const newCmptOptionList = modelUtil.normalizeToArray(newOption[mainType]);
 
             const oldCmptList = componentsMap.get(mainType);
-            const mapResult = replaceMergeMainTypeMap && replaceMergeMainTypeMap.get(mainType)
-                ? modelUtil.mappingToExistsInReplaceMerge(oldCmptList, newCmptOptionList)
-                : modelUtil.mappingToExistsInNormalMerge(oldCmptList, newCmptOptionList);
-
-            modelUtil.makeIdAndName(mapResult);
+            const mergeMode = (replaceMergeMainTypeMap && replaceMergeMainTypeMap.get(mainType))
+                ? 'replaceMerge' : 'normalMerge';
+            const mappingResult = modelUtil.mappingToExists(oldCmptList, newCmptOptionList, mergeMode);
 
             // Set mainType and complete subType.
-            each(mapResult, function (item) {
-                const opt = item.newOption;
-                if (isObject(opt)) {
-                    item.keyInfo.mainType = mainType;
-                    item.keyInfo.subType = determineSubType(mainType, opt, item.existing);
-                }
-            });
+            modelUtil.setComponentTypeToKeyInfo(mappingResult, mainType, ComponentModel as ComponentModelConstructor);
 
             // Set it before the travel, in case that `this._componentsMap` is
             // used in some `init` or `merge` of components.
@@ -271,7 +272,7 @@ class GlobalModel extends Model<ECUnitOption> {
             const cmptsByMainType = [] as ComponentModel[];
             let cmptsCountByMainType = 0;
 
-            each(mapResult, function (resultItem, index) {
+            each(mappingResult, function (resultItem, index) {
                 let componentModel = resultItem.existing;
                 const newCmptOption = resultItem.newOption;
 
@@ -309,6 +310,7 @@ class GlobalModel extends Model<ECUnitOption> {
                         componentModel = new ComponentModelClass(
                             newCmptOption, this, this, extraOpt
                         );
+                        // Assign `keyInfo`
                         extend(componentModel, extraOpt);
                         if (resultItem.brandNew) {
                             componentModel.__requireNewView = true;
@@ -357,9 +359,9 @@ class GlobalModel extends Model<ECUnitOption> {
     getOption(): ECUnitOption {
         const option = clone(this.option);
 
-        each(option, function (opts, mainType) {
-            if ((ComponentModel as ComponentModelConstructor).hasClass(mainType)) {
-                opts = modelUtil.normalizeToArray(opts);
+        each(option, function (optInMainType, mainType) {
+            if (ComponentModel.hasClass(mainType)) {
+                const opts = modelUtil.normalizeToArray(optInMainType);
                 // Inner cmpts need to be removed.
                 // Inner cmpts might not be at last since ec5.0, but still
                 // compatible for users: if inner cmpt at last, splice the returned array.
@@ -739,8 +741,6 @@ class GlobalModel extends Model<ECUnitOption> {
         };
 
         initBase = function (ecModel: GlobalModel, baseOption: ECUnitOption): void {
-            baseOption = baseOption;
-
             // Using OPTION_INNER_KEY to mark that this option can not be used outside,
             // i.e. `chart.setOption(chart.getModel().option);` is forbiden.
             ecModel.option = {} as ECUnitOption;
@@ -827,7 +827,7 @@ function mergeTheme(option: ECUnitOption, theme: ThemeOption): void {
         }
         // If it is component model mainType, the model handles that merge later.
         // otherwise, merge them here.
-        if (!(ComponentModel as ComponentModelConstructor).hasClass(name)) {
+        if (!ComponentModel.hasClass(name)) {
             if (typeof themeItem === 'object') {
                 option[name] = !option[name]
                     ? clone(themeItem)
@@ -842,22 +842,6 @@ function mergeTheme(option: ECUnitOption, theme: ThemeOption): void {
     });
 }
 
-function determineSubType(
-    mainType: ComponentMainType,
-    newCmptOption: ComponentOption,
-    existComponent: {subType: ComponentSubType} | ComponentModel
-): ComponentSubType {
-    const subType = newCmptOption.type
-        ? newCmptOption.type
-        : existComponent
-        ? existComponent.subType
-        // Use determineSubType only when there is no existComponent.
-        : (ComponentModel as ComponentModelConstructor).determineSubType(mainType, newCmptOption);
-
-    // tooltip, markline, markpoint may always has no subType
-    return subType;
-}
-
 function queryByIdOrName<T extends { id?: string, name?: string }>(
     attr: 'id' | 'name',
     idOrName: string | string[],
@@ -888,7 +872,7 @@ function normalizeReplaceMergeInput(opts: GlobalModelSetOptionOpts): InnerSetOpt
     opts && each(modelUtil.normalizeToArray(opts.replaceMerge), function (mainType) {
         if (__DEV__) {
             assert(
-                (ComponentModel as ComponentModelConstructor).hasClass(mainType),
+                ComponentModel.hasClass(mainType),
                 '"' + mainType + '" is not valid component main type in "replaceMerge"'
             );
         }
diff --git a/src/model/OptionManager.ts b/src/model/OptionManager.ts
index ee195e8..fd4a6fc 100644
--- a/src/model/OptionManager.ts
+++ b/src/model/OptionManager.ts
@@ -22,17 +22,18 @@
  */
 
 
-import * as zrUtil from 'zrender/src/core/util';
-import * as modelUtil from '../util/model';
 import ComponentModel, { ComponentModelConstructor } from './Component';
 import ExtensionAPI from '../ExtensionAPI';
-import { OptionPreprocessor, MediaQuery, ECUnitOption, MediaUnit, ECOption } from '../util/types';
+import {
+    OptionPreprocessor, MediaQuery, ECUnitOption, MediaUnit, ECOption, SeriesOption, ComponentOption
+} from '../util/types';
 import GlobalModel, { InnerSetOptionOpts } from './Global';
-
-const each = zrUtil.each;
-const clone = zrUtil.clone;
-const map = zrUtil.map;
-const merge = zrUtil.merge;
+import {
+    MappingExistingItem, normalizeToArray, setComponentTypeToKeyInfo, mappingToExists
+} from '../util/model';
+import {
+    each, clone, map, merge, isTypedArray, setAsPrimitive, HashMap, createHashMap, extend
+} from 'zrender/src/core/util';
 
 const QUERY_REG = /^(min|max)?(.+)$/;
 
@@ -43,6 +44,9 @@ interface ParsedRawOption {
     mediaList: MediaUnit[];
 }
 
+// Key: mainType
+type FakeComponentsMap = HashMap<(MappingExistingItem & { subType: string })[]>;
+
 /**
  * TERM EXPLANATIONS:
  * See `ECOption` and `ECUnitOption` in `src/util/types.ts`.
@@ -65,6 +69,8 @@ class OptionManager {
 
     private _optionBackup: ParsedRawOption;
 
+    private _fakeCmptsMap: FakeComponentsMap;
+
     private _newBaseOption: ECUnitOption;
 
     // timeline.notMerge is not supported in ec3. Firstly there is rearly
@@ -87,8 +93,8 @@ class OptionManager {
     ): void {
         if (rawOption) {
             // That set dat primitive is dangerous if user reuse the data when setOption again.
-            zrUtil.each(modelUtil.normalizeToArray((rawOption as ECUnitOption).series), function (series) {
-                series && series.data && zrUtil.isTypedArray(series.data) && zrUtil.setAsPrimitive(series.data);
+            each(normalizeToArray((rawOption as ECUnitOption).series), function (series: SeriesOption) {
+                series && series.data && isTypedArray(series.data) && setAsPrimitive(series.data);
             });
         }
 
@@ -101,28 +107,36 @@ class OptionManager {
         // If some property is set in timeline options or media option but
         // not set in baseOption, a warning should be given.
 
-        const oldOptionBackup = this._optionBackup;
+        const optionBackup = this._optionBackup;
         const newParsedOption = parseRawOption(
-            rawOption, optionPreprocessorFuncs, !oldOptionBackup
+            rawOption, optionPreprocessorFuncs, !optionBackup
         );
         this._newBaseOption = newParsedOption.baseOption;
 
         // For setOption at second time (using merge mode);
-        if (oldOptionBackup) {
-            // Only baseOption can be merged.
-            mergeOption(oldOptionBackup.baseOption, newParsedOption.baseOption, opt);
+        if (optionBackup) {
+            // The first merge is delayed, becuase in most cases, users do not call `setOption` twice.
+            let fakeCmptsMap = this._fakeCmptsMap;
+            if (!fakeCmptsMap) {
+                fakeCmptsMap = this._fakeCmptsMap = createHashMap();
+                mergeToBackupOption(fakeCmptsMap, null, optionBackup.baseOption, null);
+            }
+
+            mergeToBackupOption(
+                fakeCmptsMap, optionBackup.baseOption, newParsedOption.baseOption, opt
+            );
 
             // For simplicity, timeline options and media options do not support merge,
             // that is, if you `setOption` twice and both has timeline options, the latter
             // timeline opitons will not be merged to the formers, but just substitude them.
             if (newParsedOption.timelineOptions.length) {
-                oldOptionBackup.timelineOptions = newParsedOption.timelineOptions;
+                optionBackup.timelineOptions = newParsedOption.timelineOptions;
             }
             if (newParsedOption.mediaList.length) {
-                oldOptionBackup.mediaList = newParsedOption.mediaList;
+                optionBackup.mediaList = newParsedOption.mediaList;
             }
             if (newParsedOption.mediaDefault) {
-                oldOptionBackup.mediaDefault = newParsedOption.mediaDefault;
+                optionBackup.mediaDefault = newParsedOption.mediaDefault;
             }
         }
         else {
@@ -133,14 +147,14 @@ class OptionManager {
     mountOption(isRecreate: boolean): ECUnitOption {
         const optionBackup = this._optionBackup;
 
-        this._timelineOptions = map(optionBackup.timelineOptions, clone);
-        this._mediaList = map(optionBackup.mediaList, clone);
-        this._mediaDefault = clone(optionBackup.mediaDefault);
+        this._timelineOptions = optionBackup.timelineOptions;
+        this._mediaList = optionBackup.mediaList;
+        this._mediaDefault = optionBackup.mediaDefault;
         this._currentMediaIndices = [];
 
         return clone(isRecreate
             // this._optionBackup.baseOption, which is created at the first `setOption`
-            // called, and is merged into every new option by inner method `mergeOption`
+            // called, and is merged into every new option by inner method `mergeToBackupOption`
             // each time `setOption` called, can be only used in `isRecreate`, because
             // its reliability is under suspicion. In other cases option merge is
             // performed by `model.mergeOption`.
@@ -263,7 +277,7 @@ function parseRawOption(
 
     // Preprocess.
     each([baseOption].concat(timelineOptions)
-        .concat(zrUtil.map(mediaList, function (media) {
+        .concat(map(mediaList, function (media) {
             return media.option;
         })),
         function (option) {
@@ -295,7 +309,7 @@ function applyMediaQuery(query: MediaQuery, ecWidth: number, ecHeight: number):
 
     let applicatable = true;
 
-    zrUtil.each(query, function (value: number, attr) {
+    each(query, function (value: number, attr) {
         const matched = attr.match(QUERY_REG);
 
         if (!matched || !matched[1] || !matched[2]) {
@@ -359,33 +373,73 @@ function indicesEquals(indices1: number[], indices2: number[]): boolean {
  * When "resotre" action triggered, model from `componentActionModel` will be discarded
  * instead of recreating the "ecModel" from the "_optionBackup".
  */
-function mergeOption(
-    oldOption: ECUnitOption, newOption: ECUnitOption, opt: InnerSetOptionOpts
+function mergeToBackupOption(
+    fakeCmptsMap: FakeComponentsMap,
+    // `tarOption` Can be null/undefined, means init
+    tarOption: ECUnitOption,
+    newOption: ECUnitOption,
+    // Can be null/undefined
+    opt: InnerSetOptionOpts
 ): void {
     newOption = newOption || {} as ECUnitOption;
+    const notInit = !!tarOption;
 
-    each(newOption, function (newCptOpt, mainType) {
-        if (newCptOpt == null) {
+    each(newOption, function (newOptsInMainType, mainType) {
+        if (newOptsInMainType == null) {
             return;
         }
 
-        let oldCptOpt = oldOption[mainType];
-
-        if (!(ComponentModel as ComponentModelConstructor).hasClass(mainType)) {
-            oldOption[mainType] = merge(oldCptOpt, newCptOpt, true);
+        if (!ComponentModel.hasClass(mainType)) {
+            if (tarOption) {
+                tarOption[mainType] = merge(tarOption[mainType], newOptsInMainType, true);
+            }
         }
         else {
-            newCptOpt = modelUtil.normalizeToArray(newCptOpt);
-            oldCptOpt = modelUtil.normalizeToArray(oldCptOpt);
-
-            const mapResult = opt.replaceMergeMainTypeMap.get(mainType)
-                ? modelUtil.mappingToExistsInReplaceMerge(oldCptOpt, newCptOpt)
-                : modelUtil.mappingToExistsInNormalMerge(oldCptOpt, newCptOpt);
+            const oldTarOptsInMainType = notInit ? normalizeToArray(tarOption[mainType]) : null;
+            const oldFakeCmptsInMainType = fakeCmptsMap.get(mainType) || [];
+            const resultTarOptsInMainType = notInit ? (tarOption[mainType] = [] as ComponentOption[]) : null;
+            const resultFakeCmptsInMainType = fakeCmptsMap.set(mainType, []);
+
+            const mappingResult = mappingToExists(
+                oldFakeCmptsInMainType,
+                normalizeToArray(newOptsInMainType),
+                (opt && opt.replaceMergeMainTypeMap.get(mainType)) ? 'replaceMerge' : 'normalMerge'
+            );
+            setComponentTypeToKeyInfo(mappingResult, mainType, ComponentModel as ComponentModelConstructor);
+
+            each(mappingResult, function (resultItem, index) {
+                // The same logic as `Global.ts#_mergeOption`.
+                let fakeCmpt = resultItem.existing;
+                const newOption = resultItem.newOption;
+                const keyInfo = resultItem.keyInfo;
+                let fakeCmptOpt;
+
+                if (!newOption) {
+                    fakeCmptOpt = oldTarOptsInMainType[index];
+                }
+                else {
+                    if (fakeCmpt && fakeCmpt.subType === keyInfo.subType) {
+                        fakeCmpt.name = keyInfo.name;
+                        if (notInit) {
+                            fakeCmptOpt = merge(oldTarOptsInMainType[index], newOption, true);
+                        }
+                    }
+                    else {
+                        fakeCmpt = extend({}, keyInfo);
+                        if (notInit) {
+                            fakeCmptOpt = clone(newOption);
+                        }
+                    }
+                }
 
-            oldOption[mainType] = map(mapResult, function (item) {
-                return (item.newOption && item.existing)
-                    ? merge(item.existing, item.newOption, true)
-                    : (item.existing || item.newOption);
+                if (fakeCmpt) {
+                    notInit && resultTarOptsInMainType.push(fakeCmptOpt);
+                    resultFakeCmptsInMainType.push(fakeCmpt);
+                }
+                else {
+                    notInit && resultTarOptsInMainType.push(void 0);
+                    resultFakeCmptsInMainType.push(void 0);
+                }
             });
         }
     });
diff --git a/src/util/model.ts b/src/util/model.ts
index 95cb000..826c23f 100644
--- a/src/util/model.ts
+++ b/src/util/model.ts
@@ -30,7 +30,7 @@ import {
 } from 'zrender/src/core/util';
 import env from 'zrender/src/core/env';
 import GlobalModel, { QueryConditionKindB } from '../model/Global';
-import ComponentModel from '../model/Component';
+import ComponentModel, {ComponentModelConstructor} from '../model/Component';
 import List from '../data/List';
 import {
     ComponentOption,
@@ -142,7 +142,14 @@ export function isDataItemOption(dataItem: OptionDataItem): boolean {
         // && !(dataItem[0] && isObject(dataItem[0]) && !(dataItem[0] instanceof Array));
 }
 
-type MappingExistItem = {id?: string, name?: string} | ComponentModel;
+// Compatible with previous definition: id could be number (but not recommanded).
+// number and number-like string are trade the same when compare.
+// number id will not be converted to string in option.
+// number id will be converted to string in component instance id.
+export interface MappingExistingItem {
+    id?: string | number;
+    name?: string;
+};
 /**
  * The array `MappingResult<T>[]` exactly represents the content of the result
  * components array after merge.
@@ -150,7 +157,7 @@ type MappingExistItem = {id?: string, name?: string} | ComponentModel;
  * Items will not be `null`/`undefined` even if the corresponding `existings` will be removed.
  */
 type MappingResult<T> = MappingResultItem<T>[];
-interface MappingResultItem<T> {
+interface MappingResultItem<T extends MappingExistingItem = MappingExistingItem> {
     // Existing component instance.
     existing?: T;
     // The mapped new component option.
@@ -160,15 +167,26 @@ interface MappingResultItem<T> {
     brandNew?: boolean;
     // id?: string;
     // name?: string;
-    // keyInfo for new component option.
+    // keyInfo for new component.
+    // All of them will be assigned to a created component instance.
     keyInfo?: {
-        name?: string,
-        id?: string,
-        mainType?: ComponentMainType,
-        subType?: ComponentSubType
+        name: string,
+        id: string,
+        mainType: ComponentMainType,
+        subType: ComponentSubType
     };
 }
 
+export function mappingToExists<T extends MappingExistingItem>(
+    existings: T[],
+    newCmptOptions: ComponentOption[],
+    mode: 'normalMerge' | 'replaceMerge'
+): MappingResult<T> {
+    return mode === 'replaceMerge'
+        ? mappingToExistsInReplaceMerge(existings, newCmptOptions)
+        : mappingToExistsInNormalMerge(existings, newCmptOptions);
+}
+
 /**
  * Mapping to existings for merge.
  * The mapping result (merge result) will keep the order of the existing
@@ -179,7 +197,7 @@ interface MappingResultItem<T> {
  *
  * @return See the comment of <MappingResult>.
  */
-export function mappingToExistsInNormalMerge<T extends MappingExistItem>(
+function mappingToExistsInNormalMerge<T extends MappingExistingItem>(
     existings: T[],
     newCmptOptions: ComponentOption[]
 ): MappingResult<T> {
@@ -205,9 +223,8 @@ export function mappingToExistsInNormalMerge<T extends MappingExistItem>(
         for (let i = 0; i < result.length; i++) {
             const existing = result[i].existing;
             if (!result[i].newOption // Consider name: two map to one.
-                && cmptOption.id != null
                 && existing
-                && existing.id === cmptOption.id + ''
+                && keyExistAndEqual('id', existing, cmptOption)
             ) {
                 result[i].newOption = cmptOption;
                 newCmptOptions[index] = null;
@@ -221,10 +238,9 @@ export function mappingToExistsInNormalMerge<T extends MappingExistItem>(
                 // Can not match when both ids existing but different.
                 && existing
                 && (existing.id == null || cmptOption.id == null)
-                && cmptOption.name != null
                 && !isIdInner(cmptOption)
                 && !isIdInner(existing)
-                && existing.name === cmptOption.name + ''
+                && keyExistAndEqual('name', existing, cmptOption)
             ) {
                 result[i].newOption = cmptOption;
                 newCmptOptions[index] = null;
@@ -235,6 +251,8 @@ export function mappingToExistsInNormalMerge<T extends MappingExistItem>(
 
     mappingByIndexFinally(newCmptOptions, result, false);
 
+    makeIdAndName(result);
+
     return result;
 }
 
@@ -250,7 +268,7 @@ export function mappingToExistsInNormalMerge<T extends MappingExistItem>(
  *
  * @return See the comment of <MappingResult>.
  */
-export function mappingToExistsInReplaceMerge<T extends MappingExistItem>(
+function mappingToExistsInReplaceMerge<T extends MappingExistingItem>(
     existings: T[],
     newCmptOptions: ComponentOption[]
 ): MappingResult<T> {
@@ -272,7 +290,10 @@ export function mappingToExistsInReplaceMerge<T extends MappingExistItem>(
                 innerExisting = existing;
             }
             // Input with inner id is allowed for convenience of some internal usage.
-            existingIdIdxMap.set(existing.id, index);
+            // When `existing` is rawOption (see `OptionManager`#`mergeOption`), id might be empty.
+            if (existing.id != null) {
+                existingIdIdxMap.set(existing.id, index);
+            }
         }
         result.push({ existing: innerExisting });
     }
@@ -283,7 +304,10 @@ export function mappingToExistsInReplaceMerge<T extends MappingExistItem>(
             newCmptOptions[index] = null;
             return;
         }
-        const optionId = cmptOption.id + '';
+        if (cmptOption.id == null) {
+            return;
+        }
+        const optionId = makeComparableKey(cmptOption.id);
         const existingIdx = existingIdIdxMap.get(optionId);
         if (existingIdx != null) {
             if (__DEV__) {
@@ -303,12 +327,14 @@ export function mappingToExistsInReplaceMerge<T extends MappingExistItem>(
 
     mappingByIndexFinally(newCmptOptions, result, true);
 
+    makeIdAndName(result);
+
     // The array `result` MUST NOT contain elided items, otherwise the
     // forEach will ommit those items and result in incorrect result.
     return result;
 }
 
-function mappingByIndexFinally<T extends MappingExistItem>(
+function mappingByIndexFinally<T extends MappingExistingItem>(
     newCmptOptions: ComponentOption[],
     mappingResult: MappingResult<T>,
     allBrandNew: boolean
@@ -331,7 +357,11 @@ function mappingByIndexFinally<T extends MappingExistItem>(
             // not be merged to the existings with different id. Because id should not be overwritten.
             // (3) Name can be overwritten, because axis use name as 'show label text'.
             && (
-                (cmptOption.id != null && resultItem.existing)
+                (
+                    cmptOption.id != null
+                    && resultItem.existing
+                    && !keyExistAndEqual('id', cmptOption, resultItem.existing)
+                )
                 || resultItem.newOption
                 || isIdInner(resultItem.existing)
             )
@@ -354,8 +384,8 @@ function mappingByIndexFinally<T extends MappingExistItem>(
  * Make id and name for mapping result (result of mappingToExists)
  * into `keyInfo` field.
  */
-export function makeIdAndName(
-    mapResult: MappingResult<MappingExistItem>
+function makeIdAndName(
+    mapResult: MappingResult<MappingExistingItem>
 ): void {
     // We use this id to hash component models and view instances
     // in echarts. id can be specified by user, or auto generated.
@@ -385,7 +415,7 @@ export function makeIdAndName(
         );
 
         opt && opt.id != null && idMap.set(opt.id, item);
-        !item.keyInfo && (item.keyInfo = {});
+        !item.keyInfo && (item.keyInfo = {} as MappingResultItem['keyInfo']);
     });
 
     // Make name and id.
@@ -403,7 +433,7 @@ export function makeIdAndName(
         // only in that case: setOption with 'not merge mode' and view
         // instance will be recreated, which can be accepted.
         keyInfo.name = opt.name != null
-            ? opt.name + ''
+            ? makeComparableKey(opt.name)
             : existing
             ? existing.name
             // Avoid diffferent series has the same name,
@@ -411,10 +441,10 @@ export function makeIdAndName(
             : DUMMY_COMPONENT_NAME_PREFIX + index;
 
         if (existing) {
-            keyInfo.id = existing.id;
+            keyInfo.id = makeComparableKey(existing.id);
         }
         else if (opt.id != null) {
-            keyInfo.id = opt.id + '';
+            keyInfo.id = makeComparableKey(opt.id);
         }
         else {
             // Consider this situatoin:
@@ -433,6 +463,25 @@ export function makeIdAndName(
     });
 }
 
+function keyExistAndEqual(attr: 'id' | 'name', obj1: MappingExistingItem, obj2: MappingExistingItem): boolean {
+    const key1 = obj1[attr];
+    const key2 = obj2[attr];
+    // See `MappingExistingItem`. `id` and `name` trade number-like string equals to number.
+    return key1 != null && key2 != null && key1 + '' === key2 + '';
+}
+
+/**
+ * @return return null if not exist.
+ */
+function makeComparableKey(val: string | number): string {
+    if (__DEV__) {
+        if (val == null) {
+            throw new Error();
+        }
+    }
+    return val + '';
+}
+
 export function isNameSpecified(componentModel: ComponentModel): boolean {
     const name = componentModel.name;
     // Is specified when `indexOf` get -1 or > 0.
@@ -444,12 +493,46 @@ export function isNameSpecified(componentModel: ComponentModel): boolean {
  * @param {Object} cmptOption
  * @return {boolean}
  */
-export function isIdInner(cmptOption: ComponentOption): boolean {
+export function isIdInner(cmptOption: MappingExistingItem): boolean {
     return cmptOption
-        && cmptOption.id
-        && (cmptOption.id + '').indexOf('\0_ec_\0') === 0;
+        && cmptOption.id != null
+        && makeComparableKey(cmptOption.id).indexOf('\0_ec_\0') === 0;
+}
+
+
+export function setComponentTypeToKeyInfo(
+    mappingResult: MappingResult<MappingExistingItem & { subType?: ComponentSubType }>,
+    mainType: ComponentMainType,
+    componentModelCtor: ComponentModelConstructor
+): void {
+    // Set mainType and complete subType.
+    each(mappingResult, function (item) {
+        const newOption = item.newOption;
+        if (isObject(newOption)) {
+            item.keyInfo.mainType = mainType;
+            item.keyInfo.subType = determineSubType(mainType, newOption, item.existing, componentModelCtor);
+        }
+    });
 }
 
+function determineSubType(
+    mainType: ComponentMainType,
+    newCmptOption: ComponentOption,
+    existComponent: { subType?: ComponentSubType },
+    componentModelCtor: ComponentModelConstructor
+): ComponentSubType {
+    const subType = newCmptOption.type
+        ? newCmptOption.type
+        : existComponent
+        ? existComponent.subType
+        // Use determineSubType only when there is no existComponent.
+        : (componentModelCtor as ComponentModelConstructor).determineSubType(mainType, newCmptOption);
+
+    // tooltip, markline, markpoint may always has no subType
+    return subType;
+}
+
+
 type BatchItem = {
     seriesId: string,
     dataIndex: number[]
diff --git a/test/option-replaceMerge.html b/test/option-replaceMerge.html
index 2513ec2..30dc848 100644
--- a/test/option-replaceMerge.html
+++ b/test/option-replaceMerge.html
@@ -46,6 +46,8 @@ under the License.
         <div id="main_normalMerge_add_find_hole"></div>
         <div id="main_replaceMerge_inner_and_other_cmpt_not_effect"></div>
         <div id="main_replaceMerge_remove_all"></div>
+        <div id="main_replaceMerge_reproduce_by_getOption_src"></div>
+        <div id="main_replaceMerge_reproduce_by_getOption_tar"></div>
 
 
 
@@ -322,17 +324,39 @@ under the License.
 
         <script>
         require(['echarts'], function (echarts) {
-            var option = makeBasicOption();
+            function makeInitOption() {
+                var option = makeBasicOption();
+
+                option.toolbox = {
+                    left: 'center',
+                    top: 25,
+                    feature: {
+                        magicType: {
+                            show: true,
+                            type: ['line', 'bar']
+                        },
+                        dataZoom: {},
+                        restore: {},
+                        dataView: {}
+                    }
+                };
+
+                return option;
+            }
 
             var chart = testHelper.create(echarts, 'main_replaceMerge_add_find_hole', {
                 title: [
                     '**replaceMerge**: add (find the first hole)',
-                    'click the buttons one by one from left to right',
-                    'should show **TWO checked: Pass**'
+                    '<1> click the buttons one by one from left to right',
+                    'should show **TWO checked: Pass**',
+                    '<2> use **toolbox.dataZoom**, then click btns again, should be OK',
+                    '<3> use **toolbox.magicType**, then click btns again, should be OK',
+                    '<4> use **toolbox.dataView**, then click btns again, should be OK',
+                    '<5> use **toolbox.restore**, then click btns again, should be OK',
                 ],
-                option: option,
+                option: makeInitOption(),
                 buttons: [{
-                    text: 'setOption_remove',
+                    text: 'remove some',
                     onclick: function () {
                         chart.setOption({
                             series: [{
@@ -345,7 +369,7 @@ under the License.
                         }, {replaceMerge: 'series'});
                     }
                 }, {
-                    text: 'check after click setOption_remove',
+                    text: 'then check',
                     onclick: function () {
                         testHelper.printAssert(chart, function (assert) {
                             var seriesModels = chart.getModel().getSeries();
@@ -368,10 +392,22 @@ under the License.
 
                             assert(chart.getModel().getSeriesByIndex(1) == null);
                             assert(chart.getModel().getComponent('series', 1) == null);
+
+                            var eachModelResult = [];
+                            var eachIndexResult = [];
+                            chart.getModel().eachComponent('series', function (seriesModel, seriesIndex) {
+                                eachModelResult.push(seriesModel);
+                                eachIndexResult.push(seriesIndex);
+                            });
+                            assert(eachModelResult.length === 3 && eachIndexResult.length === 3);
+                            for (var i = 0; i < 3; i++) {
+                                assert(eachModelResult[i] === seriesModels[i]);
+                                assert(eachIndexResult[i] === seriesModels[i].componentIndex);
+                            }
                         });
                     }
                 }, {
-                    text: 'setOption_replaceMerge',
+                    text: 'replaceMerge',
                     onclick: function () {
                         chart.setOption({
                             series: [{
@@ -392,7 +428,7 @@ under the License.
                         }, {replaceMerge: 'series'});
                     }
                 }, {
-                    text: 'check after click setOption_replaceMerge',
+                    text: 'then check',
                     onclick: function () {
                         testHelper.printAssert(chart, function (assert) {
                             var seriesModels = chart.getModel().getSeries();
@@ -420,8 +456,25 @@ under the License.
 
                             assert(chart.getModel().getSeriesByIndex(1).id == 'm');
                             assert(chart.getModel().getComponent('series', 1).id == 'm');
+
+                            var eachModelResult = [];
+                            var eachIndexResult = [];
+                            chart.getModel().eachComponent('series', function (seriesModel, seriesIndex) {
+                                eachModelResult.push(seriesModel);
+                                eachIndexResult.push(seriesIndex);
+                            });
+                            assert(eachModelResult.length === 5 && eachIndexResult.length === 5);
+                            for (var i = 0; i < 5; i++) {
+                                assert(eachModelResult[i] === seriesModels[i]);
+                                assert(eachIndexResult[i] === seriesModels[i].componentIndex);
+                            }
                         });
                     }
+                }, {
+                    text: 'reset all',
+                    onclick: function () {
+                        chart.setOption(makeInitOption(), true);
+                    }
                 }],
                 height: 300
             });
@@ -656,6 +709,208 @@ under the License.
 
 
 
+        <script>
+        require(['echarts'], function (echarts) {
+            function makeInitOption() {
+                var option = {
+                    grid: [{
+                        right: '55%',
+                        bottom: 30
+                    }, {
+                        id: 'grid-r',
+                        left: '55%',
+                        bottom: 30
+                    }],
+                    xAxis: [{
+                        type: 'category',
+                        gridIndex: 0,
+                    }, {
+                        id: 'xAxis-r',
+                        type: 'category',
+                        gridIndex: 1,
+                    }],
+                    yAxis: [{
+                        gridIndex: 0
+                    }, {
+                        id: 'yAxis-r',
+                        gridIndex: 1
+                    }],
+                    legend: {},
+                    tooltip: {},
+                    // dataZoom: [{
+                    //     type: 'slider'
+                    // }, {
+                    //     type: 'inside'
+                    // }],
+                    toolbox: {
+                        left: 'center',
+                        top: 25,
+                        feature: {
+                            magicType: {
+                                show: true,
+                                type: ['line', 'bar']
+                            },
+                            dataZoom: {},
+                            restore: {},
+                            dataView: {}
+                        }
+                    },
+                    series: [{
+                        id: 'a',
+                        name: 'aa',
+                        type: 'line',
+                        data: [['a11', 22], ['a33', 44]]
+                    }, {
+                        id: 'b',
+                        name: 'bb',
+                        type: 'line',
+                        data: [['a11', 55], ['a33', 77]],
+                        xAxisIndex: 1,
+                        yAxisIndex: 1,
+                    }, {
+                        id: 'c',
+                        name: 'cc',
+                        type: 'line',
+                        data: [['a11', 66], ['a33', 100]]
+                    }, {
+                        name: 'no_id',
+                        type: 'line',
+                        data: [['a11', 130], ['a33', 160]]
+                    }]
+                };
+
+                return option;
+            }
+
+            var chartSrc = testHelper.create(echarts, 'main_replaceMerge_reproduce_by_getOption_src', {
+                title: [
+                    '**replaceMerge**: reproduce via getOption',
+                    'click the buttons one by one from left to right',
+                    'should show **TWO checked: Pass**',
+                    'The chart reproduced below should be **the same**'
+                ],
+                option: makeInitOption(),
+                buttons: [{
+                    text: 'remove left grid',
+                    onclick: function () {
+                        chartSrc.setOption({
+                            grid: {
+                                id: 'grid-r'
+                            },
+                            xAxis: {
+                                id: 'xAxis-r'
+                            },
+                            yAxis: {
+                                id: 'yAxis-r'
+                            },
+                            series: [{
+                                id: 'b'
+                            }]
+                        }, { replaceMerge: ['series', 'grid', 'xAxis', 'yAxis'] });
+                    }
+                }, {
+                    text: 'reproduce',
+                    onclick: function () {
+                        testHelper.printAssert(chartSrc, function (assert) {
+                            var seriesModels = chartSrc.getModel().getSeries();
+                            assert(seriesModels.length === 3);
+                            assert(seriesModels[0].componentIndex === 0);
+                            assert(seriesModels[1].componentIndex === 2);
+                            assert(seriesModels[2].componentIndex === 3);
+                            assert(seriesModels[0].id === 'a');
+                            assert(seriesModels[1].id === 'c');
+                            assert(seriesModels[2].id === 'd');
+
+                            assert(chartSrc.getModel().getSeriesCount() === 3);
+
+                            var optionGotten = chartSrc.getOption();
+                            assert(optionGotten.series.length === 4);
+                            assert(optionGotten.series[0].name === 'aa');
+                            assert(optionGotten.series[1] == null);
+                            assert(optionGotten.series[2].name === 'cc');
+                            assert(optionGotten.series[3].name === 'dd');
+
+                            assert(chartSrc.getModel().getSeriesByIndex(1) == null);
+                            assert(chartSrc.getModel().getComponent('series', 1) == null);
+                        });
+                    }
+                }, {
+                    text: 'replaceMerge',
+                    onclick: function () {
+                        chartSrc.setOption({
+                            series: [{
+                                id: 'm',
+                                type: 'bar',
+                                data: [['a11', 22], ['a33', 44]]
+                            }, {
+                                id: 'n',
+                                type: 'bar',
+                                data: [['a11', 32], ['a33', 54]]
+                            }, {
+                                id: 'a'
+                            }, {
+                                id: 'c'
+                            }, {
+                                id: 'd'
+                            }]
+                        }, {replaceMerge: 'series'});
+                    }
+                }, {
+                    text: 'then check',
+                    onclick: function () {
+                        testHelper.printAssert(chartSrc, function (assert) {
+                            var seriesModels = chartSrc.getModel().getSeries();
+                            assert(seriesModels.length === 5);
+                            assert(seriesModels[0].componentIndex === 0);
+                            assert(seriesModels[1].componentIndex === 1);
+                            assert(seriesModels[2].componentIndex === 2);
+                            assert(seriesModels[3].componentIndex === 3);
+                            assert(seriesModels[4].componentIndex === 4);
+                            assert(seriesModels[0].id === 'a');
+                            assert(seriesModels[1].id === 'm');
+                            assert(seriesModels[2].id === 'c');
+                            assert(seriesModels[3].id === 'd');
+                            assert(seriesModels[4].id === 'n');
+
+                            assert(chartSrc.getModel().getSeriesCount() === 5);
+
+                            var optionGotten = chartSrc.getOption();
+                            assert(optionGotten.series.length === 5);
+                            assert(optionGotten.series[0].id === 'a');
+                            assert(optionGotten.series[1].id === 'm');
+                            assert(optionGotten.series[2].id === 'c');
+                            assert(optionGotten.series[3].id === 'd');
+                            assert(optionGotten.series[4].id === 'n');
+
+                            assert(chartSrc.getModel().getSeriesByIndex(1).id == 'm');
+                            assert(chartSrc.getModel().getComponent('series', 1).id == 'm');
+                        });
+                    }
+                }, {
+                    text: 'reset all',
+                    onclick: function () {
+                        chartSrc.setOption(makeInitOption(), true);
+                    }
+                }],
+                height: 200
+            });
+
+            var chartTar = testHelper.create(echarts, 'main_replaceMerge_reproduce_by_getOption_tar', {
+                title: [
+                    '↓↓↓ reproduce ↓↓↓ ',
+                ],
+                option: {},
+                height: 200
+            });
+
+        });
+        </script>
+
+
+
+
+
+
     </body>
 </html>
 


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


[incubator-echarts] 12/16: fix: (1) Inside dataZoom can not dispose when grid removed. (2) uniform the reference between components.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit f5a9667d8e234299b5eef35089015200d0456fdb
Author: 100pah <su...@gmail.com>
AuthorDate: Thu Jul 16 19:00:06 2020 +0800

    fix: (1) Inside dataZoom can not dispose when grid removed. (2) uniform the reference between components.
---
 src/chart/themeRiver/ThemeRiverSeries.ts   |   8 +-
 src/component/dataZoom/AxisProxy.ts        |  18 +-
 src/component/dataZoom/DataZoomModel.ts    |  15 +-
 src/component/dataZoom/DataZoomView.ts     |  70 --------
 src/component/dataZoom/InsideZoomView.ts   | 128 +++++++-------
 src/component/dataZoom/SliderZoomView.ts   |  91 ++++------
 src/component/dataZoom/helper.ts           |  88 ++++++++--
 src/component/dataZoom/roams.ts            | 265 +++++++++++++++++------------
 src/component/toolbox/feature/DataZoom.ts  |   6 +-
 src/component/toolbox/feature/MagicType.ts |   7 +-
 src/coord/axisModelCommonMixin.ts          |   1 -
 src/coord/cartesian/AxisModel.ts           |   7 +-
 src/coord/cartesian/Grid.ts                |   6 +-
 src/coord/cartesian/cartesianAxisHelper.ts |   5 +-
 src/coord/geo/Geo.ts                       |   6 +-
 src/coord/parallel/parallelCreator.ts      |   9 +-
 src/coord/polar/AxisModel.ts               |   7 +-
 src/coord/polar/polarCreator.ts            |   9 +-
 src/coord/single/singleCreator.ts          |   9 +-
 src/model/Component.ts                     |   6 +-
 src/model/referHelper.ts                   |  11 +-
 src/util/model.ts                          |  38 +++--
 test/dataZoom-feature.html                 |   2 +-
 test/option-replaceMerge.html              | 123 +++++++------
 24 files changed, 494 insertions(+), 441 deletions(-)

diff --git a/src/chart/themeRiver/ThemeRiverSeries.ts b/src/chart/themeRiver/ThemeRiverSeries.ts
index 3f0cc49..50c310b 100644
--- a/src/chart/themeRiver/ThemeRiverSeries.ts
+++ b/src/chart/themeRiver/ThemeRiverSeries.ts
@@ -22,7 +22,7 @@ import createDimensions from '../../data/helper/createDimensions';
 import {getDimensionTypeByAxis} from '../../data/helper/dimensionHelper';
 import List from '../../data/List';
 import * as zrUtil from 'zrender/src/core/util';
-import {groupData} from '../../util/model';
+import {groupData, SINGLE_REFERRING} from '../../util/model';
 import {encodeHTML} from '../../util/format';
 import LegendVisualProvider from '../../visual/LegendVisualProvider';
 import {
@@ -172,11 +172,7 @@ class ThemeRiverSeriesModel extends SeriesModel<ThemeRiverSeriesOption> {
      */
     getInitialData(option: ThemeRiverSeriesOption, ecModel: GlobalModel): List {
 
-        const singleAxisModel = ecModel.queryComponents({
-            mainType: 'singleAxis',
-            index: this.get('singleAxisIndex'),
-            id: this.get('singleAxisId')
-        })[0];
+        const singleAxisModel = this.getReferringComponents('singleAxis', SINGLE_REFERRING).models[0];
 
         const axisType = singleAxisModel.get('type');
 
diff --git a/src/component/dataZoom/AxisProxy.ts b/src/component/dataZoom/AxisProxy.ts
index 196c71d..f34a08b 100644
--- a/src/component/dataZoom/AxisProxy.ts
+++ b/src/component/dataZoom/AxisProxy.ts
@@ -19,7 +19,6 @@
 
 import * as zrUtil from 'zrender/src/core/util';
 import * as numberUtil from '../../util/number';
-import * as helper from './helper';
 import sliderMove from '../helper/sliderMove';
 import GlobalModel from '../../model/Global';
 import SeriesModel from '../../model/Series';
@@ -30,6 +29,8 @@ import DataZoomModel from './DataZoomModel';
 import { AxisBaseModel } from '../../coord/AxisBaseModel';
 import { unionAxisExtentFromData } from '../../coord/axisHelper';
 import { ensureScaleRawExtentInfo } from '../../coord/scaleRawExtentInfo';
+import { getAxisMainType, isCoordSupported, DataZoomAxisDimension } from './helper';
+import { SINGLE_REFERRING } from '../../util/model';
 
 const each = zrUtil.each;
 const asc = numberUtil.asc;
@@ -54,7 +55,7 @@ class AxisProxy {
 
     ecModel: GlobalModel;
 
-    private _dimName: string;
+    private _dimName: DataZoomAxisDimension;
     private _axisIndex: number;
 
     private _valueWindow: [number, number];
@@ -66,7 +67,12 @@ class AxisProxy {
 
     private _dataZoomModel: DataZoomModel;
 
-    constructor(dimName: string, axisIndex: number, dataZoomModel: DataZoomModel, ecModel: GlobalModel) {
+    constructor(
+        dimName: DataZoomAxisDimension,
+        axisIndex: number,
+        dataZoomModel: DataZoomModel,
+        ecModel: GlobalModel
+    ) {
         this._dimName = dimName;
 
         this._axisIndex = axisIndex;
@@ -107,9 +113,9 @@ class AxisProxy {
         const seriesModels: SeriesModel[] = [];
 
         this.ecModel.eachSeries(function (seriesModel) {
-            if (helper.isCoordSupported(seriesModel.get('coordinateSystem'))) {
-                const axisMainType = helper.getAxisMainType(this._dimName);
-                const axisModel = seriesModel.getReferringComponents(axisMainType, true).models[0];
+            if (isCoordSupported(seriesModel)) {
+                const axisMainType = getAxisMainType(this._dimName);
+                const axisModel = seriesModel.getReferringComponents(axisMainType, SINGLE_REFERRING).models[0];
                 if (axisModel && this._axisIndex === axisModel.componentIndex) {
                     seriesModels.push(seriesModel);
                 }
diff --git a/src/component/dataZoom/DataZoomModel.ts b/src/component/dataZoom/DataZoomModel.ts
index db5c440..38aa4e7 100644
--- a/src/component/dataZoom/DataZoomModel.ts
+++ b/src/component/dataZoom/DataZoomModel.ts
@@ -34,6 +34,7 @@ import {
 } from './helper';
 import SingleAxisModel from '../../coord/single/AxisModel';
 import { __DEV__ } from '../../config';
+import { MULTIPLE_REFERRING, SINGLE_REFERRING } from '../../util/model';
 
 
 export interface DataZoomOption extends ComponentOption {
@@ -170,6 +171,8 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
 
     private _targetAxisInfoMap: DataZoomTargetAxisInfoMap;
 
+    private _noTarget: boolean;
+
     /**
      * It is `[rangeModeForMin, rangeModeForMax]`.
      * The optional values for `rangeMode`:
@@ -274,13 +277,15 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
             this._orient = optionOrient || 'horizontal';
             this._fillAutoTargetAxisByOrient(targetAxisIndexMap, this._orient);
         }
+
+        this._noTarget = !targetAxisIndexMap.keys().length;
     }
 
     private _fillSpecifiedTargetAxis(targetAxisIndexMap: DataZoomTargetAxisInfoMap): boolean {
         let hasAxisSpecified = false;
 
         each(DATA_ZOOM_AXIS_DIMENSIONS, function (axisDim) {
-            const refering = this.getReferringComponents(getAxisMainType(axisDim), false);
+            const refering = this.getReferringComponents(getAxisMainType(axisDim), MULTIPLE_REFERRING);
             // When user set axisIndex as a empty array, we think that user specify axisIndex
             // but do not want use auto mode. Because empty array may be encountered when
             // some error occured.
@@ -331,10 +336,10 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
 
             // Find parallel axes in the same grid.
             if (axisDim === 'x' || axisDim === 'y') {
-                const gridModel = axisModel.getReferringComponents('grid', true).models[0];
+                const gridModel = axisModel.getReferringComponents('grid', SINGLE_REFERRING).models[0];
                 gridModel && each(axisModels, function (axModel) {
                     if (axisModel.componentIndex !== axModel.componentIndex
-                        && gridModel === axModel.getReferringComponents('grid', true).models[0]
+                        && gridModel === axModel.getReferringComponents('grid', SINGLE_REFERRING).models[0]
                     ) {
                         axisInfo.add(axModel.componentIndex);
                     }
@@ -408,6 +413,10 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
         });
     }
 
+    noTarget(): boolean {
+        return this._noTarget;
+    }
+
     getFirstTargetAxisModel(): AxisBaseModel {
         let firstAxisModel: AxisBaseModel;
         this.eachTargetAxis(function (axisDim, axisIndex) {
diff --git a/src/component/dataZoom/DataZoomView.ts b/src/component/dataZoom/DataZoomView.ts
index 8f9b285..2a96a35 100644
--- a/src/component/dataZoom/DataZoomView.ts
+++ b/src/component/dataZoom/DataZoomView.ts
@@ -21,16 +21,7 @@ import ComponentView from '../../view/Component';
 import DataZoomModel from './DataZoomModel';
 import GlobalModel from '../../model/Global';
 import ExtensionAPI from '../../ExtensionAPI';
-import { AxisBaseModel } from '../../coord/AxisBaseModel';
-import { Dictionary } from '../../util/types';
-import { CoordinateSystemHostModel } from '../../coord/CoordinateSystem';
-import { getAxisMainType } from './helper';
 
-export interface CoordInfo {
-    model: CoordinateSystemHostModel
-    axisModels: AxisBaseModel[]
-    coordIndex: number
-}
 
 class DataZoomView extends ComponentView {
     static type = 'dataZoom';
@@ -46,67 +37,6 @@ class DataZoomView extends ComponentView {
         this.api = api;
     }
 
-    /**
-     * Find the first target coordinate system.
-     *
-     * @protected
-     * @return {Object} {
-     *                   grid: [
-     *                       {model: coord0, axisModels: [axis1, axis3], coordIndex: 1},
-     *                       {model: coord1, axisModels: [axis0, axis2], coordIndex: 0},
-     *                       ...
-     *                   ],  // cartesians must not be null/undefined.
-     *                   polar: [
-     *                       {model: coord0, axisModels: [axis4], coordIndex: 0},
-     *                       ...
-     *                   ],  // polars must not be null/undefined.
-     *                   singleAxis: [
-     *                       {model: coord0, axisModels: [], coordIndex: 0}
-     *                   ]
-     */
-    getTargetCoordInfo() {
-        const dataZoomModel = this.dataZoomModel;
-        const ecModel = this.ecModel;
-        const coordSysLists: Dictionary<CoordInfo[]> = {};
-
-        dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
-            const axisModel = ecModel.getComponent(getAxisMainType(axisDim), axisIndex) as AxisBaseModel;
-            if (axisModel) {
-                const coordModel = axisModel.getCoordSysModel();
-                coordModel && save(
-                    coordModel,
-                    axisModel,
-                    coordSysLists[coordModel.mainType] || (coordSysLists[coordModel.mainType] = []),
-                    coordModel.componentIndex
-                );
-            }
-        }, this);
-
-        function save(
-            coordModel: CoordinateSystemHostModel,
-            axisModel: AxisBaseModel,
-            store: CoordInfo[],
-            coordIndex: number
-        ) {
-            let item;
-            for (let i = 0; i < store.length; i++) {
-                if (store[i].model === coordModel) {
-                    item = store[i];
-                    break;
-                }
-            }
-            if (!item) {
-                store.push(item = {
-                    model: coordModel,
-                    axisModels: [],
-                    coordIndex: coordIndex
-                });
-            }
-            item.axisModels.push(axisModel);
-        }
-
-        return coordSysLists;
-    }
 }
 
 ComponentView.registerClass(DataZoomView);
diff --git a/src/component/dataZoom/InsideZoomView.ts b/src/component/dataZoom/InsideZoomView.ts
index 41f99e7..878fef2 100644
--- a/src/component/dataZoom/InsideZoomView.ts
+++ b/src/component/dataZoom/InsideZoomView.ts
@@ -17,22 +17,21 @@
 * under the License.
 */
 
-import DataZoomView, {CoordInfo} from './DataZoomView';
+import DataZoomView from './DataZoomView';
 import sliderMove from '../helper/sliderMove';
 import * as roams from './roams';
 import InsideZoomModel from './InsideZoomModel';
 import GlobalModel from '../../model/Global';
 import ExtensionAPI from '../../ExtensionAPI';
 import ComponentView from '../../view/Component';
-import { each, map, bind } from 'zrender/src/core/util';
+import { bind } from 'zrender/src/core/util';
 import RoamController, {RoamEventParams} from '../helper/RoamController';
 import { AxisBaseModel } from '../../coord/AxisBaseModel';
 import Polar from '../../coord/polar/Polar';
 import SingleAxis from '../../coord/single/SingleAxis';
+import { DataZoomCoordSysMainType, DataZoomReferCoordSysInfo } from './helper';
 
 
-type SupportedCoordSysName = 'polar' | 'grid' | 'singleAxis';
-
 class InsideZoomView extends DataZoomView {
     static type = 'dataZoom.inside';
     type = 'dataZoom.inside';
@@ -43,86 +42,71 @@ class InsideZoomView extends DataZoomView {
      */
     range: number[];
 
-    /**
-     * @override
-     */
-    render(dataZoomModel: InsideZoomModel, ecModel: GlobalModel, api: ExtensionAPI, payload: any) {
+    render(dataZoomModel: InsideZoomModel, ecModel: GlobalModel, api: ExtensionAPI) {
         super.render.apply(this, arguments as any);
 
+        if (dataZoomModel.noTarget()) {
+            this._clear();
+            return;
+        }
+
         // Hence the `throttle` util ensures to preserve command order,
         // here simply updating range all the time will not cause missing
         // any of the the roam change.
         this.range = dataZoomModel.getPercentRange();
 
         // Reset controllers.
-        each(this.getTargetCoordInfo(), function (coordInfoList, coordSysName: SupportedCoordSysName) {
-
-            const allCoordIds = map(coordInfoList, function (coordInfo) {
-                return roams.generateCoordId(coordInfo.model);
-            });
-
-            each(coordInfoList, function (coordInfo) {
-                const coordModel = coordInfo.model;
-
-                roams.register(
-                    api,
-                    {
-                        coordId: roams.generateCoordId(coordModel),
-                        allCoordIds: allCoordIds,
-                        containsPoint(e, x, y) {
-                            return coordModel.coordinateSystem.containPoint([x, y]);
-                        },
-                        dataZoomId: dataZoomModel.id,
-                        dataZoomModel: dataZoomModel,
-                        getRange: {
-                            pan: bind(roamHandlers.pan, this, coordInfo, coordSysName),
-                            zoom: bind(roamHandlers.zoom, this, coordInfo, coordSysName),
-                            scrollMove: bind(roamHandlers.scrollMove, this, coordInfo, coordSysName)
-                        }
-                    }
-                );
-            }, this);
-
-        }, this);
+        roams.setViewInfoToCoordSysRecord(
+            api,
+            dataZoomModel,
+            {
+                pan: bind(getRangeHandlers.pan, this),
+                zoom: bind(getRangeHandlers.zoom, this),
+                scrollMove: bind(getRangeHandlers.scrollMove, this)
+            }
+        );
     }
 
-    /**
-     * @override
-     */
     dispose() {
-        roams.unregister(this.api, this.dataZoomModel.id);
+        this._clear();
         super.dispose.apply(this, arguments as any);
+    }
+
+    private _clear() {
+        roams.disposeCoordSysRecordIfNeeded(this.api, this.dataZoomModel as InsideZoomModel);
         this.range = null;
     }
 }
 
-interface RoamHandler<T extends RoamEventParams['zoom'] | RoamEventParams['scrollMove'] | RoamEventParams['pan']> {
+interface DataZoomGetRangeHandler<
+    T extends RoamEventParams['zoom'] | RoamEventParams['scrollMove'] | RoamEventParams['pan']
+> {
     (
-        coordInfo: CoordInfo,
-        coordSysName: SupportedCoordSysName,
+        coordSysInfo: DataZoomReferCoordSysInfo,
+        coordSysMainType: DataZoomCoordSysMainType,
         controller: RoamController,
         e: T
     ): [number, number]
 }
 
-const roamHandlers: {
-    pan: RoamHandler<RoamEventParams['pan']>
-    zoom: RoamHandler<RoamEventParams['zoom']>
-    scrollMove: RoamHandler<RoamEventParams['scrollMove']>
+const getRangeHandlers: {
+    pan: DataZoomGetRangeHandler<RoamEventParams['pan']>
+    zoom: DataZoomGetRangeHandler<RoamEventParams['zoom']>
+    scrollMove: DataZoomGetRangeHandler<RoamEventParams['scrollMove']>
 } & ThisType<InsideZoomView> = {
 
-    zoom(coordInfo, coordSysName, controller, e: RoamEventParams['zoom']) {
+    zoom(coordSysInfo, coordSysMainType, controller, e: RoamEventParams['zoom']) {
         const lastRange = this.range;
         const range = lastRange.slice() as [number, number];
 
         // Calculate transform by the first axis.
-        const axisModel = coordInfo.axisModels[0];
+        const axisModel = coordSysInfo.axisModels[0];
         if (!axisModel) {
             return;
         }
 
-        const directionInfo = getDirectionInfo[coordSysName](
-            null, [e.originX, e.originY], axisModel, controller, coordInfo
+        const directionInfo = getDirectionInfo[coordSysMainType](
+            null, [e.originX, e.originY], axisModel, controller, coordSysInfo
         );
         const percentPoint = (
             directionInfo.signal > 0
@@ -146,9 +130,9 @@ const roamHandlers: {
         }
     },
 
-    pan: makeMover(function (range, axisModel, coordInfo, coordSysName, controller, e: RoamEventParams['pan']) {
-        const directionInfo = getDirectionInfo[coordSysName](
-            [e.oldX, e.oldY], [e.newX, e.newY], axisModel, controller, coordInfo
+    pan: makeMover(function (range, axisModel, coordSysInfo, coordSysMainType, controller, e: RoamEventParams['pan']) {
+        const directionInfo = getDirectionInfo[coordSysMainType](
+            [e.oldX, e.oldY], [e.newX, e.newY], axisModel, controller, coordSysInfo
         );
 
         return directionInfo.signal
@@ -157,29 +141,31 @@ const roamHandlers: {
     }),
 
     scrollMove: makeMover(
-        function (range, axisModel, coordInfo, coordSysName, controller, e: RoamEventParams['scrollMove']
+        function (range, axisModel, coordSysInfo, coordSysMainType, controller, e: RoamEventParams['scrollMove']
     ) {
-        const directionInfo = getDirectionInfo[coordSysName](
-            [0, 0], [e.scrollDelta, e.scrollDelta], axisModel, controller, coordInfo
+        const directionInfo = getDirectionInfo[coordSysMainType](
+            [0, 0], [e.scrollDelta, e.scrollDelta], axisModel, controller, coordSysInfo
         );
         return directionInfo.signal * (range[1] - range[0]) * e.scrollDelta;
     })
 };
 
+export type DataZoomGetRangeHandlers = typeof getRangeHandlers;
+
 function makeMover(
     getPercentDelta: (
         range: [number, number],
         axisModel: AxisBaseModel,
-        coordInfo: CoordInfo,
-        coordSysName: SupportedCoordSysName,
+        coordSysInfo: DataZoomReferCoordSysInfo,
+        coordSysMainType: DataZoomCoordSysMainType,
         controller: RoamController,
         e: RoamEventParams['scrollMove']| RoamEventParams['pan']
     ) => number
 ) {
     return function (
         this: InsideZoomView,
-        coordInfo: CoordInfo,
-        coordSysName: SupportedCoordSysName,
+        coordSysInfo: DataZoomReferCoordSysInfo,
+        coordSysMainType: DataZoomCoordSysMainType,
         controller: RoamController,
         e: RoamEventParams['scrollMove']| RoamEventParams['pan']
     ): [number, number] {
@@ -187,13 +173,13 @@ function makeMover(
         const range = lastRange.slice() as [number, number];
 
         // Calculate transform by the first axis.
-        const axisModel = coordInfo.axisModels[0];
+        const axisModel = coordSysInfo.axisModels[0];
         if (!axisModel) {
             return;
         }
 
         const percentDelta = getPercentDelta(
-            range, axisModel, coordInfo, coordSysName, controller, e
+            range, axisModel, coordSysInfo, coordSysMainType, controller, e
         );
 
         sliderMove(percentDelta, range, [0, 100], 'all');
@@ -218,16 +204,16 @@ interface GetDirectionInfo {
         newPoint: number[],
         axisModel: AxisBaseModel,
         controller: RoamController,
-        coordInfo: CoordInfo
+        coordSysInfo: DataZoomReferCoordSysInfo
     ): DirectionInfo
 }
 
 const getDirectionInfo: Record<'grid' | 'polar' | 'singleAxis', GetDirectionInfo> = {
 
-    grid(oldPoint, newPoint, axisModel, controller, coordInfo) {
+    grid(oldPoint, newPoint, axisModel, controller, coordSysInfo) {
         const axis = axisModel.axis;
         const ret = {} as DirectionInfo;
-        const rect = coordInfo.model.coordinateSystem.getRect();
+        const rect = coordSysInfo.model.coordinateSystem.getRect();
         oldPoint = oldPoint || [0, 0];
 
         if (axis.dim === 'x') {
@@ -246,10 +232,10 @@ const getDirectionInfo: Record<'grid' | 'polar' | 'singleAxis', GetDirectionInfo
         return ret;
     },
 
-    polar(oldPoint, newPoint, axisModel, controller, coordInfo) {
+    polar(oldPoint, newPoint, axisModel, controller, coordSysInfo) {
         const axis = axisModel.axis;
         const ret = {} as DirectionInfo;
-        const polar = coordInfo.model.coordinateSystem as Polar;
+        const polar = coordSysInfo.model.coordinateSystem as Polar;
         const radiusExtent = polar.getRadiusAxis().getExtent();
         const angleExtent = polar.getAngleAxis().getExtent();
 
@@ -276,9 +262,9 @@ const getDirectionInfo: Record<'grid' | 'polar' | 'singleAxis', GetDirectionInfo
         return ret;
     },
 
-    singleAxis(oldPoint, newPoint, axisModel, controller, coordInfo) {
+    singleAxis(oldPoint, newPoint, axisModel, controller, coordSysInfo) {
         const axis = axisModel.axis as SingleAxis;
-        const rect = coordInfo.model.coordinateSystem.getRect();
+        const rect = coordSysInfo.model.coordinateSystem.getRect();
         const ret = {} as DirectionInfo;
 
         oldPoint = oldPoint || [0, 0];
diff --git a/src/component/dataZoom/SliderZoomView.ts b/src/component/dataZoom/SliderZoomView.ts
index 1b47172..addd2c5 100644
--- a/src/component/dataZoom/SliderZoomView.ts
+++ b/src/component/dataZoom/SliderZoomView.ts
@@ -36,7 +36,7 @@ import { RectLike } from 'zrender/src/core/BoundingRect';
 import Axis from '../../coord/Axis';
 import SeriesModel from '../../model/Series';
 import { AxisBaseModel } from '../../coord/AxisBaseModel';
-import { getAxisMainType } from './helper';
+import { getAxisMainType, collectReferCoordSysModelInfo } from './helper';
 
 const Rect = graphic.Rect;
 
@@ -99,10 +99,6 @@ class SliderZoomView extends DataZoomView {
         this.api = api;
     }
 
-
-    /**
-     * @override
-     */
     render(
         dataZoomModel: SliderZoomModel,
         ecModel: GlobalModel,
@@ -117,13 +113,19 @@ class SliderZoomView extends DataZoomView {
         throttle.createOrUpdate(
             this,
             '_dispatchZoomAction',
-            this.dataZoomModel.get('throttle'),
+            dataZoomModel.get('throttle'),
             'fixRate'
         );
 
         this._orient = dataZoomModel.getOrient();
 
-        if (this.dataZoomModel.get('show') === false) {
+        if (dataZoomModel.get('show') === false) {
+            this.group.removeAll();
+            return;
+        }
+
+        if (dataZoomModel.noTarget()) {
+            this._clear();
             this.group.removeAll();
             return;
         }
@@ -138,22 +140,16 @@ class SliderZoomView extends DataZoomView {
         this._updateView();
     }
 
-    /**
-     * @override
-     */
-    remove() {
-        throttle.clear(this, '_dispatchZoomAction');
-    }
-
-    /**
-     * @override
-     */
     dispose() {
+        this._clear();
         super.dispose.apply(this, arguments as any);
+    }
+
+    private _clear() {
         throttle.clear(this, '_dispatchZoomAction');
     }
 
-    _buildView() {
+    private _buildView() {
         const thisGroup = this.group;
 
         thisGroup.removeAll();
@@ -174,10 +170,7 @@ class SliderZoomView extends DataZoomView {
         this._positionGroup();
     }
 
-    /**
-     * @private
-     */
-    _resetLocation() {
+    private _resetLocation() {
         const dataZoomModel = this.dataZoomModel;
         const api = this.api;
 
@@ -223,10 +216,7 @@ class SliderZoomView extends DataZoomView {
         this._orient === VERTICAL && this._size.reverse();
     }
 
-    /**
-     * @private
-     */
-    _positionGroup() {
+    private _positionGroup() {
         const thisGroup = this.group;
         const location = this._location;
         const orient = this._orient;
@@ -257,14 +247,11 @@ class SliderZoomView extends DataZoomView {
         thisGroup.markRedraw();
     }
 
-    /**
-     * @private
-     */
-    _getViewExtent() {
+    private _getViewExtent() {
         return [0, this._size[0]];
     }
 
-    _renderBackground() {
+    private _renderBackground() {
         const dataZoomModel = this.dataZoomModel;
         const size = this._size;
         const barGroup = this._displayables.barGroup;
@@ -293,7 +280,7 @@ class SliderZoomView extends DataZoomView {
         }));
     }
 
-    _renderDataShadow() {
+    private _renderDataShadow() {
         const info = this._dataShadowInfo = this._prepareDataShadowInfo();
 
         if (!info) {
@@ -383,7 +370,7 @@ class SliderZoomView extends DataZoomView {
         }));
     }
 
-    _prepareDataShadowInfo() {
+    private _prepareDataShadowInfo() {
         const dataZoomModel = this.dataZoomModel;
         const showDataShadow = dataZoomModel.get('showDataShadow');
 
@@ -440,7 +427,7 @@ class SliderZoomView extends DataZoomView {
         return result;
     }
 
-    _renderHandle() {
+    private _renderHandle() {
         const displaybles = this._displayables;
         const handles: [graphic.Path, graphic.Path] = displaybles.handles = [null, null];
         const handleLabels: [graphic.Text, graphic.Text] = displaybles.handleLabels = [null, null];
@@ -594,10 +581,7 @@ class SliderZoomView extends DataZoomView {
         this._updateDataInfo(nonRealtime);
     }
 
-    /**
-     * @private
-     */
-    _updateDataInfo(nonRealtime?: boolean) {
+    private _updateDataInfo(nonRealtime?: boolean) {
         const dataZoomModel = this.dataZoomModel;
         const displaybles = this._displayables;
         const handleLabels = displaybles.handleLabels;
@@ -660,7 +644,7 @@ class SliderZoomView extends DataZoomView {
         }
     }
 
-    _formatLabel(value: ParsedValue, axis: Axis) {
+    private _formatLabel(value: ParsedValue, axis: Axis) {
         const dataZoomModel = this.dataZoomModel;
         const labelFormatter = dataZoomModel.get('labelFormatter');
 
@@ -685,10 +669,9 @@ class SliderZoomView extends DataZoomView {
     }
 
     /**
-     * @private
      * @param showOrHide true: show, false: hide
      */
-    _showDataInfo(showOrHide?: boolean) {
+    private _showDataInfo(showOrHide?: boolean) {
         // Always show when drgging.
         showOrHide = this._dragging || showOrHide;
 
@@ -697,7 +680,7 @@ class SliderZoomView extends DataZoomView {
         handleLabels[1].attr('invisible', !showOrHide);
     }
 
-    _onDragMove(handleIndex: 0 | 1 | 'all', dx: number, dy: number, event: ZRElementEvent) {
+    private _onDragMove(handleIndex: 0 | 1 | 'all', dx: number, dy: number, event: ZRElementEvent) {
         this._dragging = true;
 
         // For mobile device, prevent screen slider on the button.
@@ -718,7 +701,7 @@ class SliderZoomView extends DataZoomView {
         changed && realtime && this._dispatchZoomAction();
     }
 
-    _onDragEnd() {
+    private _onDragEnd() {
         this._dragging = false;
         this._showDataInfo(false);
 
@@ -728,7 +711,7 @@ class SliderZoomView extends DataZoomView {
         !realtime && this._dispatchZoomAction();
     }
 
-    _onClickPanelClick(e: ZRElementEvent) {
+    private _onClickPanelClick(e: ZRElementEvent) {
         const size = this._size;
         const localPoint = this._displayables.barGroup.transformCoordToLocal(e.offsetX, e.offsetY);
 
@@ -747,8 +730,8 @@ class SliderZoomView extends DataZoomView {
     }
 
     /**
-     * This action will be throttled.
      * @private
+     * This action will be throttled.
      */
     _dispatchZoomAction() {
         const range = this._range;
@@ -762,18 +745,16 @@ class SliderZoomView extends DataZoomView {
         });
     }
 
-    /**
-     * @private
-     */
-    _findCoordRect() {
+    private _findCoordRect() {
         // Find the grid coresponding to the first axis referred by dataZoom.
         let rect: RectLike;
-        each(this.getTargetCoordInfo(), function (coordInfoList) {
-            if (!rect && coordInfoList.length) {
-                const coordSys = coordInfoList[0].model.coordinateSystem;
-                rect = coordSys.getRect && coordSys.getRect();
-            }
-        });
+        const coordSysInfoList = collectReferCoordSysModelInfo(this.dataZoomModel).infoList;
+
+        if (!rect && coordSysInfoList.length) {
+            const coordSys = coordSysInfoList[0].model.coordinateSystem;
+            rect = coordSys.getRect && coordSys.getRect();
+        }
+
         if (!rect) {
             const width = this.api.getWidth();
             const height = this.api.getHeight();
diff --git a/src/component/dataZoom/helper.ts b/src/component/dataZoom/helper.ts
index 7def11e..ef739b1 100644
--- a/src/component/dataZoom/helper.ts
+++ b/src/component/dataZoom/helper.ts
@@ -17,19 +17,31 @@
 * under the License.
 */
 
-import { Payload, DimensionName } from '../../util/types';
+import { Payload } from '../../util/types';
 import GlobalModel from '../../model/Global';
 import DataZoomModel from './DataZoomModel';
-import { indexOf, createHashMap, assert } from 'zrender/src/core/util';
+import { indexOf, createHashMap, assert, HashMap } from 'zrender/src/core/util';
 import { __DEV__ } from '../../config';
+import SeriesModel from '../../model/Series';
+import { CoordinateSystemHostModel } from '../../coord/CoordinateSystem';
+import { AxisBaseModel } from '../../coord/AxisBaseModel';
 
 
 export interface DataZoomPayloadBatchItem {
-    dataZoomId: string
-    start?: number
-    end?: number
-    startValue?: number
-    endValue?: number
+    dataZoomId: string;
+    start?: number;
+    end?: number;
+    startValue?: number;
+    endValue?: number;
+}
+
+export interface DataZoomReferCoordSysInfo {
+    model: CoordinateSystemHostModel;
+    // Notice: if two dataZooms refer the same coordinamte system model,
+    // (1) The axis they refered may different
+    // (2) The sequence the axisModels matters, may different in
+    // different dataZooms.
+    axisModels: AxisBaseModel[];
 }
 
 export const DATA_ZOOM_AXIS_DIMENSIONS = [
@@ -43,13 +55,15 @@ type DataZoomAxisIndexPropName =
     'xAxisIndex' | 'yAxisIndex' | 'radiusAxisIndex' | 'angleAxisIndex' | 'singleAxisIndex';
 type DataZoomAxisIdPropName =
     'xAxisId' | 'yAxisId' | 'radiusAxisId' | 'angleAxisId' | 'singleAxisId';
+export type DataZoomCoordSysMainType = 'polar' | 'grid' | 'singleAxis';
 
 // Supported coords.
 // FIXME: polar has been broken (but rarely used).
-const COORDS = ['cartesian2d', 'polar', 'singleAxis'] as const;
+const SERIES_COORDS = ['cartesian2d', 'polar', 'singleAxis'] as const;
 
-export function isCoordSupported(coordType: string) {
-    return indexOf(COORDS, coordType) >= 0;
+export function isCoordSupported(seriesModel: SeriesModel): boolean {
+    const coordType = seriesModel.get('coordinateSystem');
+    return indexOf(SERIES_COORDS, coordType) >= 0;
 }
 
 export function getAxisMainType(axisDim: DataZoomAxisDimension): DataZoomAxisMainType {
@@ -138,3 +152,57 @@ export function findEffectedDataZooms(ecModel: GlobalModel, payload: Payload): D
 
     return effectedModels;
 }
+
+/**
+ * Find the first target coordinate system.
+ * Available after model built.
+ *
+ * @return Like {
+ *                  grid: [
+ *                      {model: coord0, axisModels: [axis1, axis3], coordIndex: 1},
+ *                      {model: coord1, axisModels: [axis0, axis2], coordIndex: 0},
+ *                      ...
+ *                  ],  // cartesians must not be null/undefined.
+ *                  polar: [
+ *                      {model: coord0, axisModels: [axis4], coordIndex: 0},
+ *                      ...
+ *                  ],  // polars must not be null/undefined.
+ *                  singleAxis: [
+ *                      {model: coord0, axisModels: [], coordIndex: 0}
+ *                  ]
+ *              }
+ */
+export function collectReferCoordSysModelInfo(dataZoomModel: DataZoomModel): {
+    infoList: DataZoomReferCoordSysInfo[];
+    // Key: coordSysModel.uid
+    infoMap: HashMap<DataZoomReferCoordSysInfo, string>;
+} {
+    const ecModel = dataZoomModel.ecModel;
+    const coordSysInfoWrap = {
+        infoList: [] as DataZoomReferCoordSysInfo[],
+        infoMap: createHashMap<DataZoomReferCoordSysInfo, string>()
+    };
+
+    dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
+        const axisModel = ecModel.getComponent(getAxisMainType(axisDim), axisIndex) as AxisBaseModel;
+        if (!axisModel) {
+            return;
+        }
+        const coordSysModel = axisModel.getCoordSysModel();
+        if (!coordSysModel) {
+            return;
+        }
+
+        const coordSysUid = coordSysModel.uid;
+        let coordSysInfo = coordSysInfoWrap.infoMap.get(coordSysUid);
+        if (!coordSysInfo) {
+            coordSysInfo = { model: coordSysModel, axisModels: [] };
+            coordSysInfoWrap.infoList.push(coordSysInfo);
+            coordSysInfoWrap.infoMap.set(coordSysUid, coordSysInfo);
+        }
+
+        coordSysInfo.axisModels.push(axisModel);
+    });
+
+    return coordSysInfoWrap;
+}
diff --git a/src/component/dataZoom/roams.ts b/src/component/dataZoom/roams.ts
index 05524c9..7686d56 100644
--- a/src/component/dataZoom/roams.ts
+++ b/src/component/dataZoom/roams.ts
@@ -23,156 +23,205 @@
 // pan or zoom, only dispatch one action for those data zoom
 // components.
 
-import RoamController, { RoamType, RoamEventParams } from '../../component/helper/RoamController';
+import * as echarts from '../../echarts';
+import RoamController, { RoamType } from '../../component/helper/RoamController';
 import * as throttleUtil from '../../util/throttle';
 import { makeInner } from '../../util/model';
 import { Dictionary, ZRElementEvent } from '../../util/types';
 import ExtensionAPI from '../../ExtensionAPI';
 import InsideZoomModel from './InsideZoomModel';
-import { each, indexOf, curry, Curry1 } from 'zrender/src/core/util';
-import ComponentModel from '../../model/Component';
-import { DataZoomPayloadBatchItem } from './helper';
+import { each, curry, Curry1, HashMap, createHashMap } from 'zrender/src/core/util';
+import {
+    DataZoomPayloadBatchItem, collectReferCoordSysModelInfo,
+    DataZoomCoordSysMainType, DataZoomReferCoordSysInfo
+} from './helper';
+import GlobalModel from '../../model/Global';
+import { CoordinateSystemHostModel } from '../../coord/CoordinateSystem';
+import { DataZoomGetRangeHandlers } from './InsideZoomView';
+
 
 interface DataZoomInfo {
-    coordId: string
-    containsPoint: (e: ZRElementEvent, x: number, y: number) => boolean
-    allCoordIds: string[]
-    dataZoomId: string
-    getRange: {
-        pan: (controller: RoamController, e: RoamEventParams['pan']) => [number, number]
-        zoom: (controller: RoamController, e: RoamEventParams['zoom']) => [number, number]
-        scrollMove: (controller: RoamController, e: RoamEventParams['scrollMove']) => [number, number]
-    }
-    dataZoomModel: InsideZoomModel
-}
-interface Record {
-    // key is dataZoomId
-    dataZoomInfos: Dictionary<DataZoomInfo>
-    count: number
-    coordId: string
-    controller: RoamController
-    dispatchAction: Curry1<typeof dispatchAction, ExtensionAPI>
+    getRange: DataZoomGetRangeHandlers;
+    model: InsideZoomModel;
+    dzReferCoordSysInfo: DataZoomReferCoordSysInfo
 }
 
-interface PayloadBatch {
-    dataZoomId: string
+interface CoordSysRecord {
+    // key: dataZoom.uid
+    dataZoomInfoMap: HashMap<DataZoomInfo, string>;
+    model: CoordinateSystemHostModel,
+    // count: number
+    // coordId: string
+    controller: RoamController;
+    containsPoint: (e: ZRElementEvent, x: number, y: number) => boolean;
+    dispatchAction: Curry1<typeof dispatchAction, ExtensionAPI>;
 }
 
-type Store = Dictionary<Record>;
 
-const inner = makeInner<Store, ExtensionAPI>();
+const inner = makeInner<{
+    // key: coordSysModel.uid
+    coordSysRecordMap: HashMap<CoordSysRecord, string>;
+}, ExtensionAPI>();
 
-export function register(api: ExtensionAPI, dataZoomInfo: DataZoomInfo) {
-    const store = inner(api);
-    const theDataZoomId = dataZoomInfo.dataZoomId;
-    const theCoordId = dataZoomInfo.coordId;
 
-    // Do clean when a dataZoom changes its target coordnate system.
-    // Avoid memory leak, dispose all not-used-registered.
-    each(store, function (record, coordId) {
-        const dataZoomInfos = record.dataZoomInfos;
-        if (dataZoomInfos[theDataZoomId]
-            && indexOf(dataZoomInfo.allCoordIds, theCoordId) < 0
-        ) {
-            delete dataZoomInfos[theDataZoomId];
-            record.count--;
-        }
-    });
+echarts.registerProcessor(echarts.PRIORITY.PROCESSOR.FILTER, function (ecModel: GlobalModel, api: ExtensionAPI): void {
+    const apiInner = inner(api);
+    const coordSysRecordMap = apiInner.coordSysRecordMap
+        || (apiInner.coordSysRecordMap = createHashMap<CoordSysRecord, string>());
 
-    cleanStore(store);
-
-    let record = store[theCoordId];
-    // Create if needed.
-    if (!record) {
-        record = store[theCoordId] = {
-            coordId: theCoordId,
-            dataZoomInfos: {},
-            count: 0,
-            controller: null,
-            dispatchAction: curry(dispatchAction, api)
-        };
-        record.controller = createController(api, record);
-    }
+    coordSysRecordMap.each(function (coordSysRecord) {
+        // `coordSysRecordMap` always exists (becuase it hold the `roam controller`, which should
+        // better not re-create each time), but clear `dataZoomInfoMap` each round of the workflow.
+        coordSysRecord.dataZoomInfoMap = null;
+    });
 
-    // Update reference of dataZoom.
-    !(record.dataZoomInfos[theDataZoomId]) && record.count++;
-    record.dataZoomInfos[theDataZoomId] = dataZoomInfo;
+    ecModel.eachComponent(
+        { mainType: 'dataZoom', subType: 'inside' },
+        function (dataZoomModel: InsideZoomModel) {
+            const dzReferCoordSysWrap = collectReferCoordSysModelInfo(dataZoomModel);
 
-    const controllerParams = mergeControllerParams(record.dataZoomInfos);
-    record.controller.enable(controllerParams.controlType, controllerParams.opt);
+            each(dzReferCoordSysWrap.infoList, function (dzCoordSysInfo) {
 
-    // Consider resize, area should be always updated.
-    record.controller.setPointerChecker(dataZoomInfo.containsPoint);
+                const coordSysUid = dzCoordSysInfo.model.uid;
+                const coordSysRecord = coordSysRecordMap.get(coordSysUid)
+                    || coordSysRecordMap.set(coordSysUid, createCoordSysRecord(api, dzCoordSysInfo.model));
 
-    // Update throttle.
-    throttleUtil.createOrUpdate(
-        record,
-        'dispatchAction',
-        dataZoomInfo.dataZoomModel.get('throttle', true),
-        'fixRate'
+                const dataZoomInfoMap = coordSysRecord.dataZoomInfoMap
+                    || (coordSysRecord.dataZoomInfoMap = createHashMap<DataZoomInfo, string>());
+                // Notice these props might be changed each time for a single dataZoomModel.
+                dataZoomInfoMap.set(dataZoomModel.uid, {
+                    dzReferCoordSysInfo: dzCoordSysInfo,
+                    model: dataZoomModel,
+                    getRange: null
+                });
+            });
+        }
     );
-}
 
-export function unregister(api: ExtensionAPI, dataZoomId: string) {
-    const store = inner(api);
+    // (1) Merge dataZoom settings for each coord sys and set to the roam controller.
+    // (2) Clear coord sys if not refered by any dataZoom.
+    coordSysRecordMap.each(function (coordSysRecord) {
+        const controller = coordSysRecord.controller;
+        let firstDzInfo: DataZoomInfo;
+        const dataZoomInfoMap = coordSysRecord.dataZoomInfoMap;
+
+        if (dataZoomInfoMap) {
+            const firstDzKey = dataZoomInfoMap.keys()[0];
+            if (firstDzKey != null) {
+                firstDzInfo = dataZoomInfoMap.get(firstDzKey);
+            }
+        }
+
+        if (!firstDzInfo) {
+            disposeCoordSysRecord(coordSysRecordMap, coordSysRecord);
+            return;
+        }
+
+        const controllerParams = mergeControllerParams(dataZoomInfoMap);
+        controller.enable(controllerParams.controlType, controllerParams.opt);
+
+        controller.setPointerChecker(coordSysRecord.containsPoint);
+
+        throttleUtil.createOrUpdate(
+            coordSysRecord,
+            'dispatchAction',
+            firstDzInfo.model.get('throttle', true),
+            'fixRate'
+        );
+    });
+
+});
+
 
-    each(store, function (record) {
-        record.controller.dispose();
-        const dataZoomInfos = record.dataZoomInfos;
-        if (dataZoomInfos[dataZoomId]) {
-            delete dataZoomInfos[dataZoomId];
-            record.count--;
+export function setViewInfoToCoordSysRecord(
+    api: ExtensionAPI,
+    dataZoomModel: InsideZoomModel,
+    getRange: DataZoomGetRangeHandlers
+): void {
+    inner(api).coordSysRecordMap.each(function (coordSysRecord) {
+        const dzInfo = coordSysRecord.dataZoomInfoMap.get(dataZoomModel.uid);
+        if (dzInfo) {
+            dzInfo.getRange = getRange;
         }
     });
+}
 
-    cleanStore(store);
+export function disposeCoordSysRecordIfNeeded(api: ExtensionAPI, dataZoomModel: InsideZoomModel) {
+    const coordSysRecordMap = inner(api).coordSysRecordMap;
+    const coordSysKeyArr = coordSysRecordMap.keys();
+    for (let i = 0; i < coordSysKeyArr.length; i++) {
+        const coordSysKey = coordSysKeyArr[i];
+        const coordSysRecord = coordSysRecordMap.get(coordSysKey);
+        const dataZoomInfoMap = coordSysRecord.dataZoomInfoMap;
+        if (dataZoomInfoMap) {
+            const dzUid = dataZoomModel.uid;
+            const dzInfo = dataZoomInfoMap.get(dzUid);
+            if (dzInfo) {
+                dataZoomInfoMap.removeKey(dzUid);
+                if (!dataZoomInfoMap.keys().length) {
+                    disposeCoordSysRecord(coordSysRecordMap, coordSysRecord);
+                }
+            }
+        }
+    }
 }
 
-/**
- * @public
- */
-export function generateCoordId(coordModel: ComponentModel) {
-    return coordModel.type + '\0_' + coordModel.id;
+function disposeCoordSysRecord(
+    coordSysRecordMap: HashMap<CoordSysRecord, string>,
+    coordSysRecord: CoordSysRecord
+): void {
+    if (coordSysRecord) {
+        coordSysRecordMap.removeKey(coordSysRecord.model.uid);
+        const controller = coordSysRecord.controller;
+        controller && controller.dispose();
+    }
 }
 
-function createController(api: ExtensionAPI, newRecord: Record) {
-    const controller = new RoamController(api.getZr());
+function createCoordSysRecord(api: ExtensionAPI, coordSysModel: CoordinateSystemHostModel): CoordSysRecord {
+    // These init props will never change after record created.
+    const coordSysRecord: CoordSysRecord = {
+        model: coordSysModel,
+        containsPoint: curry(containsPoint, coordSysModel),
+        dispatchAction: curry(dispatchAction, api),
+        dataZoomInfoMap: null,
+        controller: null
+    };
+
+    // Must not do anything depends on coordSysRecord outside the event handler here,
+    // because coordSysRecord not completed yet.
+    const controller = coordSysRecord.controller = new RoamController(api.getZr());
 
     each(['pan', 'zoom', 'scrollMove'] as const, function (eventName) {
         controller.on(eventName, function (event) {
             const batch: DataZoomPayloadBatchItem[] = [];
 
-            each(newRecord.dataZoomInfos, function (info) {
+            coordSysRecord.dataZoomInfoMap.each(function (dzInfo) {
                 // Check whether the behaviors (zoomOnMouseWheel, moveOnMouseMove,
                 // moveOnMouseWheel, ...) enabled.
-                if (!event.isAvailableBehavior(info.dataZoomModel.option)) {
+                if (!event.isAvailableBehavior(dzInfo.model.option)) {
                     return;
                 }
 
-                const method = (info.getRange || {} as DataZoomInfo['getRange'])[eventName];
-                const range = method && method(newRecord.controller, event as any);
+                const method = (dzInfo.getRange || {} as DataZoomGetRangeHandlers)[eventName];
+                const range = method && method(
+                    dzInfo.dzReferCoordSysInfo,
+                    coordSysRecord.model.mainType as DataZoomCoordSysMainType,
+                    coordSysRecord.controller,
+                    event as any
+                );
 
-                !(info.dataZoomModel as InsideZoomModel).get('disabled', true) && range && batch.push({
-                    dataZoomId: info.dataZoomId,
+                !dzInfo.model.get('disabled', true) && range && batch.push({
+                    dataZoomId: dzInfo.model.id,
                     start: range[0],
                     end: range[1]
                 });
             });
 
-            batch.length && newRecord.dispatchAction(batch);
+            batch.length && coordSysRecord.dispatchAction(batch);
         });
     });
 
-    return controller;
-}
-
-function cleanStore(store: Store) {
-    each(store, function (record, coordId) {
-        if (!record.count) {
-            record.controller.dispose();
-            delete store[coordId];
-        }
-    });
+    return coordSysRecord;
 }
 
 /**
@@ -185,10 +234,16 @@ function dispatchAction(api: ExtensionAPI, batch: DataZoomPayloadBatchItem[]) {
     });
 }
 
+function containsPoint(
+    coordSysModel: CoordinateSystemHostModel, e: ZRElementEvent, x: number, y: number
+): boolean {
+    return coordSysModel.coordinateSystem.containPoint([x, y]);
+}
+
 /**
  * Merge roamController settings when multiple dataZooms share one roamController.
  */
-function mergeControllerParams(dataZoomInfos: Dictionary<DataZoomInfo>) {
+function mergeControllerParams(dataZoomInfoMap: HashMap<{ model: InsideZoomModel }>) {
     let controlType: RoamType;
     // DO NOT use reserved word (true, false, undefined) as key literally. Even if encapsulated
     // as string, it is probably revert to reserved word by compress tool. See #7411.
@@ -201,8 +256,8 @@ function mergeControllerParams(dataZoomInfos: Dictionary<DataZoomInfo>) {
     };
     let preventDefaultMouseMove = true;
 
-    each(dataZoomInfos, function (dataZoomInfo) {
-        const dataZoomModel = dataZoomInfo.dataZoomModel as InsideZoomModel;
+    dataZoomInfoMap.each(function (dataZoomInfo) {
+        const dataZoomModel = dataZoomInfo.model;
         const oneType = dataZoomModel.get('disabled', true)
             ? false
             : dataZoomModel.get('zoomLock', true)
diff --git a/src/component/toolbox/feature/DataZoom.ts b/src/component/toolbox/feature/DataZoom.ts
index 1e11ed2..a69b92f 100644
--- a/src/component/toolbox/feature/DataZoom.ts
+++ b/src/component/toolbox/feature/DataZoom.ts
@@ -40,16 +40,14 @@ import Cartesian2D from '../../../coord/cartesian/Cartesian2D';
 import CartesianAxisModel from '../../../coord/cartesian/AxisModel';
 import DataZoomModel from '../../dataZoom/DataZoomModel';
 import {
-    DataZoomPayloadBatchItem, DataZoomAxisDimension, getAxisIndexPropName,
-    getAxisIdPropName, getAxisMainType
+    DataZoomPayloadBatchItem, DataZoomAxisDimension
 } from '../../dataZoom/helper';
 import {
     ModelFinderObject, ModelFinderIndexQuery, makeInternalComponentId,
-    queryReferringComponents, ModelFinderIdQuery, parseFinder
+    ModelFinderIdQuery, parseFinder
 } from '../../../util/model';
 import ToolboxModel from '../ToolboxModel';
 import { registerInternalOptionCreator } from '../../../model/internalComponentCreator';
-import Model from '../../../model/Model';
 import ComponentModel from '../../../model/Component';
 
 
diff --git a/src/component/toolbox/feature/MagicType.ts b/src/component/toolbox/feature/MagicType.ts
index 2dfd038..b5b3e0f 100644
--- a/src/component/toolbox/feature/MagicType.ts
+++ b/src/component/toolbox/feature/MagicType.ts
@@ -25,6 +25,7 @@ import { SeriesOption, ECUnitOption } from '../../../util/types';
 import GlobalModel from '../../../model/Global';
 import ExtensionAPI from '../../../ExtensionAPI';
 import SeriesModel from '../../../model/Series';
+import { SINGLE_REFERRING } from '../../../util/model';
 
 const magicTypeLang = lang.toolbox.magicType;
 const INNER_STACK_KEYWORD = '__ec_magicType_stack__' as const;
@@ -117,11 +118,7 @@ class MagicType extends ToolboxFeature<ToolboxMagicTypeFeatureOption> {
                 if (categoryAxis) {
                     const axisDim = categoryAxis.dim;
                     const axisType = axisDim + 'Axis';
-                    const axisModel = ecModel.queryComponents({
-                        mainType: axisType,
-                        index: seriesModel.get(name + 'Index' as any),
-                        id: seriesModel.get(name + 'Id' as any)
-                    })[0];
+                    const axisModel = seriesModel.getReferringComponents(axisType, SINGLE_REFERRING).models[0];
                     const axisIndex = axisModel.componentIndex;
 
                     newOption[axisType] = newOption[axisType] || [];
diff --git a/src/coord/axisModelCommonMixin.ts b/src/coord/axisModelCommonMixin.ts
index c1c458f..4550618 100644
--- a/src/coord/axisModelCommonMixin.ts
+++ b/src/coord/axisModelCommonMixin.ts
@@ -17,7 +17,6 @@
 * under the License.
 */
 
-import * as zrUtil from 'zrender/src/core/util';
 import Model from '../model/Model';
 import Axis from './Axis';
 import { AxisBaseOption } from './axisCommonTypes';
diff --git a/src/coord/cartesian/AxisModel.ts b/src/coord/cartesian/AxisModel.ts
index 1d4a6d0..9a21401 100644
--- a/src/coord/cartesian/AxisModel.ts
+++ b/src/coord/cartesian/AxisModel.ts
@@ -26,6 +26,7 @@ import { AxisBaseOption } from '../axisCommonTypes';
 import GridModel from './GridModel';
 import { AxisBaseModel } from '../AxisBaseModel';
 import {OrdinalSortInfo} from '../../util/types';
+import { SINGLE_REFERRING } from '../../util/model';
 
 
 export type CartesianAxisPosition = 'top' | 'bottom' | 'left' | 'right';
@@ -50,11 +51,7 @@ class CartesianAxisModel extends ComponentModel<CartesianAxisOption>
     axis: Axis2D;
 
     getCoordSysModel(): GridModel {
-        return this.ecModel.queryComponents({
-            mainType: 'grid',
-            index: this.option.gridIndex,
-            id: this.option.gridId
-        })[0] as GridModel;
+        return this.getReferringComponents('grid', SINGLE_REFERRING).models[0] as GridModel;
     }
 }
 
diff --git a/src/coord/cartesian/Grid.ts b/src/coord/cartesian/Grid.ts
index 91e540b..ceb2787 100644
--- a/src/coord/cartesian/Grid.ts
+++ b/src/coord/cartesian/Grid.ts
@@ -36,7 +36,7 @@ import {
 import Cartesian2D, {cartesian2DDimensions} from './Cartesian2D';
 import Axis2D from './Axis2D';
 import CoordinateSystemManager from '../../CoordinateSystem';
-import {ParsedModelFinder} from '../../util/model';
+import {ParsedModelFinder, SINGLE_REFERRING} from '../../util/model';
 
 // Depends on GridModel, AxisModel, which performs preprocess.
 import GridModel from './GridModel';
@@ -254,9 +254,9 @@ class Grid implements CoordinateSystemMaster {
     } {
         const seriesModel = finder.seriesModel;
         const xAxisModel = finder.xAxisModel
-            || (seriesModel && seriesModel.getReferringComponents('xAxis', true).models[0]);
+            || (seriesModel && seriesModel.getReferringComponents('xAxis', SINGLE_REFERRING).models[0]);
         const yAxisModel = finder.yAxisModel
-            || (seriesModel && seriesModel.getReferringComponents('yAxis', true).models[0]);
+            || (seriesModel && seriesModel.getReferringComponents('yAxis', SINGLE_REFERRING).models[0]);
         const gridModel = finder.gridModel;
         const coordsList = this._coordsList;
         let cartesian: Cartesian2D;
diff --git a/src/coord/cartesian/cartesianAxisHelper.ts b/src/coord/cartesian/cartesianAxisHelper.ts
index badc476..1d66179 100644
--- a/src/coord/cartesian/cartesianAxisHelper.ts
+++ b/src/coord/cartesian/cartesianAxisHelper.ts
@@ -23,6 +23,7 @@ import GridModel from './GridModel';
 import CartesianAxisModel from './AxisModel';
 import SeriesModel from '../../model/Series';
 import { __DEV__ } from '../../config';
+import { SINGLE_REFERRING } from '../../util/model';
 
 interface CartesianAxisLayout {
     position: [number, number];
@@ -112,7 +113,9 @@ export function findAxisModels(seriesModel: SeriesModel): {
     } as ReturnType<typeof findAxisModels>;
     zrUtil.each(axisModelMap, function (v, key) {
         const axisType = key.replace(/Model$/, '');
-        const axisModel = seriesModel.getReferringComponents(axisType, true).models[0] as CartesianAxisModel;
+        const axisModel = seriesModel.getReferringComponents(
+            axisType, SINGLE_REFERRING
+        ).models[0] as CartesianAxisModel;
 
         if (__DEV__) {
             if (!axisModel) {
diff --git a/src/coord/geo/Geo.ts b/src/coord/geo/Geo.ts
index df84083..c5e0630 100644
--- a/src/coord/geo/Geo.ts
+++ b/src/coord/geo/Geo.ts
@@ -24,7 +24,7 @@ import geoSourceManager from './geoSourceManager';
 import Region from './Region';
 import { NameMap } from './geoTypes';
 import GlobalModel from '../../model/Global';
-import { ParsedModelFinder } from '../../util/model';
+import { ParsedModelFinder, SINGLE_REFERRING } from '../../util/model';
 import GeoModel from './GeoModel';
 import { resizeGeoType } from './geoCreator';
 
@@ -173,7 +173,9 @@ function getCoordSys(finder: ParsedModelFinder): Geo {
         : seriesModel
         ? (
             seriesModel.coordinateSystem as Geo // For map series.
-            || ((seriesModel.getReferringComponents('geo', true).models[0] || {}) as GeoModel).coordinateSystem
+            || (
+                (seriesModel.getReferringComponents('geo', SINGLE_REFERRING).models[0] || {}
+            ) as GeoModel).coordinateSystem
         )
         : null;
 }
diff --git a/src/coord/parallel/parallelCreator.ts b/src/coord/parallel/parallelCreator.ts
index b2c9e78..ba747ad 100644
--- a/src/coord/parallel/parallelCreator.ts
+++ b/src/coord/parallel/parallelCreator.ts
@@ -29,6 +29,7 @@ import ParallelModel from './ParallelModel';
 import { CoordinateSystemMaster } from '../CoordinateSystem';
 import ParallelSeriesModel from '../../chart/parallel/ParallelSeries';
 import CoordinateSystemManager from '../../CoordinateSystem';
+import { SINGLE_REFERRING } from '../../util/model';
 
 function create(ecModel: GlobalModel, api: ExtensionAPI): CoordinateSystemMaster[] {
     const coordSysList: CoordinateSystemMaster[] = [];
@@ -48,11 +49,9 @@ function create(ecModel: GlobalModel, api: ExtensionAPI): CoordinateSystemMaster
     // Inject the coordinateSystems into seriesModel
     ecModel.eachSeries(function (seriesModel) {
         if ((seriesModel as ParallelSeriesModel).get('coordinateSystem') === 'parallel') {
-            const parallelModel = ecModel.queryComponents({
-                mainType: 'parallel',
-                index: (seriesModel as ParallelSeriesModel).get('parallelIndex'),
-                id: (seriesModel as ParallelSeriesModel).get('parallelId')
-            })[0] as ParallelModel;
+            const parallelModel = seriesModel.getReferringComponents(
+                'parallel', SINGLE_REFERRING
+            ).models[0] as ParallelModel;
             seriesModel.coordinateSystem = parallelModel.coordinateSystem;
         }
     });
diff --git a/src/coord/polar/AxisModel.ts b/src/coord/polar/AxisModel.ts
index 84efdd4..d2acf4d 100644
--- a/src/coord/polar/AxisModel.ts
+++ b/src/coord/polar/AxisModel.ts
@@ -25,6 +25,7 @@ import { AxisBaseOption } from '../axisCommonTypes';
 import AngleAxis from './AngleAxis';
 import RadiusAxis from './RadiusAxis';
 import { AxisBaseModel } from '../AxisBaseModel';
+import { SINGLE_REFERRING } from '../../util/model';
 
 export interface AngleAxisOption extends AxisBaseOption {
     /**
@@ -66,11 +67,7 @@ class PolarAxisModel<T extends PolarAxisOption = PolarAxisOption> extends Compon
     axis: AngleAxis | RadiusAxis;
 
     getCoordSysModel(): ComponentModel {
-        return this.ecModel.queryComponents({
-            mainType: 'polar',
-            index: this.option.polarIndex,
-            id: this.option.polarId
-        })[0];
+        return this.getReferringComponents('polar', SINGLE_REFERRING).models[0];
     }
 }
 
diff --git a/src/coord/polar/polarCreator.ts b/src/coord/polar/polarCreator.ts
index 04789dd..4c807c3 100644
--- a/src/coord/polar/polarCreator.ts
+++ b/src/coord/polar/polarCreator.ts
@@ -39,6 +39,7 @@ import AngleAxis from './AngleAxis';
 import { PolarAxisModel, AngleAxisModel, RadiusAxisModel } from './AxisModel';
 import SeriesModel from '../../model/Series';
 import { SeriesOption } from '../../util/types';
+import { SINGLE_REFERRING } from '../../util/model';
 
 /**
  * Resize method bound to the polar
@@ -164,11 +165,9 @@ const polarCreator = {
             polarId?: string
         }>) {
             if (seriesModel.get('coordinateSystem') === 'polar') {
-                const polarModel = ecModel.queryComponents({
-                    mainType: 'polar',
-                    index: seriesModel.get('polarIndex'),
-                    id: seriesModel.get('polarId')
-                })[0] as PolarModel;
+                const polarModel = seriesModel.getReferringComponents(
+                    'polar', SINGLE_REFERRING
+                ).models[0] as PolarModel;
 
                 if (__DEV__) {
                     if (!polarModel) {
diff --git a/src/coord/single/singleCreator.ts b/src/coord/single/singleCreator.ts
index 6f58e1f..ba432cf 100644
--- a/src/coord/single/singleCreator.ts
+++ b/src/coord/single/singleCreator.ts
@@ -28,6 +28,7 @@ import ExtensionAPI from '../../ExtensionAPI';
 import SingleAxisModel from './AxisModel';
 import SeriesModel from '../../model/Series';
 import { SeriesOption } from '../../util/types';
+import { SINGLE_REFERRING } from '../../util/model';
 
 /**
  * Create single coordinate system and inject it into seriesModel.
@@ -50,11 +51,9 @@ function create(ecModel: GlobalModel, api: ExtensionAPI) {
         singleAxisId?: string
     }>) {
         if (seriesModel.get('coordinateSystem') === 'singleAxis') {
-            const singleAxisModel = ecModel.queryComponents({
-                mainType: 'singleAxis',
-                index: seriesModel.get('singleAxisIndex'),
-                id: seriesModel.get('singleAxisId')
-            })[0] as SingleAxisModel;
+            const singleAxisModel = seriesModel.getReferringComponents(
+                'singleAxis', SINGLE_REFERRING
+            ).models[0] as SingleAxisModel;
             seriesModel.coordinateSystem = singleAxisModel && singleAxisModel.coordinateSystem;
         }
     });
diff --git a/src/model/Component.ts b/src/model/Component.ts
index f3bfc6d..f03a0e5 100644
--- a/src/model/Component.ts
+++ b/src/model/Component.ts
@@ -29,7 +29,7 @@ import {
     ClassManager,
     mountExtend
 } from '../util/clazz';
-import {makeInner, ModelFinderIndexQuery, queryReferringComponents, ModelFinderIdQuery} from '../util/model';
+import {makeInner, ModelFinderIndexQuery, queryReferringComponents, ModelFinderIdQuery, QueryReferringOpt} from '../util/model';
 import * as layout from '../util/layout';
 import GlobalModel from './Global';
 import {
@@ -276,7 +276,7 @@ class ComponentModel<Opt extends ComponentOption = ComponentOption> extends Mode
      *        If axis index / axis id not specified, use the first target as default.
      *        In other cases like dataZoom refer axis, if not specified, measn no refer.
      */
-    getReferringComponents(mainType: ComponentMainType, useDefault: boolean): {
+    getReferringComponents(mainType: ComponentMainType, opt: QueryReferringOpt): {
         // Always be array rather than null/undefined, which is convenient to use.
         models: ComponentModel[];
         // Whether target compoent specified
@@ -292,7 +292,7 @@ class ComponentModel<Opt extends ComponentOption = ComponentOption> extends Mode
                 index: this.get(indexKey, true) as unknown as ModelFinderIndexQuery,
                 id: this.get(idKey, true) as unknown as ModelFinderIdQuery
             },
-            useDefault
+            opt
         );
     }
 
diff --git a/src/model/referHelper.ts b/src/model/referHelper.ts
index 0f6d46c..03500f9 100644
--- a/src/model/referHelper.ts
+++ b/src/model/referHelper.ts
@@ -32,6 +32,7 @@ import SeriesModel from './Series';
 import type PolarModel from '../coord/polar/PolarModel';
 import type { SeriesOption, SeriesOnCartesianOptionMixin } from '../util/types';
 import type { AxisBaseModel } from '../coord/AxisBaseModel';
+import { SINGLE_REFERRING } from '../util/model';
 
 /**
  * @class
@@ -94,8 +95,8 @@ const fetchers: Record<SupportedCoordSys, Fetcher> = {
     cartesian2d: function (
         seriesModel: SeriesModel<SeriesOption & SeriesOnCartesianOptionMixin>, result, axisMap, categoryAxisMap
     ) {
-        const xAxisModel = seriesModel.getReferringComponents('xAxis', true).models[0] as AxisBaseModel;
-        const yAxisModel = seriesModel.getReferringComponents('yAxis', true).models[0] as AxisBaseModel;
+        const xAxisModel = seriesModel.getReferringComponents('xAxis', SINGLE_REFERRING).models[0] as AxisBaseModel;
+        const yAxisModel = seriesModel.getReferringComponents('yAxis', SINGLE_REFERRING).models[0] as AxisBaseModel;
 
         if (__DEV__) {
             if (!xAxisModel) {
@@ -129,7 +130,9 @@ const fetchers: Record<SupportedCoordSys, Fetcher> = {
     },
 
     singleAxis: function (seriesModel, result, axisMap, categoryAxisMap) {
-        const singleAxisModel = seriesModel.getReferringComponents('singleAxis', true).models[0] as AxisBaseModel;
+        const singleAxisModel = seriesModel.getReferringComponents(
+            'singleAxis', SINGLE_REFERRING
+        ).models[0] as AxisBaseModel;
 
         if (__DEV__) {
             if (!singleAxisModel) {
@@ -147,7 +150,7 @@ const fetchers: Record<SupportedCoordSys, Fetcher> = {
     },
 
     polar: function (seriesModel, result, axisMap, categoryAxisMap) {
-        const polarModel = seriesModel.getReferringComponents('polar', true).models[0] as PolarModel;
+        const polarModel = seriesModel.getReferringComponents('polar', SINGLE_REFERRING).models[0] as PolarModel;
         const radiusAxisModel = polarModel.findAxisModel('radiusAxis');
         const angleAxisModel = polarModel.findAxisModel('angleAxis');
 
diff --git a/src/util/model.ts b/src/util/model.ts
index 8247a49..6225339 100644
--- a/src/util/model.ts
+++ b/src/util/model.ts
@@ -744,7 +744,7 @@ export function parseFinder(
     }
 
     const defaultMainType = opt ? opt.defaultMainType : null;
-    const queryOptionMap = createHashMap<QueryReferringComponentsOption, ComponentMainType>();
+    const queryOptionMap = createHashMap<QueryReferringOption, ComponentMainType>();
     const result = {} as ParsedModelFinder;
 
     each(finder, function (value, key) {
@@ -756,7 +756,7 @@ export function parseFinder(
 
         const parsedKey = key.match(/^(\w+)(Index|Id|Name)$/) || [];
         const mainType = parsedKey[1];
-        const queryType = (parsedKey[2] || '').toLowerCase() as keyof QueryReferringComponentsOption;
+        const queryType = (parsedKey[2] || '').toLowerCase() as keyof QueryReferringOption;
 
         if (
             !mainType
@@ -776,7 +776,11 @@ export function parseFinder(
             ecModel,
             mainType,
             queryOption,
-            mainType === defaultMainType
+            {
+                useDefault: mainType === defaultMainType,
+                enableAll: true,
+                enableNone: true
+            }
         );
         result[mainType + 'Models'] = queryResult.models;
         result[mainType + 'Model'] = queryResult.models[0];
@@ -785,26 +789,38 @@ export function parseFinder(
     return result;
 }
 
-type QueryReferringComponentsOption = {
+type QueryReferringOption = {
     index?: ModelFinderIndexQuery,
     id?: ModelFinderIdQuery,
     name?: ModelFinderNameQuery,
 };
 
+export const SINGLE_REFERRING: QueryReferringOpt = { useDefault: true, enableAll: false, enableNone: false };
+export const MULTIPLE_REFERRING: QueryReferringOpt = { useDefault: false, enableAll: true, enableNone: true };
+
+export type QueryReferringOpt = {
+    // Whether to use the first componet as the default if none of index/id/name are specified.
+    useDefault: boolean;
+    // Whether to enable `'all'` on index option.
+    enableAll: boolean;
+    // Whether to enable `'none'`/`false` on index option.
+    enableNone: boolean;
+};
+
 export function queryReferringComponents(
     ecModel: GlobalModel,
     mainType: ComponentMainType,
-    option: QueryReferringComponentsOption,
-    useDefault?: boolean
+    userOption: QueryReferringOption,
+    opt: QueryReferringOpt
 ): {
     // Always be array rather than null/undefined, which is convenient to use.
     models: ComponentModel[];
     // Whether there is indexOption/id/name specified
     specified: boolean;
 } {
-    let indexOption = option.index;
-    let idOption = option.id;
-    let nameOption = option.name;
+    let indexOption = userOption.index;
+    let idOption = userOption.id;
+    let nameOption = userOption.name;
 
     const result = {
         models: null as ComponentModel[],
@@ -815,12 +831,13 @@ export function queryReferringComponents(
         // Use the first as default if `useDefault`.
         let firstCmpt;
         result.models = (
-            useDefault && (firstCmpt = ecModel.getComponent(mainType))
+            opt.useDefault && (firstCmpt = ecModel.getComponent(mainType))
         ) ? [firstCmpt] : [];
         return result;
     }
 
     if (indexOption === 'none' || indexOption === false) {
+        assert(opt.enableNone, '`"none"` or `false` is not a valid value on index option.');
         result.models = [];
         return result;
     }
@@ -828,6 +845,7 @@ export function queryReferringComponents(
     // `queryComponents` will return all components if
     // both all of index/id/name are null/undefined.
     if (indexOption === 'all') {
+        assert(opt.enableAll, '`"all"` is not a valid value on index option.');
         indexOption = idOption = nameOption = null;
     }
     result.models = ecModel.queryComponents({
diff --git a/test/dataZoom-feature.html b/test/dataZoom-feature.html
index 520ce6e..9d42ccc 100644
--- a/test/dataZoom-feature.html
+++ b/test/dataZoom-feature.html
@@ -93,7 +93,7 @@ under the License.
             var chart = testHelper.create(echarts, 'refer_by_id', {
                 title: [
                     'refer axis by id',
-                    'Two grids, dataZoom should refer to the second grid',
+                    'Two grids, dataZoom should **only refer to the bottom grid**',
                 ],
                 option: option,
                 height: 300,
diff --git a/test/option-replaceMerge.html b/test/option-replaceMerge.html
index 1c8ff5f..0ed8388 100644
--- a/test/option-replaceMerge.html
+++ b/test/option-replaceMerge.html
@@ -920,72 +920,78 @@ under the License.
 
         <script>
         require(['echarts'], function (echarts) {
-            var option;
+            function makePartialOption() {
+                return {
+                    grid: [{
+                        bottom: '60%'
+                    }, {
+                        id: 'gb',
+                        top: '60%'
+                    }],
+                    xAxis: [{
+                        type: 'category'
+                    }, {
+                        type: 'category'
+                    }, {
+                        id: 'xb0',
+                        type: 'category',
+                        gridIndex: 1
+                    }, {
+                        id: 'xb1',
+                        type: 'category',
+                        gridIndex: 1
+                    }],
+                    yAxis: [{
 
-            option = {
-                // toolbox: {
-                //     left: 'center',
-                //     feature: {
-                //         dataZoom: {}
-                //     }
-                // },
-                grid: [{
-                    bottom: '60%'
-                }, {
-                    id: 'gb',
-                    top: '60%'
-                }],
-                xAxis: [{
-                    type: 'category'
-                }, {
-                    type: 'category'
-                }, {
-                    id: 'xb0',
-                    type: 'category',
-                    gridIndex: 1
-                }, {
-                    id: 'xb1',
-                    type: 'category',
-                    gridIndex: 1
-                }],
-                yAxis: [{
+                    }, {
+                        id: 'yb',
+                        gridIndex: 1
+                    }],
+                    series: [{
+                        type: 'line',
+                        data: [[11, 22], [33, 44]]
+                    }, {
+                        type: 'line',
+                        xAxisIndex: 1,
+                        data: [[11111, 52], [21133, 74]]
+                    }, {
+                        id: 'sb0',
+                        type: 'line',
+                        xAxisIndex: 2,
+                        yAxisIndex: 1,
+                        data: [[23, 432], [54, 552]]
+                    }, {
+                        id: 'sb1',
+                        type: 'line',
+                        xAxisIndex: 3,
+                        yAxisIndex: 1,
+                        data: [[222233, 1432], [111154, 1552]]
+                    }]
+                };
+            }
 
-                }, {
-                    id: 'yb',
-                    gridIndex: 1
-                }],
+            var option = echarts.util.extend(makePartialOption(), {
+                toolbox: {
+                    left: 'center',
+                    feature: {
+                        dataZoom: {}
+                    }
+                },
                 dataZoom: [{
-                    type: 'slider'
-                }, {
-                    type: 'inside'
-                }],
-                series: [{
-                    type: 'line',
-                    data: [[11, 22], [33, 44]]
-                }, {
-                    type: 'line',
-                    xAxisIndex: 1,
-                    data: [[11111, 52], [21133, 74]]
-                }, {
-                    id: 'sb0',
-                    type: 'line',
-                    xAxisIndex: 2,
-                    yAxisIndex: 1,
-                    data: [[23, 432], [54, 552]]
+                    type: 'slider',
+                    xAxisIndex: 'all'
                 }, {
-                    id: 'sb1',
-                    type: 'line',
-                    xAxisIndex: 3,
-                    yAxisIndex: 1,
-                    data: [[222233, 1432], [111154, 1552]]
+                    type: 'inside',
+                    xAxisIndex: 'all'
                 }]
-            };
+            });
 
             var chart = testHelper.create(echarts, 'main_replaceMerge_if_not_declared_in_option', {
                 title: [
+                    'DataZoom controlls all of the axes.',
                     'Click btn "remove all grids".',
                     'Should no error.',
-                    'Click btn "addback the first grid".',
+                    'Click btn "addback".',
                     'Functionalities should be OK.'
                 ],
                 option: option,
@@ -997,6 +1003,11 @@ under the License.
                             // Not declared in option
                         }, { replaceMerge: ['grid', 'xAxis', 'yAxis', 'series'] });
                     }
+                }, {
+                    text: 'addback',
+                    onclick: function () {
+                        chart.setOption(makePartialOption());
+                    }
                 }]
             });
         });


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


[incubator-echarts] 04/16: ts: remove "any" from type ECUnitOption (which causes "any" spread).

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 18c21561ff727e9c1795972a199c015ac63df522
Author: 100pah <su...@gmail.com>
AuthorDate: Wed Jul 8 22:53:48 2020 +0800

    ts: remove "any" from type ECUnitOption (which causes "any" spread).
---
 src/chart/bar/BarView.ts                   |  8 +++++---
 src/chart/bar/PictorialBarView.ts          |  2 +-
 src/chart/parallel/ParallelSeries.ts       |  4 +++-
 src/component/axisPointer.ts               |  6 +++---
 src/component/brush/preprocessor.ts        | 12 ++++++------
 src/component/toolbox/feature/Brush.ts     |  2 +-
 src/component/toolbox/feature/DataView.ts  |  6 +++---
 src/component/toolbox/feature/DataZoom.ts  |  9 +++------
 src/component/toolbox/feature/MagicType.ts | 11 ++++++-----
 src/coord/parallel/parallelPreprocessor.ts |  6 ++++--
 src/echarts.ts                             |  5 +++--
 src/model/Component.ts                     |  2 ++
 src/model/Model.ts                         |  2 +-
 src/model/mixin/textStyle.ts               |  2 +-
 src/util/types.ts                          |  8 +++++++-
 15 files changed, 49 insertions(+), 36 deletions(-)

diff --git a/src/chart/bar/BarView.ts b/src/chart/bar/BarView.ts
index d7dc1e1..6a29190 100644
--- a/src/chart/bar/BarView.ts
+++ b/src/chart/bar/BarView.ts
@@ -47,7 +47,9 @@ import {
     OrdinalSortInfo,
     Payload,
     OrdinalNumber,
-    ParsedValue
+    ParsedValue,
+    ECUnitOption,
+    AnimationOptionMixin
 } from '../../util/types';
 import BarSeriesModel, { BarSeriesOption, BarDataItemOption } from './BarSeries';
 import type Axis2D from '../../coord/cartesian/Axis2D';
@@ -708,7 +710,7 @@ function removeRect(
         style: {
             opacity: 0
         }
-    }, animationModel, dataIndex, function () {
+    }, animationModel as Model<AnimationOptionMixin>, dataIndex, function () {
         el.parent && el.parent.remove(el);
     });
 }
@@ -724,7 +726,7 @@ function removeSector(
         style: {
             opacity: 0
         }
-    }, animationModel, dataIndex, function () {
+    }, animationModel as Model<AnimationOptionMixin>, dataIndex, function () {
         el.parent && el.parent.remove(el);
     });
 }
diff --git a/src/chart/bar/PictorialBarView.ts b/src/chart/bar/PictorialBarView.ts
index 48a7774..d7ee71c 100644
--- a/src/chart/bar/PictorialBarView.ts
+++ b/src/chart/bar/PictorialBarView.ts
@@ -222,7 +222,7 @@ class PictorialBarView extends ChartView {
         if (ecModel.get('animation')) {
             if (data) {
                 data.eachItemGraphicEl(function (bar: PictorialBarElement) {
-                    removeBar(data, graphic.getECData(bar).dataIndex, ecModel, bar);
+                    removeBar(data, graphic.getECData(bar).dataIndex, ecModel as Model<AnimationOptionMixin>, bar);
                 });
             }
         }
diff --git a/src/chart/parallel/ParallelSeries.ts b/src/chart/parallel/ParallelSeries.ts
index 1fd5c8f..8dbe52b 100644
--- a/src/chart/parallel/ParallelSeries.ts
+++ b/src/chart/parallel/ParallelSeries.ts
@@ -32,7 +32,7 @@ import {
  } from '../../util/types';
 import GlobalModel from '../../model/Global';
 import List from '../../data/List';
-import { ParallelActiveState } from '../../coord/parallel/AxisModel';
+import { ParallelActiveState, ParallelAxisOption } from '../../coord/parallel/AxisModel';
 import Parallel from '../../coord/parallel/Parallel';
 import Source from '../../data/Source';
 import ParallelModel from '../../coord/parallel/ParallelModel';
@@ -72,6 +72,8 @@ export interface ParallelSeriesOption extends
     realtime?: boolean;
     tooltip?: SeriesTooltipOption;
 
+    parallelAxisDefault?: ParallelAxisOption;
+
     emphasis?: {
         label?: LabelOption;
         lineStyle?: LineStyleOption;
diff --git a/src/component/axisPointer.ts b/src/component/axisPointer.ts
index 2e0b9e7..1b68850 100644
--- a/src/component/axisPointer.ts
+++ b/src/component/axisPointer.ts
@@ -36,15 +36,15 @@ ComponentModel.registerClass(AxisPointerModel);
 echarts.registerPreprocessor(function (option) {
     // Always has a global axisPointerModel for default setting.
     if (option) {
-        (!option.axisPointer || option.axisPointer.length === 0)
+        (!option.axisPointer || (option.axisPointer as []).length === 0)
             && (option.axisPointer = {});
 
-        const link = option.axisPointer.link;
+        const link = (option.axisPointer as any).link;
         // Normalize to array to avoid object mergin. But if link
         // is not set, remain null/undefined, otherwise it will
         // override existent link setting.
         if (link && !zrUtil.isArray(link)) {
-            option.axisPointer.link = [link];
+            (option.axisPointer as any).link = [link];
         }
     }
 });
diff --git a/src/component/brush/preprocessor.ts b/src/component/brush/preprocessor.ts
index 8184ed6..2c49e2a 100644
--- a/src/component/brush/preprocessor.ts
+++ b/src/component/brush/preprocessor.ts
@@ -21,14 +21,14 @@
 import * as zrUtil from 'zrender/src/core/util';
 import { ECUnitOption, Dictionary } from '../../util/types';
 import { BrushOption, BrushToolboxIconType } from './BrushModel';
+import { ToolboxOption } from '../toolbox/ToolboxModel';
+import { ToolboxBrushFeatureOption } from '../toolbox/feature/Brush';
+import { normalizeToArray } from '../../util/model';
 
 const DEFAULT_TOOLBOX_BTNS: BrushToolboxIconType[] = ['rect', 'polygon', 'keep', 'clear'];
 
 export default function (option: ECUnitOption, isNew: boolean): void {
-    let brushComponents = option && option.brush;
-    if (!zrUtil.isArray(brushComponents)) {
-        brushComponents = brushComponents ? [brushComponents] : [];
-    }
+    const brushComponents = normalizeToArray(option ? option.brush : []);
 
     if (!brushComponents.length) {
         return;
@@ -45,7 +45,7 @@ export default function (option: ECUnitOption, isNew: boolean): void {
         }
     });
 
-    let toolbox = option && option.toolbox;
+    let toolbox: ToolboxOption = option && option.toolbox;
 
     if (zrUtil.isArray(toolbox)) {
         toolbox = toolbox[0];
@@ -56,7 +56,7 @@ export default function (option: ECUnitOption, isNew: boolean): void {
     }
 
     const toolboxFeature = (toolbox.feature || (toolbox.feature = {}));
-    const toolboxBrush = toolboxFeature.brush || (toolboxFeature.brush = {});
+    const toolboxBrush = (toolboxFeature.brush || (toolboxFeature.brush = {})) as ToolboxBrushFeatureOption;
     const brushTypes = toolboxBrush.type || (toolboxBrush.type = []);
 
     brushTypes.push.apply(brushTypes, brushComponentSpecifiedBtns);
diff --git a/src/component/toolbox/feature/Brush.ts b/src/component/toolbox/feature/Brush.ts
index d39fbdb..984fa33 100644
--- a/src/component/toolbox/feature/Brush.ts
+++ b/src/component/toolbox/feature/Brush.ts
@@ -36,7 +36,7 @@ const ICON_TYPES = ['rect', 'polygon', 'lineX', 'lineY', 'keep', 'clear'] as con
 
 type IconType = typeof ICON_TYPES[number];
 
-interface ToolboxBrushFeatureOption extends ToolboxFeatureOption {
+export interface ToolboxBrushFeatureOption extends ToolboxFeatureOption {
     type?: IconType[]
     icon?: {[key in IconType]?: string}
     title?: {[key in IconType]?: string}
diff --git a/src/component/toolbox/feature/DataView.ts b/src/component/toolbox/feature/DataView.ts
index 5a855ac..690f7d4 100644
--- a/src/component/toolbox/feature/DataView.ts
+++ b/src/component/toolbox/feature/DataView.ts
@@ -270,15 +270,15 @@ function parseContents(str: string, blockMetaList: SeriesGroupMeta[]) {
 
             if (blockMeta) {
                 newOption[axisKey] = newOption[axisKey] || [];
-                newOption[axisKey][blockMeta.axisIndex] = {
+                (newOption[axisKey] as any)[blockMeta.axisIndex] = {
                     data: result.categories
                 };
-                newOption.series = newOption.series.concat(result.series);
+                newOption.series = (newOption.series as SeriesOption[]).concat(result.series);
             }
         }
         else {
             const result = parseListContents(block);
-            newOption.series.push(result);
+            (newOption.series as SeriesOption[]).push(result);
         }
     });
     return newOption;
diff --git a/src/component/toolbox/feature/DataZoom.ts b/src/component/toolbox/feature/DataZoom.ts
index 5a2da53..ec2bed2 100644
--- a/src/component/toolbox/feature/DataZoom.ts
+++ b/src/component/toolbox/feature/DataZoom.ts
@@ -39,9 +39,9 @@ import ExtensionAPI from '../../../ExtensionAPI';
 import { Payload, ECUnitOption, Dictionary } from '../../../util/types';
 import Cartesian2D from '../../../coord/cartesian/Cartesian2D';
 import CartesianAxisModel from '../../../coord/cartesian/AxisModel';
-import DataZoomModel from '../../dataZoom/DataZoomModel';
+import DataZoomModel, { DataZoomOption } from '../../dataZoom/DataZoomModel';
 import { DataZoomPayloadBatchItem } from '../../dataZoom/helper';
-import { ModelFinderObject, ModelFinderIndexQuery } from '../../../util/model';
+import { ModelFinderObject, ModelFinderIndexQuery, normalizeToArray } from '../../../util/model';
 import { ToolboxOption } from '../ToolboxModel';
 
 const dataZoomLang = lang.toolbox.dataZoom;
@@ -301,10 +301,7 @@ echarts.registerPreprocessor(function (option: ECUnitOption) {
         return;
     }
 
-    let dataZoomOpts = option.dataZoom || (option.dataZoom = []);
-    if (!zrUtil.isArray(dataZoomOpts)) {
-        option.dataZoom = dataZoomOpts = [dataZoomOpts];
-    }
+    const dataZoomOpts = option.dataZoom = normalizeToArray(option.dataZoom) as DataZoomOption[];
 
     let toolboxOpt = option.toolbox as ToolboxOption;
     if (toolboxOpt) {
diff --git a/src/component/toolbox/feature/MagicType.ts b/src/component/toolbox/feature/MagicType.ts
index f05996b..2dfd038 100644
--- a/src/component/toolbox/feature/MagicType.ts
+++ b/src/component/toolbox/feature/MagicType.ts
@@ -108,7 +108,7 @@ class MagicType extends ToolboxFeature<ToolboxMagicTypeFeatureOption> {
             if (newSeriesOpt) {
                 // PENDING If merge original option?
                 zrUtil.defaults(newSeriesOpt, seriesModel.option);
-                newOption.series.push(newSeriesOpt);
+                (newOption.series as SeriesOption[]).push(newSeriesOpt);
             }
             // Modify boundaryGap
             const coordSys = seriesModel.coordinateSystem;
@@ -126,9 +126,9 @@ class MagicType extends ToolboxFeature<ToolboxMagicTypeFeatureOption> {
 
                     newOption[axisType] = newOption[axisType] || [];
                     for (let i = 0; i <= axisIndex; i++) {
-                        newOption[axisType][axisIndex] = newOption[axisType][axisIndex] || {};
+                        (newOption[axisType] as any)[axisIndex] = (newOption[axisType] as any)[axisIndex] || {};
                     }
-                    newOption[axisType][axisIndex].boundaryGap = type === 'bar';
+                    (newOption[axisType] as any)[axisIndex].boundaryGap = type === 'bar';
                 }
             }
         };
@@ -155,8 +155,9 @@ class MagicType extends ToolboxFeature<ToolboxMagicTypeFeatureOption> {
         let newTitle;
         // Change title of stack
         if (type === 'stack') {
-            const isStack = newOption.series && newOption.series[0]
-                && newOption.series[0].stack === INNER_STACK_KEYWORD;
+            const seriesOptions = newOption.series as (SeriesOption & { stack: string })[];
+            const isStack = seriesOptions && seriesOptions[0]
+                && seriesOptions[0].stack === INNER_STACK_KEYWORD;
             newTitle = isStack
                 ? zrUtil.merge({ stack: magicTypeLang.title.tiled }, magicTypeLang.title)
                 : zrUtil.clone(magicTypeLang.title);
diff --git a/src/coord/parallel/parallelPreprocessor.ts b/src/coord/parallel/parallelPreprocessor.ts
index d456ee2..fda2a2f 100644
--- a/src/coord/parallel/parallelPreprocessor.ts
+++ b/src/coord/parallel/parallelPreprocessor.ts
@@ -21,6 +21,8 @@
 import * as zrUtil from 'zrender/src/core/util';
 import * as modelUtil from '../../util/model';
 import { ECUnitOption, SeriesOption } from '../../util/types';
+import { ParallelAxisOption } from './AxisModel';
+import { ParallelSeriesOption } from '../../chart/parallel/ParallelSeries';
 
 export default function (option: ECUnitOption): void {
     createParallelIfNeeded(option);
@@ -54,7 +56,7 @@ function createParallelIfNeeded(option: ECUnitOption): void {
  * @inner
  */
 function mergeAxisOptionFromParallel(option: ECUnitOption): void {
-    const axes = modelUtil.normalizeToArray(option.parallelAxis);
+    const axes = modelUtil.normalizeToArray(option.parallelAxis) as ParallelAxisOption[];
 
     zrUtil.each(axes, function (axisOption) {
         if (!zrUtil.isObject(axisOption)) {
@@ -62,7 +64,7 @@ function mergeAxisOptionFromParallel(option: ECUnitOption): void {
         }
 
         const parallelIndex = axisOption.parallelIndex || 0;
-        const parallelOption = modelUtil.normalizeToArray(option.parallel)[parallelIndex];
+        const parallelOption = modelUtil.normalizeToArray(option.parallel)[parallelIndex] as ParallelSeriesOption;
 
         if (parallelOption && parallelOption.parallelAxisDefault) {
             zrUtil.merge(axisOption, parallelOption.parallelAxisDefault, false);
diff --git a/src/echarts.ts b/src/echarts.ts
index 3cdd73f..581bf53 100644
--- a/src/echarts.ts
+++ b/src/echarts.ts
@@ -62,7 +62,8 @@ import {
     ZRColor,
     ComponentMainType,
     ComponentSubType,
-    ZRElementEvent
+    ZRElementEvent,
+    ColorString
 } from './util/types';
 import Displayable from 'zrender/src/graphic/Displayable';
 import IncrementalDisplayable from 'zrender/src/graphic/IncrementalDisplayable';
@@ -1375,7 +1376,7 @@ class ECharts extends Eventful {
 
                 // In IE8
                 if (!env.canvasSupported) {
-                    const colorArr = colorTool.parse(backgroundColor);
+                    const colorArr = colorTool.parse(backgroundColor as ColorString);
                     backgroundColor = colorTool.stringify(colorArr, 'rgb');
                     if (colorArr[3] === 0) {
                         backgroundColor = 'transparent';
diff --git a/src/model/Component.ts b/src/model/Component.ts
index e9c89ce..74fccf0 100644
--- a/src/model/Component.ts
+++ b/src/model/Component.ts
@@ -290,6 +290,8 @@ class ComponentModel<Opt extends ComponentOption = ComponentOption> extends Mode
 
     static registerClass: ClassManager['registerClass'];
 
+    static hasClass: ClassManager['hasClass'];
+
     static registerSubTypeDefaulter: componentUtil.SubTypeDefaulterManager['registerSubTypeDefaulter'];
 }
 
diff --git a/src/model/Model.ts b/src/model/Model.ts
index e9cf064..bfeac1c 100644
--- a/src/model/Model.ts
+++ b/src/model/Model.ts
@@ -32,7 +32,7 @@ import {ItemStyleMixin} from './mixin/itemStyle';
 import GlobalModel from './Global';
 import { ModelOption } from '../util/types';
 import { Dictionary } from 'zrender/src/core/types';
-import { mixin, clone, merge, extend, isFunction } from 'zrender/src/core/util';
+import { mixin, clone, merge } from 'zrender/src/core/util';
 
 // Since model.option can be not only `Dictionary` but also primary types,
 // we do this conditional type to avoid getting type 'never';
diff --git a/src/model/mixin/textStyle.ts b/src/model/mixin/textStyle.ts
index 907bba4..9483bc2 100644
--- a/src/model/mixin/textStyle.ts
+++ b/src/model/mixin/textStyle.ts
@@ -24,7 +24,7 @@ import ZRText from 'zrender/src/graphic/Text';
 
 const PATH_COLOR = ['textStyle', 'color'] as const;
 
-type LabelFontOption = Pick<LabelOption, 'fontStyle' | 'fontWeight' | 'fontSize' | 'fontFamily'>;
+export type LabelFontOption = Pick<LabelOption, 'fontStyle' | 'fontWeight' | 'fontSize' | 'fontFamily'>;
 type LabelRectRelatedOption = Pick<LabelOption,
     'align' | 'verticalAlign' | 'padding' | 'lineHeight' | 'baseline' | 'rich'
 > & LabelFontOption;
diff --git a/src/util/types.ts b/src/util/types.ts
index 3125df8..a99219c 100644
--- a/src/util/types.ts
+++ b/src/util/types.ts
@@ -353,8 +353,14 @@ export type ECUnitOption = {
     baseOption?: never
     options?: never
     media?: never
+
     timeline?: ComponentOption | ComponentOption[]
-    [key: string]: ComponentOption | ComponentOption[] | Dictionary<any> | any
+    backgroundColor?: ZRColor
+    darkMode?: boolean | 'auto'
+    textStyle?: Pick<LabelOption, 'color' | 'fontStyle' | 'fontWeight' | 'fontSize' | 'fontFamily'>
+
+    [key: string]: ComponentOption | ComponentOption[] | Dictionary<unknown> | unknown
+
 } & AnimationOptionMixin & ColorPaletteOptionMixin;
 
 /**


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


[incubator-echarts] 06/16: feature: In replaceMerge, trade {xxx: null/undefined} the same as {xxx: []} for ec option.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 1db1349b17adfe7924483bab289f78873a4855e0
Author: 100pah <su...@gmail.com>
AuthorDate: Tue Jul 14 17:28:21 2020 +0800

    feature: In replaceMerge, trade {xxx: null/undefined} the same as {xxx: []} for ec option.
---
 src/model/Global.ts           | 60 +++++++++++++++++++---------
 test/option-replaceMerge.html | 93 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 135 insertions(+), 18 deletions(-)

diff --git a/src/model/Global.ts b/src/model/Global.ts
index 5f5555e..ad02e72 100644
--- a/src/model/Global.ts
+++ b/src/model/Global.ts
@@ -62,7 +62,7 @@ export interface GlobalModelSetOptionOpts {
     replaceMerge: ComponentMainType | ComponentMainType[];
 }
 export interface InnerSetOptionOpts {
-    replaceMergeMainTypeMap: HashMap<boolean>;
+    replaceMergeMainTypeMap: HashMap<boolean, string>;
 }
 
 // -----------------------
@@ -81,7 +81,7 @@ class GlobalModel extends Model<ECUnitOption> {
 
     private _optionManager: OptionManager;
 
-    private _componentsMap: HashMap<ComponentModel[]>;
+    private _componentsMap: HashMap<ComponentModel[], string>;
 
     /**
      * `_componentsMap` might have "hole" becuase of remove.
@@ -218,6 +218,7 @@ class GlobalModel extends Model<ECUnitOption> {
         const componentsMap = this._componentsMap;
         const componentsCount = this._componentsCount;
         const newCmptTypes: ComponentMainType[] = [];
+        const newCmptTypeMap = createHashMap<boolean, string>();
         const replaceMergeMainTypeMap = opt && opt.replaceMergeMainTypeMap;
 
         resetSourceDefaulter(this);
@@ -237,9 +238,23 @@ class GlobalModel extends Model<ECUnitOption> {
             }
             else if (mainType) {
                 newCmptTypes.push(mainType);
+                newCmptTypeMap.set(mainType, true);
             }
         });
 
+        if (replaceMergeMainTypeMap) {
+            // If there is a mainType `xxx` in `replaceMerge` but not declared in option,
+            // we trade it as it is declared in option as `{xxx: []}`. Because:
+            // (1) for normal merge, `{xxx: null/undefined}` are the same meaning as `{xxx: []}`.
+            // (2) some preprocessor may convert some of `{xxx: null/undefined}` to `{xxx: []}`.
+            replaceMergeMainTypeMap.each(function (b, mainTypeInReplaceMerge) {
+                if (!newCmptTypeMap.get(mainTypeInReplaceMerge)) {
+                    newCmptTypes.push(mainTypeInReplaceMerge);
+                    newCmptTypeMap.set(mainTypeInReplaceMerge, true);
+                }
+            });
+        }
+
         (ComponentModel as ComponentModelConstructor).topologicalTravel(
             newCmptTypes,
             (ComponentModel as ComponentModelConstructor).getAllClassMainTypes(),
@@ -249,10 +264,8 @@ class GlobalModel extends Model<ECUnitOption> {
 
         function visitComponent(
             this: GlobalModel,
-            mainType: ComponentMainType,
-            dependencies: string | string[]
+            mainType: ComponentMainType
         ): void {
-
             const newCmptOptionList = modelUtil.normalizeToArray(newOption[mainType]);
 
             const oldCmptList = componentsMap.get(mainType);
@@ -263,11 +276,13 @@ class GlobalModel extends Model<ECUnitOption> {
             // Set mainType and complete subType.
             modelUtil.setComponentTypeToKeyInfo(mappingResult, mainType, ComponentModel as ComponentModelConstructor);
 
-            // Set it before the travel, in case that `this._componentsMap` is
-            // used in some `init` or `merge` of components.
+            // Empty it before the travel, in order to prevent `this._componentsMap`
+            // from being used in the `init`/`mergeOption`/`optionUpdated` of some
+            // components, which is probably incorrect logic.
             option[mainType] = null;
             componentsMap.set(mainType, null);
             componentsCount.set(mainType, 0);
+
             const optionsByMainType = [] as ComponentOption[];
             const cmptsByMainType = [] as ComponentModel[];
             let cmptsCountByMainType = 0;
@@ -794,8 +809,8 @@ export interface QueryConditionKindB {
     mainType: ComponentMainType;
     subType?: ComponentSubType;
     index?: number | number[];
-    id?: string | string[];
-    name?: string | string[];
+    id?: string | number | (string | number)[];
+    name?: (string | number) | (string | number)[];
 }
 export interface EachComponentAllCallback {
     (mainType: string, model: ComponentModel, componentIndex: number): void;
@@ -844,16 +859,25 @@ function mergeTheme(option: ECUnitOption, theme: ThemeOption): void {
 
 function queryByIdOrName<T extends { id?: string, name?: string }>(
     attr: 'id' | 'name',
-    idOrName: string | string[],
+    idOrName: string | number | (string | number)[],
     cmpts: T[]
 ): T[] {
-    let keyMap: HashMap<string>;
-    return isArray(idOrName)
-        ? (
-            keyMap = createHashMap(idOrName),
-            filter(cmpts, cmpt => cmpt && keyMap.get(cmpt[attr]) != null)
-        )
-        : filter(cmpts, cmpt => cmpt && cmpt[attr] === idOrName + '');
+    // Here is a break from echarts4: string and number-like string are
+    // traded as equal.
+    if (isArray(idOrName)) {
+        const keyMap = createHashMap<boolean>(idOrName);
+        each(idOrName, function (idOrNameItem) {
+            if (idOrNameItem != null) {
+                modelUtil.validateIdOrName(idOrNameItem);
+                keyMap.set(idOrNameItem, true);
+            }
+        });
+        return filter(cmpts, cmpt => cmpt && keyMap.get(cmpt[attr]));
+    }
+    else {
+        modelUtil.validateIdOrName(idOrName);
+        return filter(cmpts, cmpt => cmpt && cmpt[attr] === idOrName + '');
+    }
 }
 
 function filterBySubType(
@@ -868,7 +892,7 @@ function filterBySubType(
 }
 
 function normalizeReplaceMergeInput(opts: GlobalModelSetOptionOpts): InnerSetOptionOpts {
-    const replaceMergeMainTypeMap = createHashMap<boolean>();
+    const replaceMergeMainTypeMap = createHashMap<boolean, string>();
     opts && each(modelUtil.normalizeToArray(opts.replaceMerge), function (mainType) {
         if (__DEV__) {
             assert(
diff --git a/test/option-replaceMerge.html b/test/option-replaceMerge.html
index 30dc848..e525e7a 100644
--- a/test/option-replaceMerge.html
+++ b/test/option-replaceMerge.html
@@ -48,6 +48,7 @@ under the License.
         <div id="main_replaceMerge_remove_all"></div>
         <div id="main_replaceMerge_reproduce_by_getOption_src"></div>
         <div id="main_replaceMerge_reproduce_by_getOption_tar"></div>
+        <div id="main_replaceMerge_if_not_declared_in_option"></div>
 
 
 
@@ -911,6 +912,98 @@ under the License.
 
 
 
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option;
+
+            option = {
+                // toolbox: {
+                //     left: 'center',
+                //     feature: {
+                //         dataZoom: {}
+                //     }
+                // },
+                grid: [{
+                    bottom: '60%'
+                }, {
+                    id: 'gb',
+                    top: '60%'
+                }],
+                xAxis: [{
+                    type: 'category'
+                }, {
+                    type: 'category'
+                }, {
+                    id: 'xb0',
+                    type: 'category',
+                    gridIndex: 1
+                }, {
+                    id: 'xb1',
+                    type: 'category',
+                    gridIndex: 1
+                }],
+                yAxis: [{
+
+                }, {
+                    id: 'yb',
+                    gridIndex: 1
+                }],
+                dataZoom: [{
+                    type: 'slider'
+                }, {
+                    type: 'inside'
+                }],
+                series: [{
+                    type: 'line',
+                    data: [[11, 22], [33, 44]]
+                }, {
+                    type: 'line',
+                    xAxisIndex: 1,
+                    data: [[11111, 52], [21133, 74]]
+                }, {
+                    id: 'sb0',
+                    type: 'line',
+                    xAxisIndex: 2,
+                    yAxisIndex: 1,
+                    data: [[23, 432], [54, 552]]
+                }, {
+                    id: 'sb1',
+                    type: 'line',
+                    xAxisIndex: 3,
+                    yAxisIndex: 1,
+                    data: [[222233, 1432], [111154, 1552]]
+                }]
+            };
+
+            var chart = testHelper.create(echarts, 'main_replaceMerge_if_not_declared_in_option', {
+                title: [
+                    'Click btn "remove all grids".',
+                    'Should no error.',
+                    'Click btn "addback the first grid".',
+                    'Functionalities should be OK.'
+                ],
+                option: option,
+                height: 350,
+                buttons: [{
+                    text: 'remove all grids',
+                    onclick: function () {
+                        chart.setOption({
+                            // Not declared in option
+                        }, { replaceMerge: ['grid', 'xAxis', 'yAxis', 'series'] });
+                    }
+                }]
+            });
+        });
+        </script>
+
+
+
+
     </body>
 </html>
 


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


[incubator-echarts] 11/16: fix: fix toolbox dataZoom when on grid.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 4bbe3b2f9e6017f6f416d8627305bcff147f3366
Author: 100pah <su...@gmail.com>
AuthorDate: Wed Jul 15 18:50:19 2020 +0800

    fix: fix toolbox dataZoom when on grid.
---
 src/component/helper/BrushTargetManager.ts |   4 +-
 src/component/toolbox/feature/DataZoom.ts  | 101 ++++++++++++++---------------
 2 files changed, 51 insertions(+), 54 deletions(-)

diff --git a/src/component/helper/BrushTargetManager.ts b/src/component/helper/BrushTargetManager.ts
index c9e5c6d..40661d6 100644
--- a/src/component/helper/BrushTargetManager.ts
+++ b/src/component/helper/BrushTargetManager.ts
@@ -286,10 +286,10 @@ function formatMinMax(minMax: BrushDimensionMinMax): BrushDimensionMinMax {
 }
 
 function parseFinder(
-    ecModel: GlobalModel, option: ModelFinder
+    ecModel: GlobalModel, finder: ModelFinder
 ): ParsedModelFinder {
     return modelUtilParseFinder(
-        ecModel, option, {includeMainTypes: INCLUDE_FINDER_MAIN_TYPES}
+        ecModel, finder, {includeMainTypes: INCLUDE_FINDER_MAIN_TYPES}
     );
 }
 
diff --git a/src/component/toolbox/feature/DataZoom.ts b/src/component/toolbox/feature/DataZoom.ts
index 108bda1..1e11ed2 100644
--- a/src/component/toolbox/feature/DataZoom.ts
+++ b/src/component/toolbox/feature/DataZoom.ts
@@ -45,11 +45,12 @@ import {
 } from '../../dataZoom/helper';
 import {
     ModelFinderObject, ModelFinderIndexQuery, makeInternalComponentId,
-    queryReferringComponents, ModelFinderIdQuery
+    queryReferringComponents, ModelFinderIdQuery, parseFinder
 } from '../../../util/model';
 import ToolboxModel from '../ToolboxModel';
 import { registerInternalOptionCreator } from '../../../model/internalComponentCreator';
 import Model from '../../../model/Model';
+import ComponentModel from '../../../model/Component';
 
 
 const dataZoomLang = lang.toolbox.dataZoom;
@@ -129,7 +130,9 @@ class DataZoomFeature extends ToolboxFeature<ToolboxDataZoomFeatureOption> {
         this.brushController.updateCovers([]); // remove cover
 
         const brushTargetManager = new BrushTargetManager(
-            retrieveAxisSetting(this.model.option), ecModel, {include: ['grid']}
+            makeAxisFinder(this.model),
+            ecModel,
+            {include: ['grid']}
         );
         brushTargetManager.matchOutputRanges(areas, ecModel, function (area, coordRange, coordSys: Cartesian2D) {
             if (coordSys.type !== 'cartesian2d') {
@@ -235,15 +238,23 @@ const handlers: { [key in IconType]: (this: DataZoomFeature) => void } = {
 };
 
 
-function retrieveAxisSetting(option: ToolboxDataZoomFeatureOption): ModelFinderObject {
-    const setting = {} as ModelFinderObject;
-    // Compatible with previous setting: null => all axis, false => no axis.
-    zrUtil.each(['xAxisIndex', 'yAxisIndex'] as const, function (name) {
-        let val = option[name];
-        val == null && (val = 'all');
-        (val === false || val === 'none') && (val = []);
-        setting[name] = val;
-    });
+function makeAxisFinder(dzFeatureModel: ToolboxDataZoomFeatureModel): ModelFinderObject {
+    const setting = {
+        xAxisIndex: dzFeatureModel.get('xAxisIndex', true),
+        yAxisIndex: dzFeatureModel.get('yAxisIndex', true),
+        xAxisId: dzFeatureModel.get('xAxisId', true),
+        yAxisId: dzFeatureModel.get('yAxisId', true)
+    } as ModelFinderObject;
+
+    // If not specified, it means use all axes.
+    if (setting.xAxisIndex == null
+        && setting.yAxisIndex == null
+        && setting.xAxisId == null
+        && setting.yAxisId == null
+    ) {
+        setting.xAxisIndex = setting.yAxisIndex = 'all';
+    }
+
     return setting;
 }
 
@@ -276,19 +287,23 @@ function updateZoomBtnStatus(
     featureModel.setIconStatus('zoom', zoomActive ? 'emphasis' : 'normal');
 
     const brushTargetManager = new BrushTargetManager(
-        retrieveAxisSetting(featureModel.option), ecModel, {include: ['grid']}
+        makeAxisFinder(featureModel),
+        ecModel,
+        {include: ['grid']}
     );
 
+    const panels = brushTargetManager.makePanelOpts(api, function (targetInfo: BrushTargetInfoCartesian2D) {
+        return (targetInfo.xAxisDeclared && !targetInfo.yAxisDeclared)
+            ? 'lineX'
+            : (!targetInfo.xAxisDeclared && targetInfo.yAxisDeclared)
+            ? 'lineY'
+            : 'rect';
+    });
+
     view.brushController
-        .setPanels(brushTargetManager.makePanelOpts(api, function (targetInfo: BrushTargetInfoCartesian2D) {
-            return (targetInfo.xAxisDeclared && !targetInfo.yAxisDeclared)
-                ? 'lineX'
-                : (!targetInfo.xAxisDeclared && targetInfo.yAxisDeclared)
-                ? 'lineY'
-                : 'rect';
-        }))
+        .setPanels(panels)
         .enableBrush(
-            zoomActive
+            (zoomActive && panels.length)
             ? {
                 brushType: 'auto',
                 brushStyle: {
@@ -308,40 +323,20 @@ registerInternalOptionCreator('dataZoom', function (ecModel: GlobalModel): Compo
     if (!toolboxModel) {
         return;
     }
-    const dzFeatureModel = toolboxModel.getModel(['feature', 'dataZoom'] as any);
+    const dzFeatureModel = toolboxModel.getModel(['feature', 'dataZoom'] as any) as ToolboxDataZoomFeatureModel;
     const dzOptions = [] as ComponentOption[];
-    addInternalOptionForAxis(ecModel, dzOptions, 'x', dzFeatureModel);
-    addInternalOptionForAxis(ecModel, dzOptions, 'y', dzFeatureModel);
 
-    return dzOptions;
-});
+    const finder = makeAxisFinder(dzFeatureModel);
+    const finderResult = parseFinder(ecModel, finder);
 
-function addInternalOptionForAxis(
-    ecModel: GlobalModel,
-    dzOptions: ComponentOption[],
-    axisDim: 'x' | 'y',
-    dzFeatureModel: Model<ToolboxDataZoomFeatureOption>
-): void {
-    const axisIndexPropName = getAxisIndexPropName(axisDim) as 'xAxisIndex' | 'yAxisIndex';
-    const axisIdPropName = getAxisIdPropName(axisDim) as 'xAxisId' | 'yAxisId';
-    const axisMainType = getAxisMainType(axisDim);
-    let axisIndexOption = dzFeatureModel.get(axisIndexPropName, true);
-    const axisIdOption = dzFeatureModel.get(axisIdPropName, true);
-
-    if (axisIndexOption == null && axisIdOption == null) {
-        axisIndexOption = 'all';
-    }
+    each(finderResult.xAxisModels, axisModel => buildInternalOptions(axisModel, 'xAxis', 'xAxisIndex'));
+    each(finderResult.yAxisModels, axisModel => buildInternalOptions(axisModel, 'yAxis', 'yAxisIndex'));
 
-    const queryResult = queryReferringComponents(
-        ecModel,
-        axisMainType,
-        {
-            index: axisIndexOption,
-            id: axisIdOption
-        }
-    );
-
-    each(queryResult.models, function (axisModel) {
+    function buildInternalOptions(
+        axisModel: ComponentModel,
+        axisMainType: 'xAxis' | 'yAxis',
+        axisIndexPropName: 'xAxisIndex' | 'yAxisIndex'
+    ) {
         const axisIndex = axisModel.componentIndex;
         const newOpt = {
             type: 'select',
@@ -354,8 +349,10 @@ function addInternalOptionForAxis(
         newOpt[axisIndexPropName] = axisIndex;
 
         dzOptions.push(newOpt);
-    });
-}
+    }
+
+    return dzOptions;
+});
 
 
 export default DataZoomFeature;


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


[incubator-echarts] 07/16: fix: fix type and tweak component.getReferingComponent.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit af079802ecd260265bd9ca5575726442615ff82f
Author: 100pah <su...@gmail.com>
AuthorDate: Tue Jul 14 17:30:37 2020 +0800

    fix: fix type and tweak component.getReferingComponent.
---
 src/chart/themeRiver/ThemeRiverSeries.ts   |  2 +-
 src/coord/cartesian/Grid.ts                |  4 +--
 src/coord/cartesian/cartesianAxisHelper.ts |  2 +-
 src/coord/geo/Geo.ts                       |  2 +-
 src/echarts.ts                             |  2 +-
 src/model/Component.ts                     | 41 ++++++++++++++++++++++++++----
 src/model/referHelper.ts                   |  8 +++---
 src/util/model.ts                          | 28 ++++++++++++++++----
 8 files changed, 69 insertions(+), 20 deletions(-)

diff --git a/src/chart/themeRiver/ThemeRiverSeries.ts b/src/chart/themeRiver/ThemeRiverSeries.ts
index 5e28bf2..3f0cc49 100644
--- a/src/chart/themeRiver/ThemeRiverSeries.ts
+++ b/src/chart/themeRiver/ThemeRiverSeries.ts
@@ -83,7 +83,7 @@ class ThemeRiverSeriesModel extends SeriesModel<ThemeRiverSeriesOption> {
 
     static readonly dependencies = ['singleAxis'];
 
-    nameMap: zrUtil.HashMap<number>;
+    nameMap: zrUtil.HashMap<number, string>;
 
     coordinateSystem: Single;
 
diff --git a/src/coord/cartesian/Grid.ts b/src/coord/cartesian/Grid.ts
index 6c803f9..91e540b 100644
--- a/src/coord/cartesian/Grid.ts
+++ b/src/coord/cartesian/Grid.ts
@@ -254,9 +254,9 @@ class Grid implements CoordinateSystemMaster {
     } {
         const seriesModel = finder.seriesModel;
         const xAxisModel = finder.xAxisModel
-            || (seriesModel && seriesModel.getReferringComponents('xAxis')[0]);
+            || (seriesModel && seriesModel.getReferringComponents('xAxis', true).models[0]);
         const yAxisModel = finder.yAxisModel
-            || (seriesModel && seriesModel.getReferringComponents('yAxis')[0]);
+            || (seriesModel && seriesModel.getReferringComponents('yAxis', true).models[0]);
         const gridModel = finder.gridModel;
         const coordsList = this._coordsList;
         let cartesian: Cartesian2D;
diff --git a/src/coord/cartesian/cartesianAxisHelper.ts b/src/coord/cartesian/cartesianAxisHelper.ts
index f732f36..badc476 100644
--- a/src/coord/cartesian/cartesianAxisHelper.ts
+++ b/src/coord/cartesian/cartesianAxisHelper.ts
@@ -112,7 +112,7 @@ export function findAxisModels(seriesModel: SeriesModel): {
     } as ReturnType<typeof findAxisModels>;
     zrUtil.each(axisModelMap, function (v, key) {
         const axisType = key.replace(/Model$/, '');
-        const axisModel = seriesModel.getReferringComponents(axisType)[0] as CartesianAxisModel;
+        const axisModel = seriesModel.getReferringComponents(axisType, true).models[0] as CartesianAxisModel;
 
         if (__DEV__) {
             if (!axisModel) {
diff --git a/src/coord/geo/Geo.ts b/src/coord/geo/Geo.ts
index 9627ba1..df84083 100644
--- a/src/coord/geo/Geo.ts
+++ b/src/coord/geo/Geo.ts
@@ -173,7 +173,7 @@ function getCoordSys(finder: ParsedModelFinder): Geo {
         : seriesModel
         ? (
             seriesModel.coordinateSystem as Geo // For map series.
-            || ((seriesModel.getReferringComponents('geo')[0] || {}) as GeoModel).coordinateSystem
+            || ((seriesModel.getReferringComponents('geo', true).models[0] || {}) as GeoModel).coordinateSystem
         )
         : null;
 }
diff --git a/src/echarts.ts b/src/echarts.ts
index 581bf53..63a2100 100644
--- a/src/echarts.ts
+++ b/src/echarts.ts
@@ -1298,7 +1298,7 @@ class ECharts extends Eventful {
             subType && (condition.subType = subType); // subType may be '' by parseClassType;
 
             const excludeSeriesId = payload.excludeSeriesId;
-            let excludeSeriesIdMap: zrUtil.HashMap<string[]>;
+            let excludeSeriesIdMap: zrUtil.HashMap<string[], string>;
             if (excludeSeriesId != null) {
                 excludeSeriesIdMap = zrUtil.createHashMap(modelUtil.normalizeToArray(excludeSeriesId));
             }
diff --git a/src/model/Component.ts b/src/model/Component.ts
index 74fccf0..2e1b1a8 100644
--- a/src/model/Component.ts
+++ b/src/model/Component.ts
@@ -177,7 +177,9 @@ class ComponentModel<Opt extends ComponentOption = ComponentOption> extends Mode
         }
     }
 
-    // Hooker after init or mergeOption
+    /**
+     * Called immediately after `init` or `mergeOption` of this instance called.
+     */
     optionUpdated(newCptOption: Opt, isInit: boolean): void {}
 
     /**
@@ -265,14 +267,43 @@ class ComponentModel<Opt extends ComponentOption = ComponentOption> extends Mode
         return fields.defaultOption as Opt;
     }
 
-    getReferringComponents(mainType: ComponentMainType): ComponentModel[] {
+    /**
+     * Notice: always force to input param `useDefault` in case that forget to consider it.
+     *
+     * @param useDefault In many cases like series refer axis and axis refer grid,
+     *        If axis index / axis id not specified, use the first target as default.
+     *        In other cases like dataZoom refer axis, if not specified, measn no refer.
+     */
+    getReferringComponents(mainType: ComponentMainType, useDefault: boolean): {
+        // Always be array rather than null/undefined, which is convenient to use.
+        models: ComponentModel[],
+        // Whether index or id are specified in option.
+        specified: boolean
+    } {
         const indexKey = (mainType + 'Index') as keyof Opt;
         const idKey = (mainType + 'Id') as keyof Opt;
-        return this.ecModel.queryComponents({
+        const indexOption = this.get(indexKey, true);
+        const idOption = this.get(idKey, true);
+
+        const models = this.ecModel.queryComponents({
             mainType: mainType,
-            index: this.get(indexKey, true) as unknown as number,
-            id: this.get(idKey, true) as unknown as string
+            index: indexOption as any,
+            id: idOption as any
         });
+
+        // `queryComponents` will return all components if
+        // both index and id are null/undefined
+        let specified = true;
+        if (indexOption == null && idOption == null) {
+            specified = false;
+            // Use the first as default if `useDefault`.
+            models.length = (useDefault && models.length) ? 1 : 0;
+        }
+
+        return {
+            models: models,
+            specified: specified
+        };
     }
 
     getBoxLayoutParams() {
diff --git a/src/model/referHelper.ts b/src/model/referHelper.ts
index 287eec5..0f6d46c 100644
--- a/src/model/referHelper.ts
+++ b/src/model/referHelper.ts
@@ -94,8 +94,8 @@ const fetchers: Record<SupportedCoordSys, Fetcher> = {
     cartesian2d: function (
         seriesModel: SeriesModel<SeriesOption & SeriesOnCartesianOptionMixin>, result, axisMap, categoryAxisMap
     ) {
-        const xAxisModel = seriesModel.getReferringComponents('xAxis')[0] as AxisBaseModel;
-        const yAxisModel = seriesModel.getReferringComponents('yAxis')[0] as AxisBaseModel;
+        const xAxisModel = seriesModel.getReferringComponents('xAxis', true).models[0] as AxisBaseModel;
+        const yAxisModel = seriesModel.getReferringComponents('yAxis', true).models[0] as AxisBaseModel;
 
         if (__DEV__) {
             if (!xAxisModel) {
@@ -129,7 +129,7 @@ const fetchers: Record<SupportedCoordSys, Fetcher> = {
     },
 
     singleAxis: function (seriesModel, result, axisMap, categoryAxisMap) {
-        const singleAxisModel = seriesModel.getReferringComponents('singleAxis')[0] as AxisBaseModel;
+        const singleAxisModel = seriesModel.getReferringComponents('singleAxis', true).models[0] as AxisBaseModel;
 
         if (__DEV__) {
             if (!singleAxisModel) {
@@ -147,7 +147,7 @@ const fetchers: Record<SupportedCoordSys, Fetcher> = {
     },
 
     polar: function (seriesModel, result, axisMap, categoryAxisMap) {
-        const polarModel = seriesModel.getReferringComponents('polar')[0] as PolarModel;
+        const polarModel = seriesModel.getReferringComponents('polar', true).models[0] as PolarModel;
         const radiusAxisModel = polarModel.findAxisModel('radiusAxis');
         const angleAxisModel = polarModel.findAxisModel('angleAxis');
 
diff --git a/src/util/model.ts b/src/util/model.ts
index 826c23f..7e21a2e 100644
--- a/src/util/model.ts
+++ b/src/util/model.ts
@@ -47,6 +47,7 @@ import SeriesModel from '../model/Series';
 import CartesianAxisModel from '../coord/cartesian/AxisModel';
 import GridModel from '../coord/cartesian/GridModel';
 import { __DEV__ } from '../config';
+import { isNumeric } from './number';
 
 /**
  * Make the name displayable. But we should
@@ -219,6 +220,9 @@ function mappingToExistsInNormalMerge<T extends MappingExistingItem>(
             return;
         }
 
+        cmptOption.id == null || validateIdOrName(cmptOption.id);
+        cmptOption.name == null || validateIdOrName(cmptOption.name);
+
         // id has highest priority.
         for (let i = 0; i < result.length; i++) {
             const existing = result[i].existing;
@@ -304,9 +308,10 @@ function mappingToExistsInReplaceMerge<T extends MappingExistingItem>(
             newCmptOptions[index] = null;
             return;
         }
-        if (cmptOption.id == null) {
-            return;
-        }
+
+        cmptOption.id == null || validateIdOrName(cmptOption.id);
+        cmptOption.name == null || validateIdOrName(cmptOption.name);
+
         const optionId = makeComparableKey(cmptOption.id);
         const existingIdx = existingIdIdxMap.get(optionId);
         if (existingIdx != null) {
@@ -482,6 +487,19 @@ function makeComparableKey(val: string | number): string {
     return val + '';
 }
 
+export function validateIdOrName(idOrName: unknown) {
+    if (__DEV__) {
+        assert(
+            isValidIdOrName(idOrName),
+            '`' + idOrName + '` is invalid id or name. Must be a string.'
+        );
+    }
+}
+
+function isValidIdOrName(idOrName: unknown): boolean {
+    return isString(idOrName) || isNumeric(idOrName);
+}
+
 export function isNameSpecified(componentModel: ComponentModel): boolean {
     const name = componentModel.name;
     // Is specified when `indexOf` get -1 or > 0.
@@ -810,9 +828,9 @@ export function groupData<T, R extends string | number>(
     getKey: (item: T) => R // return key
 ): {
     keys: R[],
-    buckets: HashMap<T[]> // hasmap key: the key returned by `getKey`.
+    buckets: HashMap<T[], R> // hasmap key: the key returned by `getKey`.
 } {
-    const buckets = createHashMap<T[]>();
+    const buckets = createHashMap<T[], R>();
     const keys: R[] = [];
 
     each(array, function (item) {


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


[incubator-echarts] 16/16: Merge branch 'next' into remove-component

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 31544592351e68c9a1eb86e5aa9f31e7a06c1e1e
Merge: 548bf37 446f96d
Author: 100pah <su...@gmail.com>
AuthorDate: Fri Jul 17 19:29:57 2020 +0800

    Merge branch 'next' into remove-component

 src/chart/line/LineView.ts               |  6 +++
 src/chart/map/MapView.ts                 | 67 +++++++------------------
 src/chart/parallel/ParallelView.ts       |  8 ++-
 src/chart/sankey/SankeyView.ts           |  4 +-
 src/component/axis/ParallelAxisView.ts   |  3 +-
 src/component/axis/parallelAxisAction.ts |  2 -
 src/component/dataZoom/SliderZoomView.ts |  4 ++
 src/component/dataZoom/roams.ts          |  4 ++
 src/component/helper/MapDraw.ts          | 21 ++++----
 src/component/parallel.ts                |  4 +-
 src/coord/radar/RadarModel.ts            |  6 +--
 src/echarts.ts                           | 18 ++++++-
 src/label/LabelManager.ts                |  5 +-
 src/model/Global.ts                      | 13 +++++
 src/util/graphic.ts                      | 84 +++++++++++++++++++-------------
 src/util/types.ts                        | 13 +++++
 16 files changed, 152 insertions(+), 110 deletions(-)

diff --cc src/model/Global.ts
index 6328032,d0cf090..9073aab
--- a/src/model/Global.ts
+++ b/src/model/Global.ts
@@@ -412,23 -323,21 +417,31 @@@ class GlobalModel extends Model<ECUnitO
          return this._theme;
      }
  
+     setUpdatePayload(payload: Payload) {
+         this._payload = payload;
+     }
+ 
+     getUpdatePayload(): Payload {
+         return this._payload;
+     }
+ 
      /**
 -     * @param idx 0 by default
 +     * @param idx If not specified, return the first one.
       */
 -    getComponent(mainType: string, idx?: number): ComponentModel {
 +    getComponent(mainType: ComponentMainType, idx?: number): ComponentModel {
          const list = this._componentsMap.get(mainType);
          if (list) {
 -            return list[idx || 0];
 +            const cmpt = list[idx || 0];
 +            if (cmpt) {
 +                return cmpt;
 +            }
 +            else if (idx == null) {
 +                for (let i = 0; i < list.length; i++) {
 +                    if (list[i]) {
 +                        return list[i];
 +                    }
 +                }
 +            }
          }
      }
  


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


[incubator-echarts] 10/16: feature: (1) support toolbox dataZoom works on the second setOption. (2) change the mechanism of "internal component" to support adding them dynamically. (3) uniform the "get referring component". (4) support toolbox dataZoom use axis id to refer axis (previously only axisIndex can be used). (5) remove the support to restore on the second setOption temporarily.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit cc81a49b6db902dae376257140da921f762f6505
Author: 100pah <su...@gmail.com>
AuthorDate: Wed Jul 15 16:39:00 2020 +0800

    feature:
    (1) support toolbox dataZoom works on the second setOption.
    (2) change the mechanism of "internal component" to support adding them dynamically.
    (3) uniform the "get referring component".
    (4) support toolbox dataZoom use axis id to refer axis (previously only axisIndex can be used).
    (5) remove the support to restore on the second setOption temporarily.
---
 src/component/dataZoom/DataZoomModel.ts   |   2 +-
 src/component/dataZoom/helper.ts          |   6 +-
 src/component/toolbox/feature/DataZoom.ts | 145 ++++++------
 src/model/Component.ts                    |  42 ++--
 src/model/Global.ts                       |  25 ++-
 src/model/OptionManager.ts                | 165 +++++++-------
 src/model/internalComponentCreator.ts     |  75 +++++++
 src/util/model.ts                         | 353 ++++++++++++++++--------------
 test/dataZoom-feature.html                | 145 +++++++++++-
 test/dataZoom-toolbox.html                |  86 ++++++++
 10 files changed, 692 insertions(+), 352 deletions(-)

diff --git a/src/component/dataZoom/DataZoomModel.ts b/src/component/dataZoom/DataZoomModel.ts
index d1fadbe..db5c440 100644
--- a/src/component/dataZoom/DataZoomModel.ts
+++ b/src/component/dataZoom/DataZoomModel.ts
@@ -150,7 +150,7 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
     type = DataZoomModel.type;
 
     static dependencies = [
-        'xAxis', 'yAxis', 'radiusAxis', 'angleAxis', 'singleAxis', 'series'
+        'xAxis', 'yAxis', 'radiusAxis', 'angleAxis', 'singleAxis', 'series', 'toolbox'
     ];
 
 
diff --git a/src/component/dataZoom/helper.ts b/src/component/dataZoom/helper.ts
index d36a783..7def11e 100644
--- a/src/component/dataZoom/helper.ts
+++ b/src/component/dataZoom/helper.ts
@@ -52,21 +52,21 @@ export function isCoordSupported(coordType: string) {
     return indexOf(COORDS, coordType) >= 0;
 }
 
-export function getAxisMainType(axisDim: DimensionName): DataZoomAxisMainType {
+export function getAxisMainType(axisDim: DataZoomAxisDimension): DataZoomAxisMainType {
     if (__DEV__) {
         assert(axisDim);
     }
     return axisDim + 'Axis' as DataZoomAxisMainType;
 }
 
-export function getAxisIndexPropName(axisDim: DimensionName): DataZoomAxisIndexPropName {
+export function getAxisIndexPropName(axisDim: DataZoomAxisDimension): DataZoomAxisIndexPropName {
     if (__DEV__) {
         assert(axisDim);
     }
     return axisDim + 'AxisIndex' as DataZoomAxisIndexPropName;
 }
 
-export function getAxisIdPropName(axisDim: DimensionName): DataZoomAxisIdPropName {
+export function getAxisIdPropName(axisDim: DataZoomAxisDimension): DataZoomAxisIdPropName {
     if (__DEV__) {
         assert(axisDim);
     }
diff --git a/src/component/toolbox/feature/DataZoom.ts b/src/component/toolbox/feature/DataZoom.ts
index ec2bed2..108bda1 100644
--- a/src/component/toolbox/feature/DataZoom.ts
+++ b/src/component/toolbox/feature/DataZoom.ts
@@ -19,7 +19,6 @@
 
 
 // TODO depends on DataZoom and Brush
-import * as echarts from '../../../echarts';
 import * as zrUtil from 'zrender/src/core/util';
 import BrushController, { BrushControllerEvents, BrushDimensionMinMax } from '../../helper/BrushController';
 import BrushTargetManager, { BrushTargetInfoCartesian2D } from '../../helper/BrushTargetManager';
@@ -36,19 +35,27 @@ import {
 } from '../featureManager';
 import GlobalModel from '../../../model/Global';
 import ExtensionAPI from '../../../ExtensionAPI';
-import { Payload, ECUnitOption, Dictionary } from '../../../util/types';
+import { Payload, Dictionary, ComponentOption } from '../../../util/types';
 import Cartesian2D from '../../../coord/cartesian/Cartesian2D';
 import CartesianAxisModel from '../../../coord/cartesian/AxisModel';
-import DataZoomModel, { DataZoomOption } from '../../dataZoom/DataZoomModel';
-import { DataZoomPayloadBatchItem } from '../../dataZoom/helper';
-import { ModelFinderObject, ModelFinderIndexQuery, normalizeToArray } from '../../../util/model';
-import { ToolboxOption } from '../ToolboxModel';
+import DataZoomModel from '../../dataZoom/DataZoomModel';
+import {
+    DataZoomPayloadBatchItem, DataZoomAxisDimension, getAxisIndexPropName,
+    getAxisIdPropName, getAxisMainType
+} from '../../dataZoom/helper';
+import {
+    ModelFinderObject, ModelFinderIndexQuery, makeInternalComponentId,
+    queryReferringComponents, ModelFinderIdQuery
+} from '../../../util/model';
+import ToolboxModel from '../ToolboxModel';
+import { registerInternalOptionCreator } from '../../../model/internalComponentCreator';
+import Model from '../../../model/Model';
+
 
 const dataZoomLang = lang.toolbox.dataZoom;
 const each = zrUtil.each;
 
-// Spectial component id start with \0ec\0, see echarts/model/Global.js~hasInnerId
-const DATA_ZOOM_ID_BASE = '\0_ec_\0toolbox-dataZoom_';
+const DATA_ZOOM_ID_BASE = makeInternalComponentId('toolbox-dataZoom_');
 
 const ICON_TYPES = ['zoom', 'back'] as const;
 type IconType = typeof ICON_TYPES[number];
@@ -60,8 +67,10 @@ interface ToolboxDataZoomFeatureOption extends ToolboxFeatureOption {
     // TODO: TYPE Use type in dataZoom
     filterMode?: 'filter' | 'weakFilter' | 'empty' | 'none'
     // Backward compat: false means 'none'
-    xAxisIndex?: ModelFinderIndexQuery | false
-    yAxisIndex?: ModelFinderIndexQuery | false
+    xAxisIndex?: ModelFinderIndexQuery
+    yAxisIndex?: ModelFinderIndexQuery
+    xAxisId?: ModelFinderIdQuery
+    yAxisId?: ModelFinderIdQuery
 }
 
 type ToolboxDataZoomFeatureModel = ToolboxFeatureModel<ToolboxDataZoomFeatureOption>;
@@ -134,7 +143,7 @@ class DataZoomFeature extends ToolboxFeature<ToolboxDataZoomFeatureOption> {
             }
             else {
                 setBatch(
-                    ({lineX: 'x', lineY: 'y'})[brushType as 'lineX' | 'lineY'],
+                    ({lineX: 'x', lineY: 'y'} as const)[brushType as 'lineX' | 'lineY'],
                     coordSys,
                     coordRange as BrushDimensionMinMax
                 );
@@ -145,7 +154,7 @@ class DataZoomFeature extends ToolboxFeature<ToolboxDataZoomFeatureOption> {
 
         this._dispatchZoomAction(snapshot);
 
-        function setBatch(dimName: string, coordSys: Cartesian2D, minMax: number[]) {
+        function setBatch(dimName: DataZoomAxisDimension, coordSys: Cartesian2D, minMax: number[]) {
             const axis = coordSys.getAxis(dimName);
             const axisModel = axis.model;
             const dataZoomModel = findDataZoom(dimName, axisModel, ecModel);
@@ -166,7 +175,9 @@ class DataZoomFeature extends ToolboxFeature<ToolboxDataZoomFeatureOption> {
             });
         }
 
-        function findDataZoom(dimName: string, axisModel: CartesianAxisModel, ecModel: GlobalModel): DataZoomModel {
+        function findDataZoom(
+            dimName: DataZoomAxisDimension, axisModel: CartesianAxisModel, ecModel: GlobalModel
+        ): DataZoomModel {
             let found;
             ecModel.eachComponent({mainType: 'dataZoom', subType: 'select'}, function (dzModel: DataZoomModel) {
                 const has = dzModel.getAxisModel(dimName, axisModel.componentIndex);
@@ -290,79 +301,61 @@ function updateZoomBtnStatus(
         );
 }
 
-
 registerFeature('dataZoom', DataZoomFeature);
 
-
-// Create special dataZoom option for select
-// FIXME consider the case of merge option, where axes options are not exists.
-echarts.registerPreprocessor(function (option: ECUnitOption) {
-    if (!option) {
+registerInternalOptionCreator('dataZoom', function (ecModel: GlobalModel): ComponentOption[] {
+    const toolboxModel = ecModel.getComponent('toolbox', 0) as ToolboxModel;
+    if (!toolboxModel) {
         return;
     }
+    const dzFeatureModel = toolboxModel.getModel(['feature', 'dataZoom'] as any);
+    const dzOptions = [] as ComponentOption[];
+    addInternalOptionForAxis(ecModel, dzOptions, 'x', dzFeatureModel);
+    addInternalOptionForAxis(ecModel, dzOptions, 'y', dzFeatureModel);
 
-    const dataZoomOpts = option.dataZoom = normalizeToArray(option.dataZoom) as DataZoomOption[];
-
-    let toolboxOpt = option.toolbox as ToolboxOption;
-    if (toolboxOpt) {
-        // Assume there is only one toolbox
-        if (zrUtil.isArray(toolboxOpt)) {
-            toolboxOpt = toolboxOpt[0];
-        }
+    return dzOptions;
+});
 
-        if (toolboxOpt && toolboxOpt.feature) {
-            const dataZoomOpt = toolboxOpt.feature.dataZoom as ToolboxDataZoomFeatureOption;
-            // FIXME: If add dataZoom when setOption in merge mode,
-            // no axis info to be added. See `test/dataZoom-extreme.html`
-            addForAxis('xAxis', dataZoomOpt);
-            addForAxis('yAxis', dataZoomOpt);
-        }
+function addInternalOptionForAxis(
+    ecModel: GlobalModel,
+    dzOptions: ComponentOption[],
+    axisDim: 'x' | 'y',
+    dzFeatureModel: Model<ToolboxDataZoomFeatureOption>
+): void {
+    const axisIndexPropName = getAxisIndexPropName(axisDim) as 'xAxisIndex' | 'yAxisIndex';
+    const axisIdPropName = getAxisIdPropName(axisDim) as 'xAxisId' | 'yAxisId';
+    const axisMainType = getAxisMainType(axisDim);
+    let axisIndexOption = dzFeatureModel.get(axisIndexPropName, true);
+    const axisIdOption = dzFeatureModel.get(axisIdPropName, true);
+
+    if (axisIndexOption == null && axisIdOption == null) {
+        axisIndexOption = 'all';
     }
 
-    function addForAxis(axisName: 'xAxis' | 'yAxis', dataZoomOpt: ToolboxDataZoomFeatureOption): void {
-        if (!dataZoomOpt) {
-            return;
-        }
-
-        // Try not to modify model, because it is not merged yet.
-        const axisIndicesName = axisName + 'Index' as 'xAxisIndex' | 'yAxisIndex';
-        let givenAxisIndices = dataZoomOpt[axisIndicesName];
-        if (givenAxisIndices != null
-            && givenAxisIndices !== 'all'
-            && !zrUtil.isArray(givenAxisIndices)
-        ) {
-            givenAxisIndices = (givenAxisIndices === false || givenAxisIndices === 'none') ? [] : [givenAxisIndices];
+    const queryResult = queryReferringComponents(
+        ecModel,
+        axisMainType,
+        {
+            index: axisIndexOption,
+            id: axisIdOption
         }
+    );
 
-        forEachComponent(axisName, function (axisOpt: unknown, axisIndex: number) {
-            if (givenAxisIndices != null
-                && givenAxisIndices !== 'all'
-                && zrUtil.indexOf(givenAxisIndices as number[], axisIndex) === -1
-            ) {
-                return;
-            }
-            const newOpt = {
-                type: 'select',
-                $fromToolbox: true,
-                // Default to be filter
-                filterMode: dataZoomOpt.filterMode || 'filter',
-                // Id for merge mapping.
-                id: DATA_ZOOM_ID_BASE + axisName + axisIndex
-            } as Dictionary<unknown>;
-            // FIXME
-            // Only support one axis now.
-            newOpt[axisIndicesName] = axisIndex;
-            dataZoomOpts.push(newOpt);
-        });
-    }
+    each(queryResult.models, function (axisModel) {
+        const axisIndex = axisModel.componentIndex;
+        const newOpt = {
+            type: 'select',
+            $fromToolbox: true,
+            // Default to be filter
+            filterMode: dzFeatureModel.get('filterMode', true) || 'filter',
+            // Id for merge mapping.
+            id: DATA_ZOOM_ID_BASE + axisMainType + axisIndex
+        } as Dictionary<unknown>;
+        newOpt[axisIndexPropName] = axisIndex;
+
+        dzOptions.push(newOpt);
+    });
+}
 
-    function forEachComponent(mainType: string, cb: (axisOpt: unknown, axisIndex: number) => void) {
-        let opts = option[mainType];
-        if (!zrUtil.isArray(opts)) {
-            opts = opts ? [opts] : [];
-        }
-        each(opts, cb);
-    }
-});
 
 export default DataZoomFeature;
diff --git a/src/model/Component.ts b/src/model/Component.ts
index 2e1b1a8..f3bfc6d 100644
--- a/src/model/Component.ts
+++ b/src/model/Component.ts
@@ -29,7 +29,7 @@ import {
     ClassManager,
     mountExtend
 } from '../util/clazz';
-import {makeInner} from '../util/model';
+import {makeInner, ModelFinderIndexQuery, queryReferringComponents, ModelFinderIdQuery} from '../util/model';
 import * as layout from '../util/layout';
 import GlobalModel from './Global';
 import {
@@ -45,6 +45,7 @@ const inner = makeInner<{
     defaultOption: ComponentOption
 }, ComponentModel>();
 
+
 class ComponentModel<Opt extends ComponentOption = ComponentOption> extends Model<Opt> {
 
     // [Caution]: Becuase this class or desecendants can be used as `XXX.extend(subProto)`,
@@ -269,6 +270,7 @@ class ComponentModel<Opt extends ComponentOption = ComponentOption> extends Mode
 
     /**
      * Notice: always force to input param `useDefault` in case that forget to consider it.
+     * The same behavior as `modelUtil.parseFinder`.
      *
      * @param useDefault In many cases like series refer axis and axis refer grid,
      *        If axis index / axis id not specified, use the first target as default.
@@ -276,34 +278,22 @@ class ComponentModel<Opt extends ComponentOption = ComponentOption> extends Mode
      */
     getReferringComponents(mainType: ComponentMainType, useDefault: boolean): {
         // Always be array rather than null/undefined, which is convenient to use.
-        models: ComponentModel[],
-        // Whether index or id are specified in option.
-        specified: boolean
+        models: ComponentModel[];
+        // Whether target compoent specified
+        specified: boolean;
     } {
         const indexKey = (mainType + 'Index') as keyof Opt;
         const idKey = (mainType + 'Id') as keyof Opt;
-        const indexOption = this.get(indexKey, true);
-        const idOption = this.get(idKey, true);
-
-        const models = this.ecModel.queryComponents({
-            mainType: mainType,
-            index: indexOption as any,
-            id: idOption as any
-        });
-
-        // `queryComponents` will return all components if
-        // both index and id are null/undefined
-        let specified = true;
-        if (indexOption == null && idOption == null) {
-            specified = false;
-            // Use the first as default if `useDefault`.
-            models.length = (useDefault && models.length) ? 1 : 0;
-        }
 
-        return {
-            models: models,
-            specified: specified
-        };
+        return queryReferringComponents(
+            this.ecModel,
+            mainType,
+            {
+                index: this.get(indexKey, true) as unknown as ModelFinderIndexQuery,
+                id: this.get(idKey, true) as unknown as ModelFinderIdQuery
+            },
+            useDefault
+        );
     }
 
     getBoxLayoutParams() {
@@ -324,6 +314,7 @@ class ComponentModel<Opt extends ComponentOption = ComponentOption> extends Mode
     static hasClass: ClassManager['hasClass'];
 
     static registerSubTypeDefaulter: componentUtil.SubTypeDefaulterManager['registerSubTypeDefaulter'];
+
 }
 
 // Reset ComponentModel.extend, add preConstruct.
@@ -373,4 +364,5 @@ function getDependencies(componentType: string): string[] {
     return deps;
 }
 
+
 export default ComponentModel;
diff --git a/src/model/Global.ts b/src/model/Global.ts
index 5fd20d9..93b674d 100644
--- a/src/model/Global.ts
+++ b/src/model/Global.ts
@@ -57,6 +57,7 @@ import {
 } from '../util/types';
 import OptionManager from './OptionManager';
 import Scheduler from '../stream/Scheduler';
+import { concatInternalOptions } from './internalComponentCreator';
 
 export interface GlobalModelSetOptionOpts {
     replaceMerge: ComponentMainType | ComponentMainType[];
@@ -247,7 +248,7 @@ class GlobalModel extends Model<ECUnitOption> {
             // we trade it as it is declared in option as `{xxx: []}`. Because:
             // (1) for normal merge, `{xxx: null/undefined}` are the same meaning as `{xxx: []}`.
             // (2) some preprocessor may convert some of `{xxx: null/undefined}` to `{xxx: []}`.
-            replaceMergeMainTypeMap.each(function (b, mainTypeInReplaceMerge) {
+            replaceMergeMainTypeMap.each(function (val, mainTypeInReplaceMerge) {
                 if (!newCmptTypeMap.get(mainTypeInReplaceMerge)) {
                     newCmptTypes.push(mainTypeInReplaceMerge);
                     newCmptTypeMap.set(mainTypeInReplaceMerge, true);
@@ -266,7 +267,9 @@ class GlobalModel extends Model<ECUnitOption> {
             this: GlobalModel,
             mainType: ComponentMainType
         ): void {
-            const newCmptOptionList = modelUtil.normalizeToArray(newOption[mainType]);
+            const newCmptOptionList = concatInternalOptions(
+                this, mainType, modelUtil.normalizeToArray(newOption[mainType])
+            );
 
             const oldCmptList = componentsMap.get(mainType);
             const mergeMode = (replaceMergeMainTypeMap && replaceMergeMainTypeMap.get(mainType))
@@ -407,12 +410,22 @@ class GlobalModel extends Model<ECUnitOption> {
     }
 
     /**
-     * @param idx 0 by default
+     * @param idx If not specified, return the first one.
      */
-    getComponent(mainType: string, idx?: number): ComponentModel {
+    getComponent(mainType: ComponentMainType, idx?: number): ComponentModel {
         const list = this._componentsMap.get(mainType);
         if (list) {
-            return list[idx || 0];
+            const cmpt = list[idx || 0];
+            if (cmpt) {
+                return cmpt;
+            }
+            else if (idx == null) {
+                for (let i = 0; i < list.length; i++) {
+                    if (list[i]) {
+                        return list[i];
+                    }
+                }
+            }
         }
     }
 
@@ -862,7 +875,7 @@ function queryByIdOrName<T extends { id?: string, name?: string }>(
     idOrName: string | number | (string | number)[],
     cmpts: T[]
 ): T[] {
-    // Here is a break from echarts4: string and number-like string are
+    // Here is a break from echarts4: string and number are
     // traded as equal.
     if (isArray(idOrName)) {
         const keyMap = createHashMap<boolean>(idOrName);
diff --git a/src/model/OptionManager.ts b/src/model/OptionManager.ts
index fd4a6fc..17d3767 100644
--- a/src/model/OptionManager.ts
+++ b/src/model/OptionManager.ts
@@ -69,7 +69,7 @@ class OptionManager {
 
     private _optionBackup: ParsedRawOption;
 
-    private _fakeCmptsMap: FakeComponentsMap;
+    // private _fakeCmptsMap: FakeComponentsMap;
 
     private _newBaseOption: ECUnitOption;
 
@@ -115,16 +115,21 @@ class OptionManager {
 
         // For setOption at second time (using merge mode);
         if (optionBackup) {
+            // FIXME
+            // the restore merge solution is essentially incorrect.
+            // the mapping can not be 100% consistent with ecModel, which probably brings
+            // potential bug!
+
             // The first merge is delayed, becuase in most cases, users do not call `setOption` twice.
-            let fakeCmptsMap = this._fakeCmptsMap;
-            if (!fakeCmptsMap) {
-                fakeCmptsMap = this._fakeCmptsMap = createHashMap();
-                mergeToBackupOption(fakeCmptsMap, null, optionBackup.baseOption, null);
-            }
+            // let fakeCmptsMap = this._fakeCmptsMap;
+            // if (!fakeCmptsMap) {
+            //     fakeCmptsMap = this._fakeCmptsMap = createHashMap();
+            //     mergeToBackupOption(fakeCmptsMap, null, optionBackup.baseOption, null);
+            // }
 
-            mergeToBackupOption(
-                fakeCmptsMap, optionBackup.baseOption, newParsedOption.baseOption, opt
-            );
+            // mergeToBackupOption(
+            //     fakeCmptsMap, optionBackup.baseOption, newParsedOption.baseOption, opt
+            // );
 
             // For simplicity, timeline options and media options do not support merge,
             // that is, if you `setOption` twice and both has timeline options, the latter
@@ -373,76 +378,76 @@ function indicesEquals(indices1: number[], indices2: number[]): boolean {
  * When "resotre" action triggered, model from `componentActionModel` will be discarded
  * instead of recreating the "ecModel" from the "_optionBackup".
  */
-function mergeToBackupOption(
-    fakeCmptsMap: FakeComponentsMap,
-    // `tarOption` Can be null/undefined, means init
-    tarOption: ECUnitOption,
-    newOption: ECUnitOption,
-    // Can be null/undefined
-    opt: InnerSetOptionOpts
-): void {
-    newOption = newOption || {} as ECUnitOption;
-    const notInit = !!tarOption;
-
-    each(newOption, function (newOptsInMainType, mainType) {
-        if (newOptsInMainType == null) {
-            return;
-        }
-
-        if (!ComponentModel.hasClass(mainType)) {
-            if (tarOption) {
-                tarOption[mainType] = merge(tarOption[mainType], newOptsInMainType, true);
-            }
-        }
-        else {
-            const oldTarOptsInMainType = notInit ? normalizeToArray(tarOption[mainType]) : null;
-            const oldFakeCmptsInMainType = fakeCmptsMap.get(mainType) || [];
-            const resultTarOptsInMainType = notInit ? (tarOption[mainType] = [] as ComponentOption[]) : null;
-            const resultFakeCmptsInMainType = fakeCmptsMap.set(mainType, []);
-
-            const mappingResult = mappingToExists(
-                oldFakeCmptsInMainType,
-                normalizeToArray(newOptsInMainType),
-                (opt && opt.replaceMergeMainTypeMap.get(mainType)) ? 'replaceMerge' : 'normalMerge'
-            );
-            setComponentTypeToKeyInfo(mappingResult, mainType, ComponentModel as ComponentModelConstructor);
-
-            each(mappingResult, function (resultItem, index) {
-                // The same logic as `Global.ts#_mergeOption`.
-                let fakeCmpt = resultItem.existing;
-                const newOption = resultItem.newOption;
-                const keyInfo = resultItem.keyInfo;
-                let fakeCmptOpt;
-
-                if (!newOption) {
-                    fakeCmptOpt = oldTarOptsInMainType[index];
-                }
-                else {
-                    if (fakeCmpt && fakeCmpt.subType === keyInfo.subType) {
-                        fakeCmpt.name = keyInfo.name;
-                        if (notInit) {
-                            fakeCmptOpt = merge(oldTarOptsInMainType[index], newOption, true);
-                        }
-                    }
-                    else {
-                        fakeCmpt = extend({}, keyInfo);
-                        if (notInit) {
-                            fakeCmptOpt = clone(newOption);
-                        }
-                    }
-                }
-
-                if (fakeCmpt) {
-                    notInit && resultTarOptsInMainType.push(fakeCmptOpt);
-                    resultFakeCmptsInMainType.push(fakeCmpt);
-                }
-                else {
-                    notInit && resultTarOptsInMainType.push(void 0);
-                    resultFakeCmptsInMainType.push(void 0);
-                }
-            });
-        }
-    });
-}
+// function mergeToBackupOption(
+//     fakeCmptsMap: FakeComponentsMap,
+//     // `tarOption` Can be null/undefined, means init
+//     tarOption: ECUnitOption,
+//     newOption: ECUnitOption,
+//     // Can be null/undefined
+//     opt: InnerSetOptionOpts
+// ): void {
+//     newOption = newOption || {} as ECUnitOption;
+//     const notInit = !!tarOption;
+
+//     each(newOption, function (newOptsInMainType, mainType) {
+//         if (newOptsInMainType == null) {
+//             return;
+//         }
+
+//         if (!ComponentModel.hasClass(mainType)) {
+//             if (tarOption) {
+//                 tarOption[mainType] = merge(tarOption[mainType], newOptsInMainType, true);
+//             }
+//         }
+//         else {
+//             const oldTarOptsInMainType = notInit ? normalizeToArray(tarOption[mainType]) : null;
+//             const oldFakeCmptsInMainType = fakeCmptsMap.get(mainType) || [];
+//             const resultTarOptsInMainType = notInit ? (tarOption[mainType] = [] as ComponentOption[]) : null;
+//             const resultFakeCmptsInMainType = fakeCmptsMap.set(mainType, []);
+
+//             const mappingResult = mappingToExists(
+//                 oldFakeCmptsInMainType,
+//                 normalizeToArray(newOptsInMainType),
+//                 (opt && opt.replaceMergeMainTypeMap.get(mainType)) ? 'replaceMerge' : 'normalMerge'
+//             );
+//             setComponentTypeToKeyInfo(mappingResult, mainType, ComponentModel as ComponentModelConstructor);
+
+//             each(mappingResult, function (resultItem, index) {
+//                 // The same logic as `Global.ts#_mergeOption`.
+//                 let fakeCmpt = resultItem.existing;
+//                 const newOption = resultItem.newOption;
+//                 const keyInfo = resultItem.keyInfo;
+//                 let fakeCmptOpt;
+
+//                 if (!newOption) {
+//                     fakeCmptOpt = oldTarOptsInMainType[index];
+//                 }
+//                 else {
+//                     if (fakeCmpt && fakeCmpt.subType === keyInfo.subType) {
+//                         fakeCmpt.name = keyInfo.name;
+//                         if (notInit) {
+//                             fakeCmptOpt = merge(oldTarOptsInMainType[index], newOption, true);
+//                         }
+//                     }
+//                     else {
+//                         fakeCmpt = extend({}, keyInfo);
+//                         if (notInit) {
+//                             fakeCmptOpt = clone(newOption);
+//                         }
+//                     }
+//                 }
+
+//                 if (fakeCmpt) {
+//                     notInit && resultTarOptsInMainType.push(fakeCmptOpt);
+//                     resultFakeCmptsInMainType.push(fakeCmpt);
+//                 }
+//                 else {
+//                     notInit && resultTarOptsInMainType.push(void 0);
+//                     resultFakeCmptsInMainType.push(void 0);
+//                 }
+//             });
+//         }
+//     });
+// }
 
 export default OptionManager;
diff --git a/src/model/internalComponentCreator.ts b/src/model/internalComponentCreator.ts
new file mode 100644
index 0000000..ca48540
--- /dev/null
+++ b/src/model/internalComponentCreator.ts
@@ -0,0 +1,75 @@
+/*
+* 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 GlobalModel from './Global';
+import { ComponentOption, ComponentMainType } from '../util/types';
+import { createHashMap, assert } from 'zrender/src/core/util';
+import { __DEV__ } from '../config';
+import { isComponentIdInternal } from '../util/model';
+
+// PNEDING:
+// (1) Only Internal usage at present, do not export to uses.
+// (2) "Internal components" are generated internally during the `Global.ts#_mergeOption`.
+//     It is added since echarts 3.
+// (3) Why keep supporting "internal component" in global model rather than
+//     make each type components manage their models themselves?
+//     Because a protential feature that reproduce a chart from a diffferent chart instance
+//     might be useful in some BI analysis scenario, where the entire state need to be
+//     retrieved from the current chart instance. So we'd bettern manage the all of the
+//     state universally.
+// (4) Internal component always merged in "replaceMerge" approach, that is, if the existing
+//     internal components does not matched by a new option with the same id, it will be
+//     removed.
+// (5) In `InternalOptionCreator`, only the previous component models (dependencies) can be read.
+
+interface InternalOptionCreator {
+    (ecModel: GlobalModel): ComponentOption[]
+}
+
+const internalOptionCreatorMap = createHashMap<InternalOptionCreator, string>();
+
+
+export function registerInternalOptionCreator(
+    mainType: ComponentMainType, creator: InternalOptionCreator
+) {
+    assert(internalOptionCreatorMap.get(mainType) == null && creator);
+    internalOptionCreatorMap.set(mainType, creator);
+}
+
+
+export function concatInternalOptions(
+    ecModel: GlobalModel,
+    mainType: ComponentMainType,
+    newCmptOptionList: ComponentOption[]
+): ComponentOption[] {
+    const internalOptionCreator = internalOptionCreatorMap.get(mainType);
+    if (!internalOptionCreator) {
+        return newCmptOptionList;
+    }
+    const internalOptions = internalOptionCreator(ecModel);
+    if (!internalOptions) {
+        return newCmptOptionList;
+    }
+    if (__DEV__) {
+        for (let i = 0; i < internalOptions.length; i++) {
+            assert(isComponentIdInternal(internalOptions[i]));
+        }
+    }
+    return newCmptOptionList.concat(internalOptions);
+}
diff --git a/src/util/model.ts b/src/util/model.ts
index e0939a9..8247a49 100644
--- a/src/util/model.ts
+++ b/src/util/model.ts
@@ -146,7 +146,7 @@ export function isDataItemOption(dataItem: OptionDataItem): boolean {
 }
 
 // Compatible with previous definition: id could be number (but not recommanded).
-// number and number-like string are trade the same when compare.
+// number and string are trade the same when compare.
 // number id will not be converted to string in option.
 // number id will be converted to string in component instance id.
 export interface MappingExistingItem {
@@ -180,171 +180,150 @@ interface MappingResultItem<T extends MappingExistingItem = MappingExistingItem>
     };
 }
 
-export function mappingToExists<T extends MappingExistingItem>(
-    existings: T[],
-    newCmptOptions: ComponentOption[],
-    mode: 'normalMerge' | 'replaceMerge'
-): MappingResult<T> {
-    return mode === 'replaceMerge'
-        ? mappingToExistsInReplaceMerge(existings, newCmptOptions)
-        : mappingToExistsInNormalMerge(existings, newCmptOptions);
-}
-
 /**
  * Mapping to existings for merge.
- * The mapping result (merge result) will keep the order of the existing
- * component, rather than the order of new option. Because we should ensure
- * some specified index reference (like xAxisIndex) keep work.
- * And in most cases, "merge option" is used to update partial option but not
- * be expected to change the order.
+ *
+ * Mode "normalMege":
+ *     The mapping result (merge result) will keep the order of the existing
+ *     component, rather than the order of new option. Because we should ensure
+ *     some specified index reference (like xAxisIndex) keep work.
+ *     And in most cases, "merge option" is used to update partial option but not
+ *     be expected to change the order.
+ *
+ * Mode "replaceMege":
+ *     (1) Only the id mapped components will be merged.
+ *     (2) Other existing components (except internal compoonets) will be removed.
+ *     (3) Other new options will be used to create new component.
+ *     (4) The index of the existing compoents will not be modified.
+ *     That means their might be "hole" after the removal.
+ *     The new components are created first at those available index.
  *
  * @return See the comment of <MappingResult>.
  */
-function mappingToExistsInNormalMerge<T extends MappingExistingItem>(
+export function mappingToExists<T extends MappingExistingItem>(
     existings: T[],
-    newCmptOptions: ComponentOption[]
+    newCmptOptions: ComponentOption[],
+    mode: 'normalMerge' | 'replaceMerge'
 ): MappingResult<T> {
-    newCmptOptions = (newCmptOptions || []).slice();
-    existings = existings || [];
 
     const result: MappingResultItem<T>[] = [];
-    // Do not use native `map` to in case that the array `existings`
-    // contains elided items, which will be ommited.
-    for (let index = 0; index < existings.length; index++) {
-        // Because of replaceMerge, `existing` may be null/undefined.
-        result.push({ existing: existings[index] });
-    }
+    const isReplaceMergeMode = mode === 'replaceMerge';
+    existings = existings || [];
+    newCmptOptions = (newCmptOptions || []).slice();
+    const existingIdIdxMap = createHashMap<number>();
+
+    prepareResult(result, existings, existingIdIdxMap, isReplaceMergeMode);
 
-    // Mapping by id or name if specified.
     each(newCmptOptions, function (cmptOption, index) {
         if (!isObject<ComponentOption>(cmptOption)) {
             newCmptOptions[index] = null;
             return;
         }
-
         cmptOption.id == null || validateIdOrName(cmptOption.id);
         cmptOption.name == null || validateIdOrName(cmptOption.name);
+    });
 
-        // id has highest priority.
-        for (let i = 0; i < result.length; i++) {
-            const existing = result[i].existing;
-            if (!result[i].newOption // Consider name: two map to one.
-                && existing
-                && keyExistAndEqual('id', existing, cmptOption)
-            ) {
-                result[i].newOption = cmptOption;
-                newCmptOptions[index] = null;
-                return;
-            }
-        }
+    mappingById(result, existings, existingIdIdxMap, newCmptOptions);
 
-        for (let i = 0; i < result.length; i++) {
-            const existing = result[i].existing;
-            if (!result[i].newOption // Consider name: two map to one.
-                // Can not match when both ids existing but different.
-                && existing
-                && (existing.id == null || cmptOption.id == null)
-                && !isComponentIdInternal(cmptOption)
-                && !isComponentIdInternal(existing)
-                && keyExistAndEqual('name', existing, cmptOption)
-            ) {
-                result[i].newOption = cmptOption;
-                newCmptOptions[index] = null;
-                return;
-            }
-        }
-    });
+    if (!isReplaceMergeMode) {
+        mappingByName(result, newCmptOptions);
+    }
 
-    mappingByIndexFinally(newCmptOptions, result, false);
+    mappingByIndex(result, newCmptOptions, isReplaceMergeMode);
 
     makeIdAndName(result);
 
+    // The array `result` MUST NOT contain elided items, otherwise the
+    // forEach will ommit those items and result in incorrect result.
     return result;
 }
 
-/**
- * Mapping to exists for merge.
- * The mode "replaceMerge" means that:
- * (1) Only the id mapped components will be merged.
- * (2) Other existing components (except internal compoonets) will be removed.
- * (3) Other new options will be used to create new component.
- * (4) The index of the existing compoents will not be modified.
- * That means their might be "hole" after the removal.
- * The new components are created first at those available index.
- *
- * @return See the comment of <MappingResult>.
- */
-function mappingToExistsInReplaceMerge<T extends MappingExistingItem>(
+function prepareResult<T extends MappingExistingItem>(
+    result: MappingResult<T>,
     existings: T[],
-    newCmptOptions: ComponentOption[]
-): MappingResult<T> {
-
-    existings = existings || [];
-    newCmptOptions = (newCmptOptions || []).slice();
-    const existingIdIdxMap = createHashMap<number>();
-    const result = [] as MappingResult<T>;
-
-    // Do not use native `each` to in case that the array `existings`
+    existingIdIdxMap: HashMap<number>,
+    isReplaceMergeMode: boolean
+) {
+    // Do not use native `map` to in case that the array `existings`
     // contains elided items, which will be ommited.
     for (let index = 0; index < existings.length; index++) {
         const existing = existings[index];
-        let internalExisting: T;
         // Because of replaceMerge, `existing` may be null/undefined.
-        if (existing) {
-            if (isComponentIdInternal(existing)) {
-                // internal components should not be removed.
-                internalExisting = existing;
-            }
-            // Input with internal id is allowed for convenience of some internal usage.
-            // When `existing` is rawOption (see `OptionManager`#`mergeOption`), id might be empty.
-            if (existing.id != null) {
-                existingIdIdxMap.set(existing.id, index);
-            }
+        if (existing && existing.id != null) {
+            existingIdIdxMap.set(existing.id, index);
         }
-        result.push({ existing: internalExisting });
+        // For non-internal-componnets:
+        //     Mode "normalMerge": all existings kept.
+        //     Mode "replaceMerge": all existing removed unless mapped by id.
+        // For internal-components:
+        //     go with "replaceMerge" approach in both mode.
+        result.push({
+            existing: (isReplaceMergeMode || isComponentIdInternal(existing))
+                ? null
+                : existing
+        });
     }
+}
 
+function mappingById<T extends MappingExistingItem>(
+    result: MappingResult<T>,
+    existings: T[],
+    existingIdIdxMap: HashMap<number>,
+    newCmptOptions: ComponentOption[]
+): void {
     // Mapping by id if specified.
     each(newCmptOptions, function (cmptOption, index) {
-        if (!isObject<ComponentOption>(cmptOption)) {
-            newCmptOptions[index] = null;
+        if (!cmptOption || cmptOption.id == null) {
             return;
         }
-
-        cmptOption.id == null || validateIdOrName(cmptOption.id);
-        cmptOption.name == null || validateIdOrName(cmptOption.name);
-
         const optionId = makeComparableKey(cmptOption.id);
         const existingIdx = existingIdIdxMap.get(optionId);
         if (existingIdx != null) {
-            if (__DEV__) {
-                assert(
-                    !result[existingIdx].newOption,
-                    'Duplicated option on id "' + optionId + '".'
-                );
-            }
-            result[existingIdx].newOption = cmptOption;
-            // Mark not to be removed but to be merged.
-            // In this case the existing component will be merged with the new option if `subType` is the same,
-            // or replaced with a new created component if the `subType` is different.
-            result[existingIdx].existing = existings[existingIdx];
+            const resultItem = result[existingIdx];
+            assert(
+                !resultItem.newOption,
+                'Duplicated option on id "' + optionId + '".'
+            );
+            resultItem.newOption = cmptOption;
+            // In both mode, if id matched, new option will be merged to
+            // the existings rather than creating new component model.
+            resultItem.existing = existings[existingIdx];
             newCmptOptions[index] = null;
         }
     });
+}
 
-    mappingByIndexFinally(newCmptOptions, result, true);
-
-    makeIdAndName(result);
-
-    // The array `result` MUST NOT contain elided items, otherwise the
-    // forEach will ommit those items and result in incorrect result.
-    return result;
+function mappingByName<T extends MappingExistingItem>(
+    result: MappingResult<T>,
+    newCmptOptions: ComponentOption[]
+): void {
+    // Mapping by name if specified.
+    each(newCmptOptions, function (cmptOption, index) {
+        if (!cmptOption || cmptOption.name == null) {
+            return;
+        }
+        for (let i = 0; i < result.length; i++) {
+            const existing = result[i].existing;
+            if (!result[i].newOption // Consider name: two map to one.
+                // Can not match when both ids existing but different.
+                && existing
+                && (existing.id == null || cmptOption.id == null)
+                && !isComponentIdInternal(cmptOption)
+                && !isComponentIdInternal(existing)
+                && keyExistAndEqual('name', existing, cmptOption)
+            ) {
+                result[i].newOption = cmptOption;
+                newCmptOptions[index] = null;
+                return;
+            }
+        }
+    });
 }
 
-function mappingByIndexFinally<T extends MappingExistingItem>(
+function mappingByIndex<T extends MappingExistingItem>(
+    result: MappingResult<T>,
     newCmptOptions: ComponentOption[],
-    mappingResult: MappingResult<T>,
-    allBrandNew: boolean
+    isReplaceMergeMode: boolean
 ): void {
     let nextIdx = 0;
     each(newCmptOptions, function (cmptOption) {
@@ -355,8 +334,8 @@ function mappingByIndexFinally<T extends MappingExistingItem>(
         // Find the first place that not mapped by id and not internal component (consider the "hole").
         let resultItem;
         while (
-            // Be `!resultItem` only when `nextIdx >= mappingResult.length`.
-            (resultItem = mappingResult[nextIdx])
+            // Be `!resultItem` only when `nextIdx >= result.length`.
+            (resultItem = result[nextIdx])
             // (1) Existing models that already have id should be able to mapped to. Because
             // after mapping performed, model will always be assigned with an id if user not given.
             // After that all models have id.
@@ -364,13 +343,14 @@ function mappingByIndexFinally<T extends MappingExistingItem>(
             // not be merged to the existings with different id. Because id should not be overwritten.
             // (3) Name can be overwritten, because axis use name as 'show label text'.
             && (
-                (
-                    cmptOption.id != null
-                    && resultItem.existing
+                resultItem.newOption
+                || isComponentIdInternal(resultItem.existing)
+                || (
+                    // In mode "replaceMerge", here no not-mapped-non-internal-existing.
+                    resultItem.existing
+                    && cmptOption.id != null
                     && !keyExistAndEqual('id', cmptOption, resultItem.existing)
                 )
-                || resultItem.newOption
-                || isComponentIdInternal(resultItem.existing)
             )
         ) {
             nextIdx++;
@@ -378,10 +358,10 @@ function mappingByIndexFinally<T extends MappingExistingItem>(
 
         if (resultItem) {
             resultItem.newOption = cmptOption;
-            resultItem.brandNew = allBrandNew;
+            resultItem.brandNew = isReplaceMergeMode;
         }
         else {
-            mappingResult.push({ newOption: cmptOption, brandNew: allBrandNew });
+            result.push({ newOption: cmptOption, brandNew: isReplaceMergeMode });
         }
         nextIdx++;
     });
@@ -474,7 +454,7 @@ function makeIdAndName(
 function keyExistAndEqual(attr: 'id' | 'name', obj1: MappingExistingItem, obj2: MappingExistingItem): boolean {
     const key1 = obj1[attr];
     const key2 = obj2[attr];
-    // See `MappingExistingItem`. `id` and `name` trade number-like string equals to number.
+    // See `MappingExistingItem`. `id` and `name` trade string equals to number.
     return key1 != null && key2 != null && key1 + '' === key2 + '';
 }
 
@@ -702,17 +682,21 @@ let innerUniqueIndex = Math.round(Math.random() * 5);
  * }
  * xxxIndex can be set as 'all' (means all xxx) or 'none' (means not specify)
  * If nothing or null/undefined specified, return nothing.
+ * If both `abcIndex`, `abcId`, `abcName` specified, only one work.
+ * The priority is: index > id > name, the same with `ecModel.queryComponents`.
  */
-export type ModelFinderIndexQuery = number | number[] | 'all' | 'none';
+export type ModelFinderIndexQuery = number | number[] | 'all' | 'none' | false;
+export type ModelFinderIdQuery = number | number[] | string | string[];
+export type ModelFinderNameQuery = number | number[] | string | string[];
 export type ModelFinder = string | ModelFinderObject;
 export type ModelFinderObject = {
-    seriesIndex?: ModelFinderIndexQuery, seriesId?: string, seriesName?: string,
-    geoIndex?: ModelFinderIndexQuery, geoId?: string, geoName?: string,
-    bmapIndex?: ModelFinderIndexQuery, bmapId?: string, bmapName?: string,
-    xAxisIndex?: ModelFinderIndexQuery, xAxisId?: string, xAxisName?: string,
-    yAxisIndex?: ModelFinderIndexQuery, yAxisId?: string, yAxisName?: string,
-    gridIndex?: ModelFinderIndexQuery, gridId?: string, gridName?: string,
-    // ... (can be extended)
+    seriesIndex?: ModelFinderIndexQuery, seriesId?: ModelFinderIdQuery, seriesName?: ModelFinderNameQuery
+    geoIndex?: ModelFinderIndexQuery, geoId?: ModelFinderIdQuery, geoName?: ModelFinderNameQuery
+    bmapIndex?: ModelFinderIndexQuery, bmapId?: ModelFinderIdQuery, bmapName?: ModelFinderNameQuery
+    xAxisIndex?: ModelFinderIndexQuery, xAxisId?: ModelFinderIdQuery, xAxisName?: ModelFinderNameQuery
+    yAxisIndex?: ModelFinderIndexQuery, yAxisId?: ModelFinderIdQuery, yAxisName?: ModelFinderNameQuery
+    gridIndex?: ModelFinderIndexQuery, gridId?: ModelFinderIdQuery, gridName?: ModelFinderNameQuery
+       // ... (can be extended)
     [key: string]: unknown
 };
 /**
@@ -741,10 +725,13 @@ export type ParsedModelFinder = ParsedModelFinderKnown & {
     [key: string]: ComponentModel | ComponentModel[];
 };
 
+/**
+ * The same behavior as `component.getReferringComponents`.
+ */
 export function parseFinder(
     ecModel: GlobalModel,
     finderInput: ModelFinder,
-    opt?: {defaultMainType?: string, includeMainTypes?: string[]}
+    opt?: {defaultMainType?: ComponentMainType, includeMainTypes?: ComponentMainType[]}
 ): ParsedModelFinder {
     let finder: ModelFinderObject;
     if (isString(finderInput)) {
@@ -756,15 +743,8 @@ export function parseFinder(
         finder = finderInput;
     }
 
-    const defaultMainType = opt && opt.defaultMainType;
-    if (defaultMainType
-        && !has(finder, defaultMainType + 'Index')
-        && !has(finder, defaultMainType + 'Id')
-        && !has(finder, defaultMainType + 'Name')
-    ) {
-        finder[defaultMainType + 'Index'] = 0;
-    }
-
+    const defaultMainType = opt ? opt.defaultMainType : null;
+    const queryOptionMap = createHashMap<QueryReferringComponentsOption, ComponentMainType>();
     const result = {} as ParsedModelFinder;
 
     each(finder, function (value, key) {
@@ -776,32 +756,87 @@ export function parseFinder(
 
         const parsedKey = key.match(/^(\w+)(Index|Id|Name)$/) || [];
         const mainType = parsedKey[1];
-        const queryType = (parsedKey[2] || '').toLowerCase() as ('id' | 'index' | 'name');
+        const queryType = (parsedKey[2] || '').toLowerCase() as keyof QueryReferringComponentsOption;
 
-        if (!mainType
+        if (
+            !mainType
             || !queryType
-            || value == null
-            || (queryType === 'index' && value === 'none')
+            || (mainType !== defaultMainType && value == null)
             || (opt && opt.includeMainTypes && indexOf(opt.includeMainTypes, mainType) < 0)
         ) {
             return;
         }
 
-        const queryParam = {mainType: mainType} as QueryConditionKindB;
-        if (queryType !== 'index' || value !== 'all') {
-            queryParam[queryType] = value as any;
-        }
+        const queryOption = queryOptionMap.get(mainType) || queryOptionMap.set(mainType, {});
+        queryOption[queryType] = value as any;
+    });
 
-        const models = ecModel.queryComponents(queryParam);
-        result[mainType + 'Models'] = models;
-        result[mainType + 'Model'] = models[0];
+    queryOptionMap.each(function (queryOption, mainType) {
+        const queryResult = queryReferringComponents(
+            ecModel,
+            mainType,
+            queryOption,
+            mainType === defaultMainType
+        );
+        result[mainType + 'Models'] = queryResult.models;
+        result[mainType + 'Model'] = queryResult.models[0];
     });
 
     return result;
 }
 
-function has(obj: object, prop: string): boolean {
-    return obj && obj.hasOwnProperty(prop);
+type QueryReferringComponentsOption = {
+    index?: ModelFinderIndexQuery,
+    id?: ModelFinderIdQuery,
+    name?: ModelFinderNameQuery,
+};
+
+export function queryReferringComponents(
+    ecModel: GlobalModel,
+    mainType: ComponentMainType,
+    option: QueryReferringComponentsOption,
+    useDefault?: boolean
+): {
+    // Always be array rather than null/undefined, which is convenient to use.
+    models: ComponentModel[];
+    // Whether there is indexOption/id/name specified
+    specified: boolean;
+} {
+    let indexOption = option.index;
+    let idOption = option.id;
+    let nameOption = option.name;
+
+    const result = {
+        models: null as ComponentModel[],
+        specified: indexOption != null || idOption != null || nameOption != null
+    };
+
+    if (!result.specified) {
+        // Use the first as default if `useDefault`.
+        let firstCmpt;
+        result.models = (
+            useDefault && (firstCmpt = ecModel.getComponent(mainType))
+        ) ? [firstCmpt] : [];
+        return result;
+    }
+
+    if (indexOption === 'none' || indexOption === false) {
+        result.models = [];
+        return result;
+    }
+
+    // `queryComponents` will return all components if
+    // both all of index/id/name are null/undefined.
+    if (indexOption === 'all') {
+        indexOption = idOption = nameOption = null;
+    }
+    result.models = ecModel.queryComponents({
+        mainType: mainType,
+        index: indexOption as number | number[],
+        id: idOption,
+        name: nameOption
+    });
+    return result;
 }
 
 export function setAttribute(dom: HTMLElement, key: string, value: any) {
diff --git a/test/dataZoom-feature.html b/test/dataZoom-feature.html
index f93fc4b..520ce6e 100644
--- a/test/dataZoom-feature.html
+++ b/test/dataZoom-feature.html
@@ -38,7 +38,8 @@ under the License.
 
 
         <div id="refer_by_id"></div>
-        <div id="auto_axis_second_setOption"></div>
+        <div id="auto_axis_second_setOption_normal_dz"></div>
+        <div id="auto_axis_second_setOption_only_toolbox_dz"></div>
 
 
 
@@ -172,7 +173,7 @@ under the License.
                 }]
             };
 
-            var chart = testHelper.create(echarts, 'auto_axis_second_setOption', {
+            var chart = testHelper.create(echarts, 'auto_axis_second_setOption_normal_dz', {
                 title: [
                     'two grids, each has two xAxis.',
                     'dataZoom should auto **control all of the two xAxis of the first** grid.',
@@ -251,6 +252,146 @@ under the License.
 
 
 
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option;
+
+            option = {
+                toolbox: {
+                    left: 'center',
+                    feature: {
+                        dataZoom: {}
+                    }
+                },
+                grid: [{
+                    bottom: '60%'
+                }, {
+                    id: 'gb',
+                    top: '60%'
+                }],
+                xAxis: [{
+                    type: 'category'
+                }, {
+                    type: 'category'
+                }, {
+                    id: 'xb0',
+                    type: 'category',
+                    gridIndex: 1
+                }, {
+                    id: 'xb1',
+                    type: 'category',
+                    gridIndex: 1
+                }],
+                yAxis: [{
+
+                }, {
+                    id: 'yb',
+                    gridIndex: 1
+                }],
+                series: [{
+                    type: 'line',
+                    data: [[333, 22], [666, 44]]
+                }, {
+                    type: 'line',
+                    xAxisIndex: 1,
+                    data: [[88888, 52], [99999, 74]]
+                }, {
+                    id: 'sb0',
+                    type: 'line',
+                    xAxisIndex: 2,
+                    yAxisIndex: 1,
+                    data: [[63, 432], [98, 552]]
+                }, {
+                    id: 'sb1',
+                    type: 'line',
+                    xAxisIndex: 3,
+                    yAxisIndex: 1,
+                    data: [[87654, 1432], [56789, 1552]]
+                }]
+            };
+
+            var chart = testHelper.create(echarts, 'auto_axis_second_setOption_only_toolbox_dz', {
+                title: [
+                    '[Only toolbox dataZoom] two grids, each has two xAxis.',
+                    'toolbox zoom should work on **all grids**.',
+                    'Click btn "remove the first grid".',
+                    'toolbox zoom should work only on **the second grids**.',
+                    'Click btn "addback the first grid".',
+                    'toolbox zoom should work on **all grids**.',
+                    'Click btn "remove all grids".',
+                    'Should **no error**.',
+                    'Check toolbox zoom should **not work on the original area**.',
+                    'Click btn "addback the first grid".',
+                    'toolbox zoom should work only on the **the first grids**.',
+                ],
+                option: option,
+                height: 350,
+                buttons: [{
+                    text: 'remove the first grid',
+                    onclick: function () {
+                        chart.setOption({
+                            grid: [{
+                                id: 'gb',
+                            }],
+                            xAxis: [{
+                                id: 'xb0',
+                            }, {
+                                id: 'xb1',
+                            }],
+                            yAxis: [{
+                                id: 'yb'
+                            }],
+                            series: [{
+                                id: 'sb0',
+                            }, {
+                                id: 'sb1',
+                            }]
+                        }, { replaceMerge: ['grid', 'xAxis', 'yAxis', 'series'] });
+                    }
+                }, {
+                    text: 'addback the first grid',
+                    onclick: function () {
+                        chart.setOption({
+                            grid: [{
+                                bottom: '60%'
+                            }],
+                            xAxis: [{
+                            }, {
+                            }],
+                            yAxis: [{
+                            }],
+                            series: [{
+                                type: 'line',
+                                data: [[333, 22], [666, 44]]
+                            }, {
+                                type: 'line',
+                                xAxisIndex: 1,
+                                data: [[88888, 52], [99999, 74]]
+                            }]
+                        });
+                    }
+                }, {
+                    text: 'remove all grids',
+                    onclick: function () {
+                        chart.setOption({
+                            grid: [],
+                            xAxis: [],
+                            yAxis: [],
+                            series: []
+                        }, { replaceMerge: ['grid', 'xAxis', 'yAxis', 'series'] });
+                    }
+                }]
+            });
+        });
+        </script>
+
+
+
+
+
     </body>
 </html>
 
diff --git a/test/dataZoom-toolbox.html b/test/dataZoom-toolbox.html
index 992ab4c..54bd40e 100644
--- a/test/dataZoom-toolbox.html
+++ b/test/dataZoom-toolbox.html
@@ -60,6 +60,7 @@ under the License.
         <div class="chart" id="main-specify-x-axis"></div>
 
         <div id="main0"></div>
+        <div id="main-refer-by-axis-id"></div>
 
         <script>
 
@@ -735,5 +736,90 @@ under the License.
         </script>
 
 
+
+
+        <script>
+        require(['echarts'/*, 'map/js/china' */], function (echarts) {
+            var option;
+
+            option = {
+                toolbox: {
+                    feature: {
+                        dataZoom: {
+                            xAxisId: 'xr',
+                            yAxisId: ['yl0', 'yl1']
+                        }
+                    }
+                },
+                legend: {},
+                grid: [{
+                    right: '60%',
+                    bottom: '60%',
+                }, {
+                    id: 'gr',
+                    left: '60%',
+                    bottom: '60%',
+                }, {
+                    id: 'gb',
+                    top: '60%',
+                }],
+                xAxis: [{
+
+                }, {
+                    id: 'xr',
+                    gridId: 'gr'
+                }, {
+                    id: 'xb',
+                    gridId: 'gb'
+                }],
+                yAxis: [{
+                    id: 'yl0'
+                }, {
+                    id: 'yl1'
+                }, {
+                    id: 'yr',
+                    gridId: 'gr'
+                }, {
+                    id: 'yb',
+                    gridId: 'gb'
+                }],
+                series: [{
+                    type: 'line',
+                    yAxisId: 'yl0',
+                    data: [[11, 12], [22, 45], [33, 76]]
+                }, {
+                    type: 'line',
+                    yAxisId: 'yl1',
+                    data: [[11, 2212], [22, 3345], [33, 4476]]
+                }, {
+                    type: 'line',
+                    xAxisId: 'xr',
+                    yAxisId: 'yr',
+                    data: [[45, 65], [13, 25], [56, 71]]
+                }, {
+                    type: 'line',
+                    xAxisId: 'xb',
+                    yAxisId: 'yb',
+                    data: [[123, 654], [234, 321], [345, 812]]
+                }]
+            };
+
+            var chart = testHelper.create(echarts, 'main-refer-by-axis-id', {
+                title: [
+                    'Test toolbox datazoom refer with axis id',
+                    'left grid: toolbox only work **on two yAxis**',
+                    'right grid: toolbox only work **on xAxis**',
+                    'bottom grid: toolbox **does not work**'
+                ],
+                option: option
+                // height: 300,
+                // buttons: [{text: 'btn-txt', onclick: function () {}}],
+                // recordCanvas: true,
+            });
+        });
+        </script>
+
+
+
     </body>
 </html>
\ No newline at end of file


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