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:01 UTC

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

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