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:39:09 UTC

[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.

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