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/10/23 16:26:37 UTC

[incubator-echarts] branch next updated: fix: fix custom morph for multiple "morph" settings in one data item.

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

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


The following commit(s) were added to refs/heads/next by this push:
     new 9eccb26  fix: fix custom morph for multiple "morph" settings in one data item.
     new b6ca47f  Merge branch 'next' of github.com:apache/incubator-echarts into next
9eccb26 is described below

commit 9eccb2662f62e2b6882f5c209d86f3e27c9a60e2
Author: 100pah <su...@gmail.com>
AuthorDate: Sat Oct 24 00:15:08 2020 +0800

    fix: fix custom morph for multiple "morph" settings in one data item.
---
 src/chart/custom.ts              | 627 ++++++++++++++++++++--------------
 test/custom-shape-morphing2.html | 710 ++++++++++++++++++++-------------------
 2 files changed, 728 insertions(+), 609 deletions(-)

diff --git a/src/chart/custom.ts b/src/chart/custom.ts
index 7d793dc..4d4a1a6 100644
--- a/src/chart/custom.ts
+++ b/src/chart/custom.ts
@@ -88,6 +88,7 @@ import {
     combine, isInAnyMorphing, morphPath, isCombiningPath, CombineSeparateConfig, separate, CombineSeparateResult
 } from 'zrender/src/tool/morphPath';
 import { AnimationEasing } from 'zrender/src/animation/easing';
+import * as matrix from 'zrender/src/core/matrix';
 
 
 const inner = makeInner<{
@@ -98,7 +99,8 @@ const inner = makeInner<{
     // customText: string;
     txConZ2Set: number;
     leaveToProps: ElementProps;
-    morphOption: boolean;
+    // Can morph: "morph" specified in option and el is Path.
+    canMorph: boolean;
     userDuring: CustomBaseElementOption['during'];
 }, Element>();
 
@@ -368,6 +370,8 @@ export type PrepareCustomInfo = (coordSys: CoordinateSystem) => {
     api: CustomSeriesRenderItemCoordinateSystemAPI
 };
 
+const tmpTransformable = new Transformable();
+
 /**
  * To reduce total package size of each coordinate systems, the modules `prepareCustom`
  * of each coordinate systems are not required by each coordinate systems directly, but
@@ -469,7 +473,6 @@ class CustomSeriesView extends ChartView {
         // roam or data zoom according to `actionType`.
 
         const transOpt = customSeries.__transientTransitionOpt;
-        const diffMode: DataDiffMode = transOpt ? 'multiple' : 'oneToOne';
 
         // Enable user to disable transition animation by both set
         // `from` and `to` dimension as `null`/`undefined`.
@@ -484,7 +487,8 @@ class CustomSeriesView extends ChartView {
             });
         }
         else {
-            const morphPreparation = new MorphPreparation();
+            const morphPreparation = new MorphPreparation(customSeries, transOpt);
+            const diffMode: DataDiffMode = transOpt ? 'multiple' : 'oneToOne';
 
             (new DataDiffer(
                 oldData ? oldData.getIndices() : [],
@@ -504,52 +508,53 @@ class CustomSeriesView extends ChartView {
                 doRemoveEl(oldData.getItemGraphicEl(oldIdx), customSeries, group);
             })
             .update(function (newIdx, oldIdx) {
-                morphPreparation.reset();
-                morphPreparation.enableNextAddTo();
-                const oldEl = oldData.getItemGraphicEl(oldIdx);
-                const from = findMayMorphFrom(oldEl);
-                morphPreparation.addFrom(from);
-                morphPreparation.needCheckFromSameEl = true;
-                // Do not call `removeElementDirectly` here.
-                // `oldEl` is passed into the first param of `createOrUpdateItem`,
-                // which will handle the remove or replace.
+                morphPreparation.reset('oneToOne');
+                let oldEl = oldData.getItemGraphicEl(oldIdx);
+                morphPreparation.findAndAddFrom(oldEl);
+
+                // PENDING:
+                // if may morph, currently we alway recreate the whole el.
+                // because if reuse some of the el in the group tree, the old el has to
+                // be removed from the group, and consequently we can not calculate
+                // the "global transition" of the old element.
+                // But is there performance issue?
+                if (morphPreparation.hasFrom()) {
+                    removeElementDirectly(oldEl, group);
+                    oldEl = null;
+                }
                 createOrUpdateItem(
                     oldEl, newIdx, renderItem(newIdx, payload), customSeries, group,
                     data, morphPreparation
                 );
-                morphPreparation.applyMorphing('oneToOne', customSeries, transOpt);
+                morphPreparation.applyMorphing();
             })
             .updateManyToOne(function (newIdx, oldIndices) {
-                morphPreparation.reset();
+                morphPreparation.reset('manyToOne');
                 for (let i = 0; i < oldIndices.length; i++) {
                     const oldEl = oldData.getItemGraphicEl(oldIndices[i]);
-                    const from = findMayMorphFrom(oldEl);
-                    morphPreparation.addFrom(from);
+                    morphPreparation.findAndAddFrom(oldEl);
                     removeElementDirectly(oldEl, group);
                 }
-                morphPreparation.enableNextAddTo();
                 createOrUpdateItem(
                     null, newIdx, renderItem(newIdx, payload), customSeries, group,
                     data, morphPreparation
                 );
-                morphPreparation.applyMorphing('manyToOne', customSeries, transOpt);
+                morphPreparation.applyMorphing();
             })
             .updateOneToMany(function (newIndices, oldIdx) {
-                morphPreparation.reset();
+                morphPreparation.reset('oneToMany');
                 const newLen = newIndices.length;
                 const oldEl = oldData.getItemGraphicEl(oldIdx);
-                const from = findMayMorphFrom(oldEl);
-                morphPreparation.addFrom(from);
+                morphPreparation.findAndAddFrom(oldEl);
                 removeElementDirectly(oldEl, group);
 
                 for (let i = 0; i < newLen; i++) {
-                    morphPreparation.enableNextAddTo();
                     createOrUpdateItem(
                         null, newIndices[i], renderItem(newIndices[i], payload), customSeries, group,
                         data, morphPreparation
                     );
                 }
-                morphPreparation.applyMorphing('oneToMany', customSeries, transOpt);
+                morphPreparation.applyMorphing();
             })
             .execute();
         }
@@ -1021,7 +1026,6 @@ function prepareTransformTransitionFrom(
     isInit: boolean
 ): void {
     const enterFrom = elOption.enterFrom;
-    const fromEl = morphFromEl || el;
     if (isInit && enterFrom) {
         const enterFromKeys = keys(enterFrom);
         for (let i = 0; i < enterFromKeys.length; i++) {
@@ -1038,13 +1042,14 @@ function prepareTransformTransitionFrom(
         // If morphing, force transition all transform props.
         // otherwise might have incorrect morphing animation.
         if (morphFromEl) {
-            setTransformPropToTransitionFrom(transFromProps, 'x', fromEl);
-            setTransformPropToTransitionFrom(transFromProps, 'y', fromEl);
-            setTransformPropToTransitionFrom(transFromProps, 'scaleX', fromEl);
-            setTransformPropToTransitionFrom(transFromProps, 'scaleY', fromEl);
-            setTransformPropToTransitionFrom(transFromProps, 'originX', fromEl);
-            setTransformPropToTransitionFrom(transFromProps, 'originY', fromEl);
-            setTransformPropToTransitionFrom(transFromProps, 'rotation', fromEl);
+            const fromTransformable = calcOldElLocalTransformBasedOnNewElParent(morphFromEl, el);
+            setTransformPropToTransitionFrom(transFromProps, 'x', fromTransformable);
+            setTransformPropToTransitionFrom(transFromProps, 'y', fromTransformable);
+            setTransformPropToTransitionFrom(transFromProps, 'scaleX', fromTransformable);
+            setTransformPropToTransitionFrom(transFromProps, 'scaleY', fromTransformable);
+            setTransformPropToTransitionFrom(transFromProps, 'originX', fromTransformable);
+            setTransformPropToTransitionFrom(transFromProps, 'originY', fromTransformable);
+            setTransformPropToTransitionFrom(transFromProps, 'rotation', fromTransformable);
         }
         else if (elOption.transition) {
             const transitionKeys = normalizeToArray(elOption.transition);
@@ -1053,7 +1058,7 @@ function prepareTransformTransitionFrom(
                 if (key === 'style' || key === 'shape' || key === 'extra') {
                     continue;
                 }
-                const elVal = fromEl[key];
+                const elVal = el[key];
                 if (__DEV__) {
                     checkTransformPropRefer(key, 'el.transition');
                     checkNonStyleTansitionRefer(key, elOption[key], elVal);
@@ -1064,8 +1069,8 @@ function prepareTransformTransitionFrom(
         }
         // This default transition see [STRATEGY_TRANSITION]
         else {
-            setTransformPropToTransitionFrom(transFromProps, 'x', fromEl);
-            setTransformPropToTransitionFrom(transFromProps, 'y', fromEl);
+            setTransformPropToTransitionFrom(transFromProps, 'x', el);
+            setTransformPropToTransitionFrom(transFromProps, 'y', el);
         }
     }
 
@@ -1173,6 +1178,54 @@ function prepareStyleTransitionFrom(
     }
 }
 
+/**
+ * If make "transform"(x/y/scaleX/scaleY/orient/originX/originY) transition between
+ * two path elements that have different hierarchy, before we retrieve the "from" props,
+ * we have to calculate the local transition of the "oldPath" based on the parent of
+ * the "newPath".
+ * At present, the case only happend in "morphing". Without morphing, the transform
+ * transition are all between elements in the same hierarchy, where this kind of process
+ * is not needed.
+ *
+ * [CAVEAT]:
+ * This method makes sense only if: (very tricky)
+ * (1) "newEl" has been added to its final parent.
+ * (2) Local transform props of "newPath.parent" are not at their final value but already
+ * have been at the "from value".
+ *     This is currently ensured by:
+ *     (2.1) "graphicUtil.animationFrom", which will set the element to the "from value"
+ *     immediately.
+ *     (2.2) "morph" option is not allowed to be set on Group, so all of the groups have
+ *     been finished their "updateElNormal" when calling this method in morphing process.
+ */
+function calcOldElLocalTransformBasedOnNewElParent(oldEl: Element, newEl: Element): Transformable {
+    if (!oldEl || oldEl === newEl || oldEl.parent === newEl.parent) {
+        return oldEl;
+    }
+
+    // Not sure oldEl is rendered (may have "lazyUpdate"),
+    // so always call `getComputedTransform`.
+    const tmpM = tmpTransformable.transform
+        || (tmpTransformable.transform = matrix.identity([]));
+
+    const oldGlobalTransform = oldEl.getComputedTransform();
+    oldGlobalTransform
+        ? matrix.copy(tmpM, oldGlobalTransform)
+        : matrix.identity(tmpM);
+
+    const newParent = newEl.parent;
+    if (newParent) {
+        newParent.getComputedTransform();
+    }
+
+    tmpTransformable.originX = oldEl.originX;
+    tmpTransformable.originY = oldEl.originY;
+    tmpTransformable.parent = newParent;
+    tmpTransformable.decomposeTransform();
+
+    return tmpTransformable;
+}
+
 let checkNonStyleTansitionRefer: (propName: string, optVal: unknown, elVal: unknown) => void;
 if (__DEV__) {
     checkNonStyleTansitionRefer = function (propName: string, optVal: unknown, elVal: unknown): void {
@@ -1447,14 +1500,14 @@ function setLagecyTransformProp(
     elOption: CustomElementOption,
     targetProps: Partial<Pick<Transformable, TransformProp>>,
     legacyName: LegacyTransformProp,
-    fromEl?: Element // If provided, retrieve from the element.
+    fromTransformable?: Transformable // If provided, retrieve from the element.
 ): void {
     const legacyArr = (elOption as any)[legacyName];
     const xyName = LEGACY_TRANSFORM_PROPS[legacyName];
     if (legacyArr) {
-        if (fromEl) {
-            targetProps[xyName[0]] = fromEl[xyName[0]];
-            targetProps[xyName[1]] = fromEl[xyName[1]];
+        if (fromTransformable) {
+            targetProps[xyName[0]] = fromTransformable[xyName[0]];
+            targetProps[xyName[1]] = fromTransformable[xyName[1]];
         }
         else {
             targetProps[xyName[0]] = legacyArr[0];
@@ -1467,20 +1520,20 @@ function setTransformProp(
     elOption: CustomElementOption,
     allProps: Partial<Pick<Transformable, TransformProp>>,
     name: TransformProp,
-    fromEl?: Element // If provided, retrieve from the element.
+    fromTransformable?: Transformable // If provided, retrieve from the element.
 ): void {
     if (elOption[name] != null) {
-        allProps[name] = fromEl ? fromEl[name] : elOption[name];
+        allProps[name] = fromTransformable ? fromTransformable[name] : elOption[name];
     }
 }
 
 function setTransformPropToTransitionFrom(
     transitionFrom: Partial<Pick<Transformable, TransformProp>>,
     name: TransformProp,
-    fromEl?: Element // If provided, retrieve from the element.
+    fromTransformable?: Transformable // If provided, retrieve from the element.
 ): void {
-    if (fromEl) {
-        transitionFrom[name] = fromEl[name];
+    if (fromTransformable) {
+        transitionFrom[name] = fromTransformable[name];
     }
 }
 
@@ -1847,23 +1900,16 @@ function doCreateOrUpdateEl(
         assert(elOption, 'should not have an null/undefined element setting');
     }
 
-    const optionMorph = (elOption as CustomZRPathOption).morph;
-    if (optionMorph && morphPreparation) {
-        morphPreparation.meetMorphOption();
-    }
-
     let toBeReplacedIdx = -1;
     if (
         el && (
             doesElNeedRecreate(el, elOption)
-            || (
-                // el has been used as the "from" for other el.
-                // So can not be used as the old el in this place.
-                morphPreparation
-                && morphPreparation.needCheckFromSameEl
-                && morphPreparation.getSingleFrom() === el
-                && !optionMorph
-            )
+            // || (
+            //     // PENDING: even in one-to-one mapping case, if el is marked as morph,
+            //     // do not sure whether the el will be mapped to another el with different
+            //     // hierarchy in Group tree. So always recreate el rather than reuse the el.
+            //     morphPreparation && morphPreparation.isOneToOneFrom(el)
+            // )
         )
     ) {
         // Should keep at the original index, otherwise "merge by index" will be incorrect.
@@ -1883,8 +1929,8 @@ function doCreateOrUpdateEl(
         el.clearStates();
     }
 
-    inner(el).morphOption = optionMorph;
-    const thisElIsMorphTo = optionMorph && isPath(el) && morphPreparation && morphPreparation.hasFrom();
+    const canMorph = inner(el).canMorph = (elOption as CustomZRPathOption).morph && isPath(el);
+    const thisElIsMorphTo = canMorph && morphPreparation && morphPreparation.hasFrom();
 
     // Use update animation when morph is enabled.
     const isInit = elIsNewCreated && !thisElIsMorphTo;
@@ -2312,42 +2358,62 @@ function removeElementDirectly(el: Element, group: ViewRootGroup): void {
 }
 
 
+type MorphPreparationType = 'oneToOne' | 'oneToMany' | 'manyToOne';
+
+/**
+ * Any morph-potential el should added by `morphPreparation.addTo(el)`.
+ * And they may apply morph or not when `morphPreparation.applyMorphing()`.
+ * But at least, all of the "to" elements will apply all of the updates
+ * as `doCreateOrUpdateItem` did.
+ */
 class MorphPreparation {
+    private _type: MorphPreparationType;
     private _fromList: graphicUtil.Path[] = [];
     private _toList: graphicUtil.Path[] = [];
     private _toElOptionList: CustomElementOption[] = [];
     private _allPropsFinalList: ElementProps[] = [];
     private _toDataIndices: number[] = [];
-    private _preventAddTo: boolean;
-    private _metMorphOption: boolean;
-    needCheckFromSameEl: boolean;
+    private _transOpt: SeriesModel['__transientTransitionOpt'];
+    private _seriesModel: CustomSeriesModel;
+    // Key: `toDataIndex`, not `toIdx`
+    private _morphConfigList: CombineSeparateConfig[] = [];
 
-    hasFrom(): boolean {
-        return !!this._fromList.length;
+    constructor(
+        seriesModel: CustomSeriesModel,
+        transOpt: SeriesModel['__transientTransitionOpt']
+    ) {
+        this._seriesModel = seriesModel;
+        this._transOpt = transOpt;
     }
 
-    getSingleFrom(): graphicUtil.Path {
-        return this._fromList[0];
+    hasFrom(): boolean {
+        return !!this._fromList.length;
     }
 
-    addFrom(path: graphicUtil.Path): void {
-        path && this._fromList.push(path);
-    }
+    // isOneToOneFrom(el: Element): boolean {
+    //     if (el && inner(el).canMorph) {
+    //         const fromList = this._fromList;
+    //         for (let i = 0; i < fromList.length; i++) {
+    //             if (fromList[i] === el) {
+    //                 return true;
+    //             }
+    //         }
+    //     }
+    // }
 
-    meetMorphOption() {
-        let errMsg = '';
-        if (this._metMorphOption) {
-            if (__DEV__) {
-                errMsg = 'Only one "morph" allowed in each `renderItem` return.';
+    findAndAddFrom(el: Element): void {
+        if (!el) {
+            return;
+        }
+        if (inner(el).canMorph) {
+            this._fromList.push(el as graphicUtil.Path);
+        }
+        if (el.isGroup) {
+            const children = (el as graphicUtil.Group).childrenRef();
+            for (let i = 0; i < children.length; i++) {
+                this.findAndAddFrom(children[i]);
             }
-            throwError(errMsg);
         }
-        this._metMorphOption = true;
-    }
-
-    enableNextAddTo(): void {
-        this._metMorphOption = false;
-        this._preventAddTo = false;
     }
 
     addTo(
@@ -2356,36 +2422,15 @@ class MorphPreparation {
         dataIndex: number,
         allPropsFinal: ElementProps
     ): void {
-        // After added successfully, can not perform a next add until `enableNextAddTo` called.
-        // At present only one 'morph' option works if multiple 'morph` option sepecified for a data item.
-        if (!this._preventAddTo && path) {
+        if (path) {
             this._toList.push(path);
             this._toElOptionList.push(elOption);
             this._toDataIndices.push(dataIndex);
             this._allPropsFinalList.push(allPropsFinal);
-            this._preventAddTo = true;
         }
     }
 
-    applyMorphing(
-        type: 'oneToOne' | 'oneToMany' | 'manyToOne',
-        seriesModel: CustomSeriesModel,
-        transOpt: SeriesModel['__transientTransitionOpt'] // May be null/undefined
-    ): void {
-        if (!seriesModel.isAnimationEnabled()) {
-            return;
-        }
-        const fromList = this._fromList;
-        const toList = this._toList;
-        const toElOptionList = this._toElOptionList;
-        const toDataIndices = this._toDataIndices;
-        const allPropsFinalList = this._allPropsFinalList;
-        if (!fromList.length || !toList.length) {
-            return;
-        }
-        const elAnimationConfig = prepareMorphElementAnimateConfig(seriesModel, toDataIndices[0], transOpt);
-        const morphDuration = !!elAnimationConfig.duration;
-
+    applyMorphing(): void {
         // [MORPHING_LOGIC_HINT]
         // Pay attention to the order:
         // (A) Apply `allPropsFinal` and `styleOption` to "to".
@@ -2410,181 +2455,247 @@ class MorphPreparation {
         // [MORPHING_LOGIC_HINT]
         // Make sure `applyPropsFinal` always be called for "to".
 
-        if (type === 'oneToOne') {
-            const from = fromList[0];
-            const to = toList[0];
-            const toElOption = toElOptionList[0];
-            const toDataIndex = toDataIndices[0];
-            const allPropsFinal = allPropsFinalList[0];
-
-            if (!isCombiningPath(from)) {
-                const shouldMorph = morphDuration
-                    // from === to usually happen in scenarios where internal update like
-                    // "dataZoom", "legendToggle" happen. If from is not in any morphing,
-                    // we do not need to call `morphPath`.
-                    && (from !== to || isInAnyMorphing(from));
-                const morphFrom = shouldMorph ? from : null;
-
-                // See [Case_II] above.
-                // In this case, there is probably `from === to`. And the `transitionFromProps` collecting
-                // does not depends on morphing. So we collect `transitionFromProps` first.
-                const transFromProps = {} as ElementProps;
-                prepareShapeOrExtraTransitionFrom('shape', to, morphFrom, toElOption, transFromProps, false);
-                prepareShapeOrExtraTransitionFrom('extra', to, morphFrom, toElOption, transFromProps, false);
-                prepareTransformTransitionFrom(to, morphFrom, toElOption, transFromProps, false);
-                prepareStyleTransitionFrom(to, morphFrom, toElOption, toElOption.style, transFromProps, false);
-
-                applyPropsFinal(to, allPropsFinal, toElOption.style);
-
-                if (shouldMorph) {
-                    morphPath(from, to, elAnimationConfig);
-                }
-                applyTransitionFrom(to, toDataIndex, toElOption, seriesModel, transFromProps, false);
-            }
-            else {
-                applyPropsFinal(to, allPropsFinal, toElOption.style);
+        const type = this._type;
+        const fromList = this._fromList;
+        const toList = this._toList;
+        const toListLen = toList.length;
+        const fromListLen = fromList.length;
 
-                if (morphDuration) {
-                    const combineResult = combine([from], to, elAnimationConfig, copyPropsWhenDivided);
-                    processCombineSeparateIndividuals(
-                        'combine', combineResult, seriesModel, toElOption, toDataIndex
-                    );
-                }
-                // The target el will not be displayed and transition from multiple path.
-                // transition on the target el does not make sense.
+        if (!fromListLen || !toListLen) {
+            return;
+        }
+
+        if (type === 'oneToOne') {
+            // In one-to-one case, we by default apply a simple rule:
+            // map "from" and "to" one by one.
+            // For this case: old_data_item_el and new_data_item_el
+            // has the same hierarchy of group tree but only some path type changed.
+            for (let toIdx = 0; toIdx < toListLen; toIdx++) {
+                this._oneToOneForSingleTo(toIdx, toIdx);
             }
         }
 
         else if (type === 'manyToOne') {
-            const to = toList[0];
-            const toElOption = toElOptionList[0];
-            const toDataIndex = toDataIndices[0];
-            const allPropsFinal = allPropsFinalList[0];
-
-            applyPropsFinal(to, allPropsFinal, toElOption.style);
-
-            if (morphDuration) {
-                const combineResult = combine(fromList, to, elAnimationConfig, copyPropsWhenDivided);
-                processCombineSeparateIndividuals(
-                    'combine', combineResult, seriesModel, toElOption, toDataIndex
+            // A rough strategy: if there are more than one "to", we simply divide "fromList" equally.
+            const fromSingleSegLen = Math.max(1, Math.floor(fromListLen / toListLen));
+            for (
+                let toIdx = 0, fromIdxStart = 0;
+                toIdx < toListLen;
+                toIdx++, fromIdxStart += fromSingleSegLen
+            ) {
+                const fromCount = toIdx + 1 >= toListLen
+                    ? fromListLen - fromIdxStart
+                    : fromSingleSegLen;
+                this._manyToOneForSingleTo(
+                    toIdx, fromIdxStart >= fromListLen ? null : fromIdxStart, fromCount
                 );
             }
         }
 
         else if (type === 'oneToMany') {
-            const from = fromList[0];
-            for (let i = 0; i < toList.length; i++) {
-                applyPropsFinal(toList[i], allPropsFinalList[i], toElOptionList[i].style);
+            // A rough strategy: if there are more than one "from", we simply divide "toList" equally.
+            const toSingleSegLen = Math.max(1, Math.floor(toListLen / fromListLen));
+            for (
+                let toIdxStart = 0, fromIdx = 0;
+                toIdxStart < toListLen;
+                toIdxStart += toSingleSegLen, fromIdx++
+            ) {
+                const toCount = toIdxStart + toSingleSegLen >= toListLen
+                    ? toListLen - toIdxStart
+                    : toSingleSegLen;
+                this._oneToManyForSingleFrom(
+                    toIdxStart, toCount, fromIdx >= fromListLen ? null : fromIdx
+                );
             }
+        }
+    }
+
+    private _oneToOneForSingleTo(
+        // "to" must NOT be null/undefined.
+        toIdx: number,
+        // May `fromIdx >= this._fromList.length`
+        fromIdx: number
+    ): void {
+        const to = this._toList[toIdx];
+        const toElOption = this._toElOptionList[toIdx];
+        const toDataIndex = this._toDataIndices[toIdx];
+        const allPropsFinal = this._allPropsFinalList[toIdx];
+        const from = this._fromList[fromIdx];
+
+        const elAnimationConfig = this._getOrCreateMorphConfig(toDataIndex);
+        const morphDuration = elAnimationConfig.duration;
+
+        if (from && isCombiningPath(from)) {
+            applyPropsFinal(to, allPropsFinal, toElOption.style);
+
             if (morphDuration) {
-                const separateResult = separate(from, toList, elAnimationConfig, copyPropsWhenDivided);
-                processCombineSeparateIndividuals(
-                    'separate', separateResult, seriesModel, toElOptionList, toDataIndices
-                );
+                const combineResult = combine([from], to, elAnimationConfig, copyPropsWhenDivided);
+                this._processResultIndividuals(combineResult, toIdx, null);
             }
+            // The target el will not be displayed and transition from multiple path.
+            // transition on the target el does not make sense.
         }
+        else {
+            const morphFrom = (
+                morphDuration
+                // from === to usually happen in scenarios where internal update like
+                // "dataZoom", "legendToggle" happen. If from is not in any morphing,
+                // we do not need to call `morphPath`.
+                && from
+                && (from !== to || isInAnyMorphing(from))
+            ) ? from : null;
+
+            // See [Case_II] above.
+            // In this case, there is probably `from === to`. And the `transitionFromProps` collecting
+            // does not depends on morphing. So we collect `transitionFromProps` first.
+            const transFromProps = {} as ElementProps;
+            prepareShapeOrExtraTransitionFrom('shape', to, morphFrom, toElOption, transFromProps, false);
+            prepareShapeOrExtraTransitionFrom('extra', to, morphFrom, toElOption, transFromProps, false);
+            prepareTransformTransitionFrom(to, morphFrom, toElOption, transFromProps, false);
+            prepareStyleTransitionFrom(to, morphFrom, toElOption, toElOption.style, transFromProps, false);
 
-    }
+            applyPropsFinal(to, allPropsFinal, toElOption.style);
 
-    reset(): void {
-        this._preventAddTo = true;
-        this.needCheckFromSameEl = false;
-        this._metMorphOption = false;
-        this._fromList.length =
-            this._toList.length =
-            this._toElOptionList.length =
-            this._allPropsFinalList.length =
-            this._toDataIndices.length = 0;
+            if (morphFrom) {
+                morphPath(morphFrom, to, elAnimationConfig);
+            }
+            applyTransitionFrom(to, toDataIndex, toElOption, this._seriesModel, transFromProps, false);
+        }
     }
-}
 
-function processCombineSeparateIndividuals<TYPE extends 'combine' | 'separate'>(
-    type: TYPE,
-    combineSeparateResult: CombineSeparateResult,
-    seriesModel: CustomSeriesModel,
-    toElOptionInput: TYPE extends 'separate' ? CustomElementOption[] : CustomElementOption,
-    dataIndexInput: TYPE extends 'separate' ? number[] : number
-): void {
-    const isSeparate = type === 'separate';
-
-    for (let i = 0; i < combineSeparateResult.count; i++) {
-        const fromIndividual = combineSeparateResult.fromIndividuals[i];
-        const toIndividual = combineSeparateResult.toIndividuals[i];
-
-        const toElOption = isSeparate
-            ? (toElOptionInput as CustomElementOption[])[i]
-            : (toElOptionInput as CustomElementOption);
-        const dataIndex = isSeparate
-            ? (dataIndexInput as number[])[i]
-            : (dataIndexInput as number);
-
-        const transFromProps = {} as ElementProps;
-        prepareTransformTransitionFrom(
-            toIndividual, fromIndividual, toElOption, transFromProps, false
-        );
-        prepareStyleTransitionFrom(
-            toIndividual, fromIndividual, toElOption, toElOption.style, transFromProps, false
-        );
-        applyTransitionFrom(
-            toIndividual, dataIndex, toElOption, seriesModel, transFromProps, false
-        );
+    private _manyToOneForSingleTo(
+        // "to" must NOT be null/undefined.
+        toIdx: number,
+        // May be null.
+        fromIdxStart: number,
+        fromCount: number
+    ): void {
+        const to = this._toList[toIdx];
+        const toElOption = this._toElOptionList[toIdx];
+        const allPropsFinal = this._allPropsFinalList[toIdx];
+
+        applyPropsFinal(to, allPropsFinal, toElOption.style);
+
+        const elAnimationConfig = this._getOrCreateMorphConfig(this._toDataIndices[toIdx]);
+        if (elAnimationConfig.duration && fromIdxStart != null) {
+            const combineFromList = [];
+            for (let fromIdx = fromIdxStart; fromIdx < fromCount; fromIdx++) {
+                combineFromList.push(this._fromList[fromIdx]);
+            }
+            const combineResult = combine(combineFromList, to, elAnimationConfig, copyPropsWhenDivided);
+            this._processResultIndividuals(combineResult, toIdx, null);
+        }
     }
-}
 
-// At present only one 'morph' enabled in each data item.
-function findMayMorphFrom(el: Element): graphicUtil.Path {
-    if (!el) {
-        return;
+    private _oneToManyForSingleFrom(
+        // "to" must NOT be null/undefined.
+        toIdxStart: number,
+        toCount: number,
+        // May be null
+        fromIdx: number
+    ): void {
+        const from = fromIdx == null ? null : this._fromList[fromIdx];
+        const toList = this._toList;
+
+        const separateToList = [];
+        for (let toIdx = toIdxStart; toIdx < toCount; toIdx++) {
+            const to = toList[toIdx];
+            applyPropsFinal(to, this._allPropsFinalList[toIdx], this._toElOptionList[toIdx].style);
+            separateToList.push(to);
+        }
+
+        const elAnimationConfig = this._getOrCreateMorphConfig(this._toDataIndices[toIdxStart]);
+        if (elAnimationConfig.duration && from) {
+            const separateResult = separate(from, separateToList, elAnimationConfig, copyPropsWhenDivided);
+            this._processResultIndividuals(separateResult, toIdxStart, toCount);
+        }
     }
-    if (inner(el).morphOption && isPath(el)) {
-        return el;
+
+    private _processResultIndividuals(
+        combineSeparateResult: CombineSeparateResult,
+        toIdxStart: number,
+        toCount: number
+    ): void {
+        const isSeparate = toCount != null;
+
+        for (let i = 0; i < combineSeparateResult.count; i++) {
+            const fromIndividual = combineSeparateResult.fromIndividuals[i];
+            const toIndividual = combineSeparateResult.toIndividuals[i];
+            // Here it's a trick:
+            // For "combine" case, all of the `toIndividuals` map to the same `toIdx`.
+            // For "separate" case, the `toIndividuals` map to some certain segment of `_toList` accurately.
+            const toIdx = toIdxStart + (isSeparate ? i : 0);
+
+            const toElOption = this._toElOptionList[toIdx];
+            const dataIndex = this._toDataIndices[toIdx];
+
+            const transFromProps = {} as ElementProps;
+            prepareTransformTransitionFrom(
+                toIndividual, fromIndividual, toElOption, transFromProps, false
+            );
+            prepareStyleTransitionFrom(
+                toIndividual, fromIndividual, toElOption, toElOption.style, transFromProps, false
+            );
+            applyTransitionFrom(
+                toIndividual, dataIndex, toElOption, this._seriesModel, transFromProps, false
+            );
+        }
     }
-    if (el.isGroup) {
-        const elGroup = el as graphicUtil.Group;
-        const children = elGroup.childrenRef();
-        for (let i = 0; i < children.length; i++) {
-            const path = findMayMorphFrom(children[i]);
-            if (path) {
-                return path;
+
+    _getOrCreateMorphConfig(dataIndex: number): CombineSeparateConfig {
+        const morphConfigList = this._morphConfigList;
+        let config = morphConfigList[dataIndex];
+        if (config) {
+            return config;
+        }
+
+        let duration: number;
+        let easing: AnimationEasing;
+        let delay: number;
+        const seriesModel = this._seriesModel;
+        const transOpt = this._transOpt;
+
+        if (seriesModel.isAnimationEnabled()) {
+            // PENDING: refactor? this is the same logic as `src/util/graphic.ts#animateOrSetProps`.
+            let animationPayload: PayloadAnimationPart;
+            if (seriesModel && seriesModel.ecModel) {
+                const updatePayload = seriesModel.ecModel.getUpdatePayload();
+                animationPayload = (updatePayload && updatePayload.animation) as PayloadAnimationPart;
+            }
+            if (animationPayload) {
+                duration = animationPayload.duration || 0;
+                easing = animationPayload.easing || 'cubicOut';
+                delay = animationPayload.delay || 0;
+            }
+            else {
+                easing = seriesModel.get('animationEasingUpdate');
+                const delayOption = seriesModel.get('animationDelayUpdate');
+                delay = isFunction(delayOption) ? delayOption(dataIndex) : delayOption;
+                const durationOption = seriesModel.get('animationDurationUpdate');
+                duration = isFunction(durationOption) ? durationOption(dataIndex) : durationOption;
             }
         }
+
+        config = {
+            duration: duration || 0,
+            delay: delay,
+            easing: easing,
+            dividingMethod: transOpt ? transOpt.dividingMethod : null
+        };
+        morphConfigList[dataIndex] = config;
+
+        return config;
     }
-}
 
-function prepareMorphElementAnimateConfig(
-    seriesModel: CustomSeriesModel,
-    dataIndex: number,
-    transOpt: SeriesModel['__transientTransitionOpt'] // may be null/undefined
-): CombineSeparateConfig {
-    let duration: number;
-    let easing: AnimationEasing;
-    let delay: number;
-
-    // PENDING: refactor? this is the same logic as `src/util/graphic.ts#animateOrSetProps`.
-    let animationPayload: PayloadAnimationPart;
-    if (seriesModel && seriesModel.ecModel) {
-        const updatePayload = seriesModel.ecModel.getUpdatePayload();
-        animationPayload = (updatePayload && updatePayload.animation) as PayloadAnimationPart;
-    }
-    if (animationPayload) {
-        duration = animationPayload.duration || 0;
-        easing = animationPayload.easing || 'cubicOut';
-        delay = animationPayload.delay || 0;
+    reset(type: MorphPreparationType): void {
+        // `this._morphConfigList` can be kept. It only related to `dataIndex`.
+        this._type = type;
+        this._fromList.length =
+            this._toList.length =
+            this._toElOptionList.length =
+            this._allPropsFinalList.length =
+            this._toDataIndices.length = 0;
     }
-    else {
-        easing = seriesModel.get('animationEasingUpdate');
-        const delayOption = seriesModel.get('animationDelayUpdate');
-        delay = isFunction(delayOption) ? delayOption(dataIndex) : delayOption;
-        const durationOption = seriesModel.get('animationDurationUpdate');
-        duration = isFunction(durationOption) ? durationOption(dataIndex) : durationOption;
-    }
-
-    return {
-        duration: duration || 0,
-        delay: delay,
-        easing: easing,
-        dividingMethod: transOpt ? transOpt.dividingMethod : null
-    };
 }
 
 function copyPropsWhenDivided(
@@ -2592,12 +2703,14 @@ function copyPropsWhenDivided(
     tarPath: graphicUtil.Path,
     willClone: boolean
 ): void {
-    tarPath.x = srcPath.x;
-    tarPath.y = srcPath.y;
-    tarPath.scaleX = srcPath.scaleX;
-    tarPath.scaleY = srcPath.scaleY;
-    tarPath.originX = srcPath.originX;
-    tarPath.originY = srcPath.originY;
+    // Do not copy transform props.
+    // Sub paths are transfrom based on their host path.
+    // tarPath.x = srcPath.x;
+    // tarPath.y = srcPath.y;
+    // tarPath.scaleX = srcPath.scaleX;
+    // tarPath.scaleY = srcPath.scaleY;
+    // tarPath.originX = srcPath.originX;
+    // tarPath.originY = srcPath.originY;
 
     // If just carry the style, will not be modifed, so do not copy.
     tarPath.style = willClone
diff --git a/test/custom-shape-morphing2.html b/test/custom-shape-morphing2.html
index 28c95ec..ac12738 100644
--- a/test/custom-shape-morphing2.html
+++ b/test/custom-shape-morphing2.html
@@ -92,19 +92,10 @@ under the License.
                     '#c23531', '#2f4554', '#61a0a8', '#d48265', '#91c7ae', '#749f83',
                     '#ca8622', '#bda29a', '#6e7074', '#546570', '#c4ccd3'
                 ];
-                const zTagColorMap = {};
-                let zTagColorIndex = 0;
-                function getZTagColor(zTag) {
-                    if (!zTagColorMap[zTag]) {
-                        zTagColorMap[zTag] = Z_TAG_COLORS[zTagColorIndex];
-                        zTagColorIndex++;
-                        if (zTagColorIndex >= Z_TAG_COLORS.length) {
-                            zTagColorIndex = 0;
-                        }
-                    }
-                    return zTagColorMap[zTag]
-                }
-
+                const Z_TAG_COLORS_2 = [
+                    '#51689b', '#ce5c5c', '#fbc357', '#8fbf8f', '#659d84', '#fb8e6a', '#c77288', '#786090',
+                    '#91c4c5', '#6890ba'
+                ];
                 const SYMBOL_PATHS = [
                     'path://m67.25,28.9c27.42,-69.1 134.84,0 0,88.85c-134.84,-88.85 -27.42,-157.96 0,-88.85z',
                     'path://M16 0c-8.837 0-16 7.163-16 16s7.163 16 16 16 16-7.163 16-16-7.163-16-16-16zM22 8c1.105 0 2 1.343 2 3s-0.895 3-2 3-2-1.343-2-3 0.895-3 2-3zM10 8c1.105 0 2 1.343 2 3s-0.895 3-2 3-2-1.343-2-3 0.895-3 2-3zM16 28c-5.215 0-9.544-4.371-10-9.947 2.93 1.691 6.377 2.658 10 2.658s7.070-0.963 10-2.654c-0.455 5.576-4.785 9.942-10 9.942z',
@@ -113,25 +104,10 @@ under the License.
                     'path://M237.062,81.761L237.062,81.761c-12.144-14.24-25.701-20.1-40.68-19.072 c-10.843,0.747-20.938,5.154-30.257,13.127c-9.51-5.843-19.8-9.227-30.859-10.366c0.521-3.197,1.46-6.306,2.85-9.363 c3.458-7.038,8.907-12.741,16.331-17.296c-5.609-3.384-11.227-6.799-16.854-10.279c-16.257,8.104-25.06,20.601-26.463,38.417 c-7.599,1.705-14.685,4.486-21.247,8.437c-9.164-7.677-18.996-11.917-29.496-12.632c-14.819-0.998-28.467,4.787-40.938,18.827 C6.445,96.182,0,114.867,0,136.242c-0.0 [...]
                     'path://M237.062,81.761L237.062,81.761c-12.144-14.24-25.701-20.1-40.68-19.072 c-10.843,0.747-20.938,5.154-30.257,13.127c-9.51-5.843-19.8-9.227-30.859-10.366c0.521-3.197,1.46-6.306,2.85-9.363 c3.458-7.038,8.907-12.741,16.331-17.296c-5.609-3.384-11.227-6.799-16.854-10.279c-16.257,8.104-25.06,20.601-26.463,38.417 c-7.599,1.705-14.685,4.486-21.247,8.437c-9.164-7.677-18.996-11.917-29.496-12.632c-14.819-0.998-28.467,4.787-40.938,18.827 C6.445,96.182,0,114.867,0,136.242c-0.0 [...]
                 ];
-                const zTagSymbolMap = {};
-                let zTagSymbolIndex = 0;
-                function getZTagSymbolPath(zTag) {
-                    if (!zTagSymbolMap[zTag]) {
-                        zTagSymbolMap[zTag] = SYMBOL_PATHS[zTagSymbolIndex];
-                        zTagSymbolIndex++;
-                        if (zTagSymbolIndex >= SYMBOL_PATHS.length) {
-                            zTagSymbolIndex = 0;
-                        }
-                    }
-                    return zTagSymbolMap[zTag]
-                }
-
-
-                var COUNT = 50;
                 var CONTENT_COLOR = '#37A2DA';
-
                 var ANIMATION_DURATION_UPDATE = 1500;
 
+
                 // var rawData = initRawData(COUNT);
                 // console.log(JSON.stringify(rawData));
                 var rawData = [[1425139200000,34,0.13,2,"MD","ZD","P0"],[1425225600000,28,0.71,1.5,"MB","ZD","P1"],[1425312000000,23,0.9,2.8,"MA","ZC","P2"],[1425398400000,21,0.58,6,"MB","ZC","P3"],[1425484800000,14,0.1,1.6,"MC","ZA","P4"],[1425571200000,21,0.6,7.7,"MC","ZA","P5"],[1425657600000,23,0.31,2.6,"MC","ZC","P6"],[1425744000000,34,0.74,2.4,"MD","ZE","P7"],[1425830400000,14,0.59,2.3,"MB","ZD","P8"],[1425916800000,18,0.85,5.1,"MB","ZB","P9"],[1426003200000,36,0.96,1.2,"MC","ZC"," [...]
@@ -142,6 +118,25 @@ under the License.
                 var RAW_CLUSTER_CENTERS_DIMENSIONS = ['COUNT', 'CLUSTER_IDX', 'CLUSTER_CENTER_ATA', 'CLUSTER_CENTER_STE'];
 
 
+                function getFromPalette(value, palette) {
+                    if (!palette.__colorMap) {
+                        palette.__colorMap = {};
+                    }
+                    if (palette.__colorIdx == null) {
+                        palette.__colorIdx = 0;
+                    }
+                    if (!palette.__colorMap[value]) {
+                        palette.__colorMap[value] = palette[palette.__colorIdx];
+                        palette.__colorIdx++;
+                        if (palette.__colorIdx >= palette.length) {
+                            palette.__colorIdx = 0;
+                        }
+                    }
+                    return palette.__colorMap[value];
+                }
+
+
+
                 var baseOption = {
                     dataset: [{
                         id: 'raw',
@@ -224,307 +219,344 @@ under the License.
                 };
                 var BG_COLOR = '#000';
 
-                var optionCreators = {
 
-                    'Scatter_ATA_STE': function (datasetId) {
+
+                function makeScatterOptionCreator(renderItem) {
+                    return function () {
+                        var datasetId = 'rawClusters';
                         return {
-                            backgroundColor: BG_COLOR,
-                            grid: {
-                                containLabel: true
-                            },
-                            xAxis: {
-                                name: 'STE',
-                                ...AXIS_STYLE
-                            },
-                            yAxis: {
-                                name: 'ATA',
-                                ...AXIS_STYLE
-                            },
-                            dataZoom: [{
-                                type: 'slider',
-                            }, {
-                                type: 'inside'
-                            }],
-                            series: {
-                                type: 'custom',
-                                coordinateSystem: 'cartesian2d',
-                                animationDurationUpdate: ANIMATION_DURATION_UPDATE,
-                                datasetId: datasetId,
-                                encode: {
-                                    itemName: 'ID',
-                                    x: 'STE',
-                                    y: 'ATA',
-                                    tooltip: ['STE', 'ATA']
+                            datasetId: datasetId,
+                            option: {
+                                backgroundColor: BG_COLOR,
+                                grid: {
+                                    containLabel: true
+                                },
+                                xAxis: {
+                                    name: 'STE',
+                                    ...AXIS_STYLE
+                                },
+                                yAxis: {
+                                    name: 'ATA',
+                                    ...AXIS_STYLE
                                 },
-                                renderItem: function (params, api) {
-                                    var pos = api.coord([
-                                        api.value('STE'),
-                                        api.value('ATA')
-                                    ]);
-                                    var zTagVal = api.value('Z_TAG');
-                                    var color = getZTagColor(zTagVal);
-                                    // var clusterIndex = api.value('CLUSTER_IDX');
-                                    var symbolPath = getZTagSymbolPath(zTagVal);
-                                    return {
-                                        type: 'circle',
-                                        morph: true,
-                                        shape: {
-                                            cx: pos[0],
-                                            cy: pos[1],
-                                            r: 10
-                                        },
-                                        style: {
-                                            fill: color,
-                                            stroke: '#555',
-                                            lineWidth: 0
-                                        },
-                                        transition: ['shape', 'style']
-                                    };
+                                dataZoom: [{
+                                    type: 'slider',
+                                }, {
+                                    type: 'inside'
+                                }],
+                                series: {
+                                    type: 'custom',
+                                    coordinateSystem: 'cartesian2d',
+                                    animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                                    datasetId: datasetId,
+                                    encode: {
+                                        itemName: 'ID',
+                                        x: 'STE',
+                                        y: 'ATA',
+                                        tooltip: ['STE', 'ATA']
+                                    },
+                                    renderItem: renderItem
                                 }
                             }
-                        };
-                    },
+                        }
+                    };
+                }
 
-                    'Scatter_SVG_ATA_STE': function (datasetId) {
-                        return {
-                            backgroundColor: BG_COLOR,
-                            grid: {
-                                containLabel: true
-                            },
-                            xAxis: {
-                                name: 'STE',
-                                ...AXIS_STYLE
-                            },
-                            yAxis: {
-                                name: 'ATA',
-                                ...AXIS_STYLE
-                            },
-                            dataZoom: [{
-                                type: 'slider',
-                            }, {
-                                type: 'inside'
-                            }],
-                            series: {
-                                type: 'custom',
-                                coordinateSystem: 'cartesian2d',
-                                animationDurationUpdate: ANIMATION_DURATION_UPDATE,
-                                datasetId: datasetId,
-                                encode: {
-                                    itemName: 'ID',
-                                    x: 'STE',
-                                    y: 'ATA',
-                                    tooltip: ['STE', 'ATA']
+
+                var optionCreators = {
+
+                    'Scatter_circle_ATA_STE': makeScatterOptionCreator(
+                        function (params, api) {
+                            var pos = api.coord([
+                                api.value('STE'),
+                                api.value('ATA')
+                            ]);
+                            var zTagVal = api.value('Z_TAG');
+                            var color = getFromPalette(zTagVal, Z_TAG_COLORS);
+                            var symbolPath = getFromPalette(zTagVal, SYMBOL_PATHS);
+                            return {
+                                type: 'circle',
+                                morph: true,
+                                shape: {
+                                    cx: pos[0],
+                                    cy: pos[1],
+                                    r: 10
                                 },
-                                renderItem: function (params, api) {
-                                    var pos = api.coord([
-                                        api.value('STE'),
-                                        api.value('ATA')
-                                    ]);
-                                    var zTagVal = api.value('Z_TAG');
-                                    var color = getZTagColor(zTagVal);
-                                    // var clusterIndex = api.value('CLUSTER_IDX');
-                                    var symbolPath = getZTagSymbolPath(zTagVal);
-                                    return {
-                                        type: 'path',
-                                        morph: true,
-                                        x: pos[0],
-                                        y: pos[1],
-                                        shape: {
-                                            pathData: symbolPath,
-                                            width: 40,
-                                            height: 40
-                                        },
-                                        style: {
-                                            fill: color,
-                                            stroke: '#555',
-                                            lineWidth: 0
-                                        },
-                                        transition: ['style']
-                                    };
-                                }
-                            }
-                        };
-                    },
+                                style: {
+                                    fill: color
+                                },
+                                transition: ['shape', 'style']
+                            };
+                        }
+                    ),
+
+                    'Scatter_singleSVG_ATA_STE': makeScatterOptionCreator(
+                        function (params, api) {
+                            var pos = api.coord([
+                                api.value('STE'),
+                                api.value('ATA')
+                            ]);
+                            var zTagVal = api.value('Z_TAG');
+                            var color = getFromPalette(zTagVal, Z_TAG_COLORS);
+                            var symbolPath = getFromPalette(zTagVal, SYMBOL_PATHS);
+                            return {
+                                type: 'path',
+                                morph: true,
+                                x: pos[0],
+                                y: pos[1],
+                                shape: {
+                                    pathData: symbolPath,
+                                    width: 40,
+                                    height: 40
+                                },
+                                style: {
+                                    fill: color
+                                },
+                                transition: ['style']
+                            };
+                        }
+                    ),
+
+                    'Scatter_groupShape_ATA_STE': makeScatterOptionCreator(
+                        function (params, api) {
+                            var pos = api.coord([
+                                api.value('STE'),
+                                api.value('ATA')
+                            ]);
+                            var zTagVal = api.value('Z_TAG');
+                            var color1 = getFromPalette(zTagVal, Z_TAG_COLORS);
+                            var color2 = getFromPalette(zTagVal, Z_TAG_COLORS_2);
+                            return {
+                                type: 'group',
+                                x: pos[0],
+                                y: pos[1],
+                                children: [{
+                                    type: 'polygon',
+                                    morph: true,
+                                    shape: {
+                                        points: [
+                                            [-40, -2],
+                                            [40, -2],
+                                            [0, -35]
+                                        ]
+                                    },
+                                    style: {
+                                        fill: color1,
+                                        stroke: '#333',
+                                        lineWidth: 1
+                                    },
+                                    transition: ['shape', 'style']
+                                }, {
+                                    type: 'rect',
+                                    morph: true,
+                                    shape: {
+                                        x: -20,
+                                        y: 0,
+                                        width: 40,
+                                        height: 30
+                                    },
+                                    style: {
+                                        fill: color2,
+                                        stroke: '#333',
+                                        lineWidth: 1
+                                    },
+                                    transition: ['shape', 'style']
+                                }]
+                            };
+                        }
+                    ),
 
-                    'Bar_mSum_ATA': function (datasetId) {
+                    'Bar_mSum_ATA': function () {
+                        var datasetId = 'mTagSum'
                         return {
-                            backgroundColor: BG_COLOR,
-                            grid: {
-                                containLabel: true
-                            },
-                            xAxis: {
-                                type: 'category',
-                                ...AXIS_STYLE
-                            },
-                            yAxis: {
-                                ...AXIS_STYLE
-                            },
-                            dataZoom: [{
-                                type: 'slider',
-                            }, {
-                                type: 'inside'
-                            }],
-                            series: {
-                                type: 'custom',
-                                coordinateSystem: 'cartesian2d',
-                                animationDurationUpdate: ANIMATION_DURATION_UPDATE,
-                                datasetId: datasetId,
-                                encode: {
-                                    x: 'M_TAG',
-                                    y: 'ATA',
-                                    tooltip: ['M_TAG', 'ATA']
+                            datasetId: datasetId,
+                            option: {
+                                backgroundColor: BG_COLOR,
+                                grid: {
+                                    containLabel: true
                                 },
-                                renderItem: function (params, api) {
-                                    var mTagVal = api.value('M_TAG');
-                                    var ataVal = api.value('ATA');
-                                    var tarPos = api.coord([mTagVal, ataVal]);
-                                    var zeroPos = api.coord([mTagVal, 0]);
-                                    var size = api.size([mTagVal, ataVal]);
-                                    var width = size[0] * 0.4;
-                                    return {
-                                        type: 'rect',
-                                        morph: true,
-                                        shape: {
-                                            x: tarPos[0] - width / 2,
-                                            y: tarPos[1],
-                                            height: zeroPos[1] - tarPos[1],
-                                            width: width,
-                                        },
-                                        style: {
-                                            fill: CONTENT_COLOR,
-                                            stroke: '#555',
-                                            lineWidth: 0
-                                        },
-                                        transition: ['shape', 'style']
-                                    };
+                                xAxis: {
+                                    type: 'category',
+                                    ...AXIS_STYLE
+                                },
+                                yAxis: {
+                                    ...AXIS_STYLE
+                                },
+                                dataZoom: [{
+                                    type: 'slider',
+                                }, {
+                                    type: 'inside'
+                                }],
+                                series: {
+                                    type: 'custom',
+                                    coordinateSystem: 'cartesian2d',
+                                    animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                                    datasetId: datasetId,
+                                    encode: {
+                                        x: 'M_TAG',
+                                        y: 'ATA',
+                                        tooltip: ['M_TAG', 'ATA']
+                                    },
+                                    renderItem: function (params, api) {
+                                        var mTagVal = api.value('M_TAG');
+                                        var ataVal = api.value('ATA');
+                                        var tarPos = api.coord([mTagVal, ataVal]);
+                                        var zeroPos = api.coord([mTagVal, 0]);
+                                        var size = api.size([mTagVal, ataVal]);
+                                        var width = size[0] * 0.4;
+                                        return {
+                                            type: 'rect',
+                                            morph: true,
+                                            shape: {
+                                                x: tarPos[0] - width / 2,
+                                                y: tarPos[1],
+                                                height: zeroPos[1] - tarPos[1],
+                                                width: width,
+                                            },
+                                            style: {
+                                                fill: CONTENT_COLOR,
+                                                stroke: '#555',
+                                                lineWidth: 0
+                                            },
+                                            transition: ['shape', 'style']
+                                        };
+                                    }
                                 }
                             }
                         };
                     },
 
-                    'Pie_mSum_ATA': function (datasetId) {
+                    'Pie_mSum_ATA': function () {
+                        var datasetId = 'mTagSum';
                         return {
-                            backgroundColor: BG_COLOR,
-                            series: {
-                                type: 'custom',
-                                coordinateSystem: 'none',
-                                animationDurationUpdate: ANIMATION_DURATION_UPDATE,
-                                datasetId: datasetId,
-                                encode: {
-                                    itemName: 'M_TAG',
-                                    value: 'ATA',
-                                    tooltip: 'ATA'
-                                },
-                                renderItem: function (params, api) {
-                                    var context = params.context;
-                                    if (!context.layout) {
-                                        context.layout = true;
-                                        var totalValue = 0;
-                                        for (var i = 0; i < params.dataInsideLength; i++) {
-                                            totalValue += api.value('ATA', i);
-                                        }
-                                        var angles = [];
-                                        var colors = [];
-                                        var currentAngle = -Math.PI / 2;
-                                        for (var i = 0; i < params.dataInsideLength; i++) {
-                                            colors.push(PIE_COLORS[i]);
-                                            var angle = api.value('ATA', i) / totalValue * Math.PI * 2;
-                                            angles.push([currentAngle, angle + currentAngle - 0.01]);
-                                            currentAngle += angle;
+                            datasetId: datasetId,
+                            option: {
+                                backgroundColor: BG_COLOR,
+                                series: {
+                                    type: 'custom',
+                                    coordinateSystem: 'none',
+                                    animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                                    datasetId: datasetId,
+                                    encode: {
+                                        itemName: 'M_TAG',
+                                        value: 'ATA',
+                                        tooltip: 'ATA'
+                                    },
+                                    renderItem: function (params, api) {
+                                        var context = params.context;
+                                        if (!context.layout) {
+                                            context.layout = true;
+                                            var totalValue = 0;
+                                            for (var i = 0; i < params.dataInsideLength; i++) {
+                                                totalValue += api.value('ATA', i);
+                                            }
+                                            var angles = [];
+                                            var colors = [];
+                                            var currentAngle = -Math.PI / 2;
+                                            for (var i = 0; i < params.dataInsideLength; i++) {
+                                                colors.push(PIE_COLORS[i]);
+                                                var angle = api.value('ATA', i) / totalValue * Math.PI * 2;
+                                                angles.push([currentAngle, angle + currentAngle - 0.01]);
+                                                currentAngle += angle;
+                                            }
+                                            context.angles = angles;
+                                            context.colors = colors;
                                         }
-                                        context.angles = angles;
-                                        context.colors = colors;
-                                    }
 
-                                    var width = chart.getWidth();
-                                    var height = chart.getHeight();
-                                    return {
-                                        type: 'sector',
-                                        morph: true,
-                                        shape: {
-                                            cx: width / 2,
-                                            cy: height / 2,
-                                            r: Math.min(width, height) / 3,
-                                            r0: Math.min(width, height) / 5,
-                                            startAngle: context.angles[params.dataIndex][0],
-                                            endAngle: context.angles[params.dataIndex][1],
-                                            clockwise: true
-                                        },
-                                        style: {
-                                            // fill: CONTENT_COLOR,
-                                            fill: context.colors[params.dataIndex],
-                                            stroke: '#555',
-                                            lineWidth: 0,
-                                            strokeNoScale: true
-                                        },
-                                        transition: ['shape', 'style']
-                                    };
+                                        var width = chart.getWidth();
+                                        var height = chart.getHeight();
+                                        return {
+                                            type: 'sector',
+                                            morph: true,
+                                            shape: {
+                                                cx: width / 2,
+                                                cy: height / 2,
+                                                r: Math.min(width, height) / 3,
+                                                r0: Math.min(width, height) / 5,
+                                                startAngle: context.angles[params.dataIndex][0],
+                                                endAngle: context.angles[params.dataIndex][1],
+                                                clockwise: true
+                                            },
+                                            style: {
+                                                // fill: CONTENT_COLOR,
+                                                fill: context.colors[params.dataIndex],
+                                                stroke: '#555',
+                                                lineWidth: 0,
+                                                strokeNoScale: true
+                                            },
+                                            transition: ['shape', 'style']
+                                        };
+                                    }
                                 }
                             }
                         };
                     },
 
-                    'Scatter_ATA_STE_Cluster_Center': function (datasetId) {
+                    'Scatter_ATA_STE_Cluster_Center': function () {
+                        var datasetId = 'rawClusterCenters';
                         return {
-                            backgroundColor: BG_COLOR,
-                            grid: {
-                                containLabel: true
-                            },
-                            xAxis: {
-                                name: 'STE',
-                                ...AXIS_STYLE
-                            },
-                            yAxis: {
-                                name: 'ATA',
-                                ...AXIS_STYLE
-                            },
-                            dataZoom: [{
-                                type: 'slider',
-                            }, {
-                                type: 'inside'
-                            }],
-                            series: {
-                                type: 'custom',
-                                coordinateSystem: 'cartesian2d',
-                                animationDurationUpdate: ANIMATION_DURATION_UPDATE,
-                                datasetId: datasetId,
-                                encode: {
-                                    x: 'CLUSTER_CENTER_STE',
-                                    y: 'CLUSTER_CENTER_ATA',
-                                    tooltip: ['CLUSTER_CENTER_STE', 'CLUSTER_CENTER_ATA']
+                            datasetId: datasetId,
+                            option: {
+                                backgroundColor: BG_COLOR,
+                                grid: {
+                                    containLabel: true
                                 },
-                                renderItem: function (params, api) {
-                                    var context = params.context;
-                                    if (!context.layout) {
-                                        context.layout = true;
-                                        context.totalCount = 0;
-                                        for (var i = 0; i < params.dataInsideLength; i++) {
-                                            context.totalCount += api.value('COUNT', i);
+                                xAxis: {
+                                    name: 'STE',
+                                    ...AXIS_STYLE
+                                },
+                                yAxis: {
+                                    name: 'ATA',
+                                    ...AXIS_STYLE
+                                },
+                                dataZoom: [{
+                                    type: 'slider',
+                                }, {
+                                    type: 'inside'
+                                }],
+                                series: {
+                                    type: 'custom',
+                                    coordinateSystem: 'cartesian2d',
+                                    animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                                    datasetId: datasetId,
+                                    encode: {
+                                        x: 'CLUSTER_CENTER_STE',
+                                        y: 'CLUSTER_CENTER_ATA',
+                                        tooltip: ['CLUSTER_CENTER_STE', 'CLUSTER_CENTER_ATA']
+                                    },
+                                    renderItem: function (params, api) {
+                                        var context = params.context;
+                                        if (!context.layout) {
+                                            context.layout = true;
+                                            context.totalCount = 0;
+                                            for (var i = 0; i < params.dataInsideLength; i++) {
+                                                context.totalCount += api.value('COUNT', i);
+                                            }
                                         }
-                                    }
 
-                                    var pos = api.coord([
-                                        api.value('CLUSTER_CENTER_STE'),
-                                        api.value('CLUSTER_CENTER_ATA')
-                                    ]);
-                                    var count = api.value('COUNT');
-                                    var radius = count / context.totalCount * 100 + 10;
-                                    return {
-                                        type: 'circle',
-                                        morph: true,
-                                        shape: {
-                                            cx: pos[0],
-                                            cy: pos[1],
-                                            r: radius,
-                                        },
-                                        style: {
-                                            // fill: CONTENT_COLOR,
-                                            fill: CLUSTER_COLORS[params.dataIndex],
-                                            stroke: '#555',
-                                            lineWidth: 0
-                                        },
-                                        transition: ['shape', 'style']
-                                    };
+                                        var pos = api.coord([
+                                            api.value('CLUSTER_CENTER_STE'),
+                                            api.value('CLUSTER_CENTER_ATA')
+                                        ]);
+                                        var count = api.value('COUNT');
+                                        var radius = count / context.totalCount * 100 + 10;
+                                        return {
+                                            type: 'circle',
+                                            morph: true,
+                                            shape: {
+                                                cx: pos[0],
+                                                cy: pos[1],
+                                                r: radius,
+                                            },
+                                            style: {
+                                                // fill: CONTENT_COLOR,
+                                                fill: CLUSTER_COLORS[params.dataIndex],
+                                                stroke: '#555',
+                                                lineWidth: 0
+                                            },
+                                            transition: ['shape', 'style']
+                                        };
+                                    }
                                 }
                             }
                         };
@@ -532,6 +564,25 @@ under the License.
                 };
 
 
+
+                var optionList = [];
+                var buttons = [];
+                echarts.util.each(optionCreators, function (creator, key) {
+                    var optionWrap = creator();
+                    optionList.push({
+                        key: key,
+                        dataMetaKey: optionWrap.datasetId,
+                        option: optionWrap.option
+                    });
+                    buttons.push({
+                        text: key,
+                        onclick: function () {
+                            player.go(key);
+                        }
+                    });
+                });
+
+
                 var player = transitionPlayer.create({
                     chart: function () {
                         return chart;
@@ -557,27 +608,7 @@ under the License.
                             uniqueDimension: 'CLUSTER_IDX'
                         }
                     },
-                    optionList: [{
-                        key: 'Scatter_ATA_STE',
-                        dataMetaKey: 'rawClusters',
-                        option: optionCreators['Scatter_ATA_STE']('rawClusters')
-                    }, {
-                        key: 'Scatter_SVG_ATA_STE',
-                        dataMetaKey: 'rawClusters',
-                        option: optionCreators['Scatter_SVG_ATA_STE']('rawClusters')
-                    }, {
-                        key: 'Bar_mTagSum_ATA',
-                        dataMetaKey: 'mTagSum',
-                        option: optionCreators['Bar_mSum_ATA']('mTagSum')
-                    }, {
-                        key: 'Pie_mTagSum_ATA',
-                        dataMetaKey: 'mTagSum',
-                        option: optionCreators['Pie_mSum_ATA']('mTagSum')
-                    }, {
-                        key: 'Scatter_ATA_STE_Cluster_Center',
-                        dataMetaKey: 'rawClusterCenters',
-                        option: optionCreators['Scatter_ATA_STE_Cluster_Center']('rawClusterCenters')
-                    }]
+                    optionList: optionList
                 });
 
 
@@ -591,32 +622,7 @@ under the License.
                     option: baseOption,
                     lazyUpdate: true,
                     height: 600,
-                    buttons: [{
-                        text: 'Bar_mTagSum_ATA',
-                        onclick: function () {
-                            player.go('Bar_mTagSum_ATA');
-                        }
-                    }, {
-                        text: 'Scatter_ATA_STE',
-                        onclick: function () {
-                            player.go('Scatter_ATA_STE');
-                        }
-                    }, {
-                        text: 'Scatter_SVG_ATA_STE',
-                        onclick: function () {
-                            player.go('Scatter_SVG_ATA_STE');
-                        }
-                    }, {
-                        text: 'Pie_mTagSum_ATA',
-                        onclick: function () {
-                            player.go('Pie_mTagSum_ATA');
-                        }
-                    }, {
-                        text: 'Scatter_ATA_STE_Cluster_Center',
-                        onclick: function () {
-                            player.go('Scatter_ATA_STE_Cluster_Center');
-                        }
-                    }, {
+                    buttons: buttons.concat([{
                         text: 'resizeToSmall',
                         onclick: function () {
                             chart.resize({
@@ -632,10 +638,10 @@ under the License.
                                 height: 600
                             });
                         }
-                    }]
+                    }])
                 });
 
-                player.go('Scatter_SVG_ATA_STE');
+                player.go('Scatter_singleSVG_ATA_STE');
 
             });
 


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