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

[incubator-echarts] branch custom-series-enhance updated: feature: enable "enterFrom" "leaveTo" "transition" animation setting in custom series.

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

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


The following commit(s) were added to refs/heads/custom-series-enhance by this push:
     new dfad53d  feature: enable "enterFrom" "leaveTo" "transition" animation setting in custom series.
dfad53d is described below

commit dfad53d54fc1ca02d1e605b114b087fda6d7e623
Author: 100pah <su...@gmail.com>
AuthorDate: Thu Jun 4 11:39:59 2020 +0800

    feature: enable "enterFrom" "leaveTo" "transition" animation setting in custom series.
---
 src/chart/custom.ts                       | 513 ++++++++++++++++++-------
 src/component/visualMap/PiecewiseModel.ts |   2 +-
 src/util/graphic.ts                       |  51 ++-
 test/custom-transition-texture.js         |   1 +
 test/custom-transition.html               | 613 ++++++++++++++++++++++++------
 5 files changed, 926 insertions(+), 254 deletions(-)

diff --git a/src/chart/custom.ts b/src/chart/custom.ts
index 2bb85c9..cfd1a1f 100644
--- a/src/chart/custom.ts
+++ b/src/chart/custom.ts
@@ -19,7 +19,9 @@
 
 
 import {__DEV__} from '../config';
-import * as zrUtil from 'zrender/src/core/util';
+import {
+    hasOwn, assert, isString, retrieve2, retrieve3, defaults, each, keys, isArrayLike
+} from 'zrender/src/core/util';
 import * as graphicUtil from '../util/graphic';
 import {getDefaultLabel} from './helper/labelHelper';
 import createListFromArray from './helper/createListFromArray';
@@ -57,7 +59,7 @@ import prepareCalendar from '../coord/calendar/prepareCustom';
 import ComponentModel from '../model/Component';
 import List, { DefaultDataVisual } from '../data/List';
 import GlobalModel from '../model/Global';
-import { makeInner } from '../util/model';
+import { makeInner, normalizeToArray } from '../util/model';
 import ExtensionAPI from '../ExtensionAPI';
 import Displayable from 'zrender/src/graphic/Displayable';
 import Axis2D from '../coord/cartesian/Axis2D';
@@ -75,6 +77,7 @@ import {
 } from '../util/styleCompat';
 import Transformable from 'zrender/src/core/Transformable';
 import { ItemStyleProps } from '../model/mixin/itemStyle';
+import { cloneValue } from 'zrender/src/animation/Animator';
 
 
 const inner = makeInner<{
@@ -86,17 +89,38 @@ const inner = makeInner<{
     txConZ2Set: number;
     orginalDuring: Element['updateDuringAnimation'];
     customDuring: CustomZRPathOption['during'];
+    leaveToProps: ElementProps
 }, Element>();
 
 type CustomExtraElementInfo = Dictionary<unknown>;
-type TransformPropsX = 'x' | 'scaleX' | 'originX';
-type TransformPropsY = 'y' | 'scaleY' | 'originY';
-type TransformProps = TransformPropsX | TransformPropsY | 'rotation';
-
+const TRANSFORM_PROPS = {
+    x: 1,
+    y: 1,
+    scaleX: 1,
+    scaleY: 1,
+    originX: 1,
+    originY: 1,
+    rotation: 1
+} as const;
+type TransformProps = keyof typeof TRANSFORM_PROPS;
+
+type TransitionAnyProps = string | string[];
+type TransitionTransformProps = TransformProps | TransformProps[];
+// Do not declare "Dictionary" in TransitionAnyOption to restrict the type check.
+type TransitionAnyOption = {
+    $transition?: TransitionAnyProps;
+    $enterFrom?: Dictionary<unknown>;
+    $leaveTo?: Dictionary<unknown>;
+};
+type TransitionTransformOption = {
+    $transition?: TransitionTransformProps;
+    $enterFrom?: Dictionary<unknown>;
+    $leaveTo?: Dictionary<unknown>;
+};
 
 interface CustomBaseElementOption extends Partial<Pick<
     Element, TransformProps | 'silent' | 'ignore' | 'textConfig'
->> {
+>>, TransitionTransformOption {
     // element type, mandatory.
     type: string;
     id?: string;
@@ -107,13 +131,15 @@ interface CustomBaseElementOption extends Partial<Pick<
     textContent?: CustomTextOption | false;
     // `false` means remove the clipPath
     clipPath?: CustomZRPathOption | false;
+    // Shape can be set in any el option for custom prop for annimation duration.
+    shape?: TransitionAnyOption;
     // updateDuringAnimation
     during?(elProps: CustomDuringElProps): void;
 };
 interface CustomDisplayableOption extends CustomBaseElementOption, Partial<Pick<
     Displayable, 'zlevel' | 'z' | 'z2' | 'invisible'
 >> {
-    style?: ZRStyleProps;
+    style?: ZRStyleProps & TransitionAnyOption;
     // `false` means remove emphasis trigger.
     styleEmphasis?: ZRStyleProps | false;
     emphasis?: CustomDisplayableOptionOnState;
@@ -122,7 +148,7 @@ interface CustomDisplayableOptionOnState extends Partial<Pick<
     Displayable, TransformProps | 'textConfig' | 'z2'
 >> {
     // `false` means remove emphasis trigger.
-    style?: ZRStyleProps | false;
+    style?: (ZRStyleProps & TransitionAnyOption) | false;
 }
 interface CustomGroupOption extends CustomBaseElementOption {
     type: 'group';
@@ -133,7 +159,8 @@ interface CustomGroupOption extends CustomBaseElementOption {
     children: CustomElementOption[];
     $mergeChildren: false | 'byName' | 'byIndex';
 }
-interface CustomZRPathOption extends CustomDisplayableOption, Pick<PathProps, 'shape'> {
+interface CustomZRPathOption extends CustomDisplayableOption {
+    shape?: PathProps['shape'] & TransitionAnyOption;
 }
 interface CustomDuringElProps extends Partial<Pick<Element, TransformProps>> {
     shape?: PathProps['shape'];
@@ -151,22 +178,21 @@ interface CustomSVGPathOption extends CustomDisplayableOption {
         y?: number;
         width?: number;
         height?: number;
-    };
+    } & TransitionAnyOption;
 }
 interface CustomImageOption extends CustomDisplayableOption {
     type: 'image';
-    style?: ImageStyleProps;
+    style?: ImageStyleProps & TransitionAnyOption;
     emphasis?: CustomImageOptionOnState;
 }
 interface CustomImageOptionOnState extends CustomDisplayableOptionOnState {
-    style?: ImageStyleProps;
+    style?: ImageStyleProps & TransitionAnyOption;
 }
 interface CustomTextOption extends CustomDisplayableOption {
     type: 'text';
 }
 type CustomElementOption = CustomZRPathOption | CustomSVGPathOption | CustomImageOption | CustomTextOption;
 type CustomElementOptionOnState = CustomDisplayableOptionOnState | CustomImageOptionOnState;
-type StyleOption = ZRStyleProps | ImageStyleProps | false;
 
 
 interface CustomSeriesRenderItemAPI extends
@@ -237,6 +263,11 @@ interface CustomSeriesOption extends
     };
 }
 
+interface LooseElementProps extends ElementProps {
+    style?: ZRStyleProps;
+    shape?: Dictionary<unknown>;
+}
+
 // Also compat with ec4, where
 // `visual('color') visual('borderColor')` is supported.
 const STYLE_VISUAL_TYPE = {
@@ -291,6 +322,13 @@ const Z2_SPECIFIED_BIT = {
 const tmpDuringStyle = {} as CustomDuringElProps['style'];
 const tmpDuringElProps = {} as CustomDuringElProps;
 
+const LEGACY_TRANSFORM_PROPS = {
+    position: ['x', 'y'],
+    scale: ['scaleX', 'scaleY'],
+    origin: ['originX', 'originY']
+} as const;
+type LegacyTransformProp = keyof typeof LEGACY_TRANSFORM_PROPS;
+
 export type PrepareCustomInfo = (coordSys: CoordinateSystem) => {
     coordSys: CustomSeriesRenderItemParamsCoordSys;
     api: CustomSeriesRenderItemCoordinateSystemAPI
@@ -400,19 +438,18 @@ class CustomSeriesView extends ChartView {
         // roam or data zoom according to `actionType`.
         data.diff(oldData)
             .add(function (newIdx) {
-                createOrUpdate(
+                createOrUpdateItemEl(
                     null, newIdx, renderItem(newIdx, payload), customSeries, group, data
                 );
             })
             .update(function (newIdx, oldIdx) {
-                const el = oldData.getItemGraphicEl(oldIdx);
-                createOrUpdate(
-                    el, newIdx, renderItem(newIdx, payload), customSeries, group, data
+                createOrUpdateItemEl(
+                    oldData.getItemGraphicEl(oldIdx),
+                    newIdx, renderItem(newIdx, payload), customSeries, group, data
                 );
             })
             .remove(function (oldIdx) {
-                const el = oldData.getItemGraphicEl(oldIdx);
-                el && group.remove(el);
+                removeItemEl(oldData.getItemGraphicEl(oldIdx), customSeries, group);
             })
             .execute();
 
@@ -455,7 +492,7 @@ class CustomSeriesView extends ChartView {
             }
         }
         for (let idx = params.start; idx < params.end; idx++) {
-            const el = createOrUpdate(null, idx, renderItem(idx, payload), customSeries, this.group, data);
+            const el = createOrUpdateItemEl(null, idx, renderItem(idx, payload), customSeries, this.group, data);
             el.traverse(setIncrementalAndHoverLayer);
         }
     }
@@ -523,7 +560,7 @@ function createEl(elOption: CustomElementOption): Element {
         const Clz = graphicUtil.getShapeClass(graphicType);
 
         if (__DEV__) {
-            zrUtil.assert(Clz, 'graphic type "' + graphicType + '" can not be found.');
+            assert(Clz, 'graphic type "' + graphicType + '" can not be found.');
         }
 
         el = new Clz();
@@ -541,7 +578,8 @@ function createEl(elOption: CustomElementOption): Element {
 }
 
 /**
- * [STRATEGY] Merge properties or erase all properties:
+ * ----------------------------------------------------------
+ * [STRATEGY_MERGE] Merge properties or erase all properties:
  *
  * Based on the fact that the existing zr element probably be reused, we discuss whether
  * merge or erase all properties to the exsiting elements.
@@ -558,7 +596,8 @@ function createEl(elOption: CustomElementOption): Element {
  * Some "object-like" config like `textConfig`, `textContent`, `style` which are not needed for
  * every elment, so we replace them only when user specify them. And the that is a total replace.
  *
- * [STRATEGY] `hasOwnProperty` or `== null`:
+ * ----------------------------------------------
+ * [STRATEGY_NULL] `hasOwnProperty` or `== null`:
  *
  * Ditinguishing "own property" probably bring little trouble to user when make el options.
  * So we  trade a {xx: null} or {xx: undefined} as "not specified" if possible rather than
@@ -566,33 +605,36 @@ function createEl(elOption: CustomElementOption): Element {
  * clearable props like `style`/`textConfig`/`textContent` we enable `false` to means
  * "clear". In some othere special cases that the prop is able to set as null/undefined,
  * but not suitable to use `false`, `hasOwnProperty` is checked.
+ *
+ * ---------------------------------------------
+ * [STRATEGY_TRANSITION] The rule of transition:
+ * + For props on the root level of a element:
+ *      If there is no `$transition` specified, tansform props will be transitioned by default,
+ *      which is the same as the previous setting in echarts4 and suitable for the scenario
+ *      of dataZoom change.
+ *      If `$transition` specified, only the specified props will be transitioned.
+ * + For props in `shape` and `style`:
+ *      Only props specified in `$transition` will be transitioned.
+ * + Break:
+ *      Since ec5, do not make transition to shape by default, because it might result in
+ *      performance issue (especially `points` of polygon) and do not necessary in most cases.
  */
 function updateElNormal(
     el: Element,
     dataIndex: number,
     elOption: CustomElementOption,
-    styleOpt: StyleOption,
+    styleOpt: CustomElementOption['style'],
     attachedTxInfo: AttachedTxInfo,
     seriesModel: CustomSeriesModel,
     isInit: boolean,
     isTextContent: boolean
 ): void {
-    const transitionProps = {} as ElementProps;
+    const transFromProps = {} as ElementProps;
+    const allProps = {} as ElementProps;
     const elDisplayable = el.isGroup ? null : el as Displayable;
 
-    (elOption as CustomZRPathOption).shape && (
-        (transitionProps as PathProps).shape = zrUtil.clone((elOption as CustomZRPathOption).shape)
-    );
-    setLagecyProp(elOption, transitionProps, 'position', 'x', 'y');
-    setLagecyProp(elOption, transitionProps, 'scale', 'scaleX', 'scaleY');
-    setLagecyProp(elOption, transitionProps, 'origin', 'originX', 'originY');
-    setTransProp(elOption, transitionProps, 'x');
-    setTransProp(elOption, transitionProps, 'y');
-    setTransProp(elOption, transitionProps, 'scaleX');
-    setTransProp(elOption, transitionProps, 'scaleY');
-    setTransProp(elOption, transitionProps, 'originX');
-    setTransProp(elOption, transitionProps, 'originY');
-    setTransProp(elOption, transitionProps, 'rotation');
+    prepareShapeUpdate(el, elOption, allProps, transFromProps, isInit);
+    prepareTransformUpdate(el, elOption, allProps, transFromProps, isInit);
 
     const txCfgOpt = attachedTxInfo && attachedTxInfo.normal.cfg;
     if (txCfgOpt) {
@@ -601,55 +643,59 @@ function updateElNormal(
         el.setTextConfig(txCfgOpt);
     }
 
-    if (el.type === 'image' && styleOpt) {
-        const targetStyle = (transitionProps as Displayable).style = {};
-        const imgStyle = (el as graphicUtil.Image).style;
-        prepareStyleTransition('x', targetStyle, styleOpt, imgStyle, isInit);
-        prepareStyleTransition('y', targetStyle, styleOpt, imgStyle, isInit);
-        prepareStyleTransition('width', targetStyle, styleOpt, imgStyle, isInit);
-        prepareStyleTransition('height', targetStyle, styleOpt, imgStyle, isInit);
-    }
-
     if (el.type === 'text' && styleOpt) {
         const textOptionStyle = styleOpt as TextStyleProps;
-        const targetStyle = (transitionProps as Displayable).style = {};
-        const textStyle = (el as graphicUtil.Text).style;
-        prepareStyleTransition('x', targetStyle, textOptionStyle, textStyle, isInit);
-        prepareStyleTransition('y', targetStyle, textOptionStyle, textStyle, isInit);
         // Compatible with ec4: if `textFill` or `textStroke` exists use them.
-        zrUtil.hasOwn(textOptionStyle, 'textFill') && (
+        hasOwn(textOptionStyle, 'textFill') && (
             textOptionStyle.fill = (textOptionStyle as any).textFill
         );
-        zrUtil.hasOwn(textOptionStyle, 'textStroke') && (
+        hasOwn(textOptionStyle, 'textStroke') && (
             textOptionStyle.stroke = (textOptionStyle as any).textStroke
         );
     }
 
+    prepareStyleUpdate(el, styleOpt, transFromProps, isInit);
+
     if (elDisplayable) {
         // PENDING: here the input style object is used directly.
         // Good for performance but bad for compatibility control.
         styleOpt && elDisplayable.useStyle(styleOpt);
 
-        // Init animation.
-        if (isInit) {
-            elDisplayable.style.opacity = 0;
-            const targetOpacity = (styleOpt && styleOpt.opacity != null) ? styleOpt.opacity : 1;
-            graphicUtil.initProps(elDisplayable, {style: {opacity: targetOpacity}}, seriesModel, dataIndex);
+        // When style object changed, how to trade the existing animation?
+        // It is probably conplicated and not needed to cover all the cases.
+        // But still need consider the case:
+        // (1) When using init animation on `style.opacity`, and before the animation
+        //     ended users triggers an update by mousewhell. At that time the init
+        //     animation should better be continued rather than terminated.
+        //     So after `useStyle` called, we should change the animation target manually
+        //     to continue the effect of the init animation.
+        // (2) PENDING: If the previous animation targeted at a `val1`, and currently we need
+        //     to update the value to `val2` and no animation declared, should be terminate
+        //     the previous animation or just modify the target of the animation?
+        //     Therotically That will happen not only on `style` but also on `shape` and
+        //     `transfrom` props. But we haven't handle this case at present yet.
+        // (3) PENDING: Is it proper to visit `animators` and `targetName`?
+        const animators = elDisplayable.animators;
+        for (let i = 0; i < animators.length; i++) {
+            const animator = animators[i];
+            // targetName is the "topKey".
+            if (animator.targetName === 'style') {
+                animator.changeTarget(elDisplayable.style);
+            }
         }
 
-        zrUtil.hasOwn(elOption, 'invisible') && (elDisplayable.invisible = elOption.invisible);
+        hasOwn(elOption, 'invisible') && (elDisplayable.invisible = elOption.invisible);
     }
 
-    if (isInit) {
-        el.attr(transitionProps);
-    }
-    else {
-        graphicUtil.updateProps(el, transitionProps, seriesModel, dataIndex);
-    }
+    el.attr(allProps);
+    const params = {dataIndex: dataIndex, isFrom: true};
+    isInit
+        ? graphicUtil.initProps(el, transFromProps, seriesModel, params)
+        : graphicUtil.updateProps(el, transFromProps, seriesModel, params);
 
     // Merge by default.
-    zrUtil.hasOwn(elOption, 'silent') && (el.silent = elOption.silent);
-    zrUtil.hasOwn(elOption, 'ignore') && (el.ignore = elOption.ignore);
+    hasOwn(elOption, 'silent') && (el.silent = elOption.silent);
+    hasOwn(elOption, 'ignore') && (el.ignore = elOption.ignore);
 
     const customDuringMounted = el.updateDuringAnimation === elUpdateDuringAnimation;
     if (elOption.during) {
@@ -668,12 +714,223 @@ function updateElNormal(
         // `elOption.info` enables user to mount some info on
         // elements and use them in event handlers.
         // Update them only when user specified, otherwise, remain.
-        zrUtil.hasOwn(elOption, 'info') && (inner(el).info = elOption.info);
+        hasOwn(elOption, 'info') && (inner(el).info = elOption.info);
     }
 
     styleOpt ? el.dirty() : el.markRedraw();
 }
 
+// See [STRATEGY_TRANSITION]
+function prepareShapeUpdate(
+    el: Element,
+    elOption: CustomElementOption,
+    allProps: LooseElementProps,
+    transFromProps: LooseElementProps,
+    isInit: boolean
+): void {
+
+    const shapeOpt = (elOption as CustomElementOption).shape;
+    if (!shapeOpt) {
+        return;
+    }
+
+    const elShape = (el as LooseElementProps).shape;
+    let tranFromShapeProps: LooseElementProps['shape'];
+
+    const enterFrom = shapeOpt.$enterFrom;
+    if (isInit && enterFrom) {
+        !tranFromShapeProps && (tranFromShapeProps = transFromProps.shape = {});
+        const enterFromKeys = keys(enterFrom);
+        for (let i = 0; i < enterFromKeys.length; i++) {
+            // `$enterFrom` props are not necessarily also declared in `shape`/`style`/...,
+            // for example, `opacity` can only declared in `$enterFrom` but not in `style`.
+            const key = enterFromKeys[i];
+            // Do not clone, animator will perform that clone.
+            tranFromShapeProps[key] = enterFrom[key];
+        }
+    }
+
+    if (!isInit && elShape && shapeOpt.$transition) {
+        !tranFromShapeProps && (tranFromShapeProps = transFromProps.shape = {});
+        const transitionKeys = normalizeToArray(shapeOpt.$transition);
+        for (let i = 0; i < transitionKeys.length; i++) {
+            const key = transitionKeys[i];
+            const elVal = elShape[key];
+            if (__DEV__) {
+                checkTansitionRefer(key, (shapeOpt as any)[key], elVal);
+            }
+            // Do not clone, see `checkTansitionRefer`.
+            tranFromShapeProps[key] = elVal;
+        }
+    }
+
+    const allPropsShape = allProps.shape = {} as LooseElementProps['shape'];
+    const shapeOptKeys = keys(shapeOpt);
+    for (let i = 0; i < shapeOptKeys.length; i++) {
+        const key = shapeOptKeys[i];
+        // To avoid share one object with different element, and
+        // to avoid user modify the object inexpectedly, have to clone.
+        allPropsShape[key] = cloneValue((shapeOpt as any)[key]);
+    }
+
+    const leaveTo = shapeOpt.$leaveTo;
+    if (leaveTo) {
+        const leaveToProps = getOrCreateLeaveToPropsFromEl(el);
+        const leaveToShapeProps = leaveToProps.shape || (leaveToProps.shape = {});
+        const leaveToKeys = keys(leaveTo);
+        for (let i = 0; i < leaveToKeys.length; i++) {
+            const key = leaveToKeys[i];
+            leaveToShapeProps[key] = leaveTo[key];
+        }
+    }
+}
+
+// See [STRATEGY_TRANSITION].
+function prepareTransformUpdate(
+    el: Element,
+    elOption: CustomElementOption,
+    allProps: ElementProps,
+    transFromProps: ElementProps,
+    isInit: boolean
+): void {
+    const enterFrom = elOption.$enterFrom;
+    if (isInit && enterFrom) {
+        const enterFromKeys = keys(enterFrom);
+        for (let i = 0; i < enterFromKeys.length; i++) {
+            const key = enterFromKeys[i] as TransformProps;
+            if (__DEV__) {
+                checkTransformPropRefer(key, 'el.$enterFrom');
+            }
+            // Do not clone, animator will perform that clone.
+            transFromProps[key] = enterFrom[key] as number;
+        }
+    }
+
+    if (!isInit) {
+        if (elOption.$transition) {
+            const transitionKeys = normalizeToArray(elOption.$transition);
+            for (let i = 0; i < transitionKeys.length; i++) {
+                const key = transitionKeys[i];
+                const elVal = el[key];
+                if (__DEV__) {
+                    checkTransformPropRefer(key, 'el.$transition');
+                    checkTansitionRefer(key, elOption[key], elVal);
+                }
+                // Do not clone, see `checkTansitionRefer`.
+                transFromProps[key] = elVal;
+            }
+        }
+        // This default transition see [STRATEGY_TRANSITION]
+        else {
+            setLagecyProp(elOption, transFromProps, 'position', el);
+            setTransProp(elOption, transFromProps, 'x', el);
+            setTransProp(elOption, transFromProps, 'y', el);
+        }
+    }
+
+    setLagecyProp(elOption, allProps, 'position');
+    setLagecyProp(elOption, allProps, 'scale');
+    setLagecyProp(elOption, allProps, 'origin');
+    setTransProp(elOption, allProps, 'x');
+    setTransProp(elOption, allProps, 'y');
+    setTransProp(elOption, allProps, 'scaleX');
+    setTransProp(elOption, allProps, 'scaleY');
+    setTransProp(elOption, allProps, 'originX');
+    setTransProp(elOption, allProps, 'originY');
+    setTransProp(elOption, allProps, 'rotation');
+
+    const leaveTo = elOption.$leaveTo;
+    if (leaveTo) {
+        const leaveToProps = getOrCreateLeaveToPropsFromEl(el);
+        const leaveToKeys = keys(leaveTo);
+        for (let i = 0; i < leaveToKeys.length; i++) {
+            const key = leaveToKeys[i] as TransformProps;
+            if (__DEV__) {
+                checkTransformPropRefer(key, 'el.$leaveTo');
+            }
+            leaveToProps[key] = leaveTo[key] as number;
+        }
+    }
+}
+
+// See [STRATEGY_TRANSITION].
+function prepareStyleUpdate(
+    el: Element,
+    styleOpt: CustomElementOption['style'],
+    transFromProps: LooseElementProps,
+    isInit: boolean
+): void {
+    if (!styleOpt) {
+        return;
+    }
+
+    const elStyle = (el as LooseElementProps).style as LooseElementProps['style'];
+    let transFromStyleProps: LooseElementProps['style'];
+
+    const enterFrom = styleOpt.$enterFrom;
+    if (isInit && enterFrom) {
+        const enterFromKeys = keys(enterFrom);
+        !transFromStyleProps && (transFromStyleProps = transFromProps.style = {});
+        for (let i = 0; i < enterFromKeys.length; i++) {
+            const key = enterFromKeys[i];
+            // Do not clone, animator will perform that clone.
+            (transFromStyleProps as any)[key] = enterFrom[key];
+        }
+    }
+
+    if (!isInit && elStyle && styleOpt.$transition) {
+        const transitionKeys = normalizeToArray(styleOpt.$transition);
+        !transFromStyleProps && (transFromStyleProps = transFromProps.style = {});
+        for (let i = 0; i < transitionKeys.length; i++) {
+            const key = transitionKeys[i];
+            const elVal = (elStyle as any)[key];
+            if (__DEV__) {
+                checkTansitionRefer(key, (styleOpt as any)[key], elVal);
+            }
+            // Do not clone, see `checkTansitionRefer`.
+            (transFromStyleProps as any)[key] = elVal;
+        }
+    }
+
+    const leaveTo = styleOpt.$leaveTo;
+    if (leaveTo) {
+        const leaveToKeys = keys(leaveTo);
+        const leaveToProps = getOrCreateLeaveToPropsFromEl(el);
+        const leaveToStyleProps = leaveToProps.style || (leaveToProps.style = {});
+        for (let i = 0; i < leaveToKeys.length; i++) {
+            const key = leaveToKeys[i];
+            (leaveToStyleProps as any)[key] = leaveTo[key];
+        }
+    }
+}
+
+function checkTansitionRefer(propName: string, optVal: unknown, elVal: unknown): void {
+    const isArrLike = isArrayLike(optVal);
+    assert(
+        isArrLike || (optVal != null && isFinite(optVal as number)),
+        'Prop `' + propName + '` must refer to a finite number or ArrayLike for transition.'
+    );
+    // Try not to copy array for performance, but if user use the same object in different
+    // call of `renderItem`, it will casue animation transition fail.
+    assert(
+        !isArrLike || optVal !== elVal,
+        'Prop `' + propName + '` must use different Array object each time for transition.'
+    );
+}
+
+function checkTransformPropRefer(key: string, usedIn: string): void {
+    assert(
+        hasOwn(TRANSFORM_PROPS, key),
+        'Prop `' + key + '` is not a permitted in `' + usedIn + '`. '
+            + 'Only `' + keys(TRANSFORM_PROPS).join('`, `') + '` are permitted.'
+    );
+}
+
+function getOrCreateLeaveToPropsFromEl(el: Element): LooseElementProps {
+    const innerEl = inner(el);
+    return innerEl.leaveToProps || (innerEl.leaveToProps = {});
+}
+
 function elUpdateDuringAnimation(this: Element, key: string): void {
     const innerEl = inner(this);
     // FIXME `this.markRedraw();` directly ?
@@ -731,7 +988,7 @@ function updateElOnState(
     state: DisplayStateNonNormal,
     el: Element,
     elStateOpt: CustomElementOptionOnState,
-    styleOpt: StyleOption,
+    styleOpt: CustomElementOptionOnState['style'],
     attachedTxInfo: AttachedTxInfo,
     isRoot: boolean,
     isTextContent: boolean
@@ -842,46 +1099,32 @@ function updateZForEachState(
 
 function setLagecyProp(
     elOption: CustomElementOption,
-    transitionProps: Partial<Pick<Transformable, TransformProps>>,
-    legacyName: 'position' | 'scale' | 'origin',
-    xName: TransformPropsX,
-    yName: TransformPropsY
+    targetProps: Partial<Pick<Transformable, TransformProps>>,
+    legacyName: LegacyTransformProp,
+    fromEl?: Element // If provided, retrieve from the element.
 ): void {
     const legacyArr = (elOption as any)[legacyName];
-    legacyArr && (transitionProps[xName] = legacyArr[0], transitionProps[yName] = legacyArr[1]);
+    const xyName = LEGACY_TRANSFORM_PROPS[legacyName];
+    if (legacyArr) {
+        if (fromEl) {
+            targetProps[xyName[0]] = fromEl[xyName[0]];
+            targetProps[xyName[1]] = fromEl[xyName[1]];
+        }
+        else {
+            targetProps[xyName[0]] = legacyArr[0];
+            targetProps[xyName[1]] = legacyArr[1];
+        }
+    }
 }
+
 function setTransProp(
     elOption: CustomElementOption,
-    transitionProps: Partial<Pick<Transformable, TransformProps>>,
-    name: TransformProps
+    targetProps: Partial<Pick<Transformable, TransformProps>>,
+    name: TransformProps,
+    fromEl?: Element // If provided, retrieve from the element.
 ): void {
-    elOption[name] != null && (transitionProps[name] = elOption[name]);
-}
-
-function prepareStyleTransition(
-    prop: 'x' | 'y',
-    targetStyle: CustomTextOption['style'],
-    elOptionStyle: CustomTextOption['style'],
-    oldElStyle: graphicUtil.Text['style'],
-    isInit: boolean
-): void;
-function prepareStyleTransition(
-    prop: 'x' | 'y' | 'width' | 'height',
-    targetStyle: CustomImageOption['style'],
-    elOptionStyle: CustomImageOption['style'],
-    oldElStyle: graphicUtil.Image['style'],
-    isInit: boolean
-): void;
-function prepareStyleTransition(
-    prop: string,
-    targetStyle: any,
-    elOptionStyle: any,
-    oldElStyle: any,
-    isInit: boolean
-): void {
-    if (elOptionStyle[prop] != null && !isInit) {
-        targetStyle[prop] = elOptionStyle[prop];
-        elOptionStyle[prop] = oldElStyle[prop];
+    if (elOption[name] != null) {
+        targetProps[name] = fromEl ? fromEl[name] : elOption[name];
     }
 }
 
@@ -897,8 +1140,8 @@ function makeRenderItem(
 
     if (coordSys) {
         if (__DEV__) {
-            zrUtil.assert(renderItem, 'series.render is required.');
-            zrUtil.assert(
+            assert(renderItem, 'series.render is required.');
+            assert(
                 coordSys.prepareCustoms || prepareCustoms[coordSys.type],
                 'This coordSys does not support custom series.'
             );
@@ -910,7 +1153,7 @@ function makeRenderItem(
             : prepareCustoms[coordSys.type](coordSys);
     }
 
-    const userAPI = zrUtil.defaults({
+    const userAPI = defaults({
         getWidth: api.getWidth,
         getHeight: api.getHeight,
         getZr: api.getZr,
@@ -987,7 +1230,7 @@ function makeRenderItem(
         currLabelModels = {};
 
         return renderItem && renderItem(
-            zrUtil.defaults({
+            defaults({
                 dataIndexInside: dataIndexInside,
                 dataIndex: data.getRawIndex(dataIndexInside),
                 // Can be used for optimization when zoom or roam.
@@ -1041,14 +1284,14 @@ function makeRenderItem(
         visualColor != null && (itemStyle.fill = visualColor);
         opacity != null && (itemStyle.opacity = opacity);
 
-        const opt = {autoColor: zrUtil.isString(visualColor) ? visualColor : '#000'};
+        const opt = {autoColor: isString(visualColor) ? visualColor : '#000'};
         const labelModel = getLabelModel(dataIndexInside, NORMAL);
         // Now that the feture of "auto adjust text fill/stroke" has been migrated to zrender
         // since ec5, we should set `isAttached` as `false` here and make compat in
         // `convertToEC4StyleForCustomSerise`.
         const textStyle = graphicUtil.createTextStyle(labelModel, null, opt, false, true);
         textStyle.text = labelModel.getShallow('show')
-            ? zrUtil.retrieve2(
+            ? retrieve2(
                 customSeries.getFormattedLabel(dataIndexInside, NORMAL),
                 getDefaultLabel(data, dataIndexInside)
             )
@@ -1080,7 +1323,7 @@ function makeRenderItem(
         const labelModel = getLabelModel(dataIndexInside, EMPHASIS);
         const textStyle = graphicUtil.createTextStyle(labelModel, null, null, true, true);
         textStyle.text = labelModel.getShallow('show')
-            ? zrUtil.retrieve3(
+            ? retrieve3(
                 customSeries.getFormattedLabel(dataIndexInside, EMPHASIS),
                 customSeries.getFormattedLabel(dataIndexInside, NORMAL),
                 getDefaultLabel(data, dataIndexInside)
@@ -1117,7 +1360,7 @@ function makeRenderItem(
     ): ReturnType<List['getItemVisual']> {
         dataIndexInside == null && (dataIndexInside = currDataIndexInside);
 
-        if (zrUtil.hasOwn(STYLE_VISUAL_TYPE, visualType)) {
+        if (hasOwn(STYLE_VISUAL_TYPE, visualType)) {
             const style = data.getItemVisual(dataIndexInside, 'style');
             return style
                 ? style[STYLE_VISUAL_TYPE[visualType as keyof typeof STYLE_VISUAL_TYPE]] as any
@@ -1125,7 +1368,7 @@ function makeRenderItem(
         }
         // Only support these visuals. Other visual might be inner tricky
         // for performance (like `style`), do not expose to users.
-        if (zrUtil.hasOwn(VISUAL_PROPS, visualType)) {
+        if (hasOwn(VISUAL_PROPS, visualType)) {
             return data.getItemVisual(dataIndexInside, visualType);
         }
     }
@@ -1139,7 +1382,7 @@ function makeRenderItem(
     ): ReturnType<typeof getLayoutOnAxis> {
         if (coordSys.type === 'cartesian2d') {
             const baseAxis = coordSys.getBaseAxis() as Axis2D;
-            return getLayoutOnAxis(zrUtil.defaults({axis: baseAxis}, opt));
+            return getLayoutOnAxis(defaults({axis: baseAxis}, opt));
         }
     }
 
@@ -1163,7 +1406,7 @@ function makeRenderItem(
 
 function wrapEncodeDef(data: List<CustomSeriesModel>): Dictionary<number[]> {
     const encodeDef = {} as Dictionary<number[]>;
-    zrUtil.each(data.dimensions, function (dimName, dataDimIndex) {
+    each(data.dimensions, function (dimName, dataDimIndex) {
         const dimInfo = data.getDimensionInfo(dimName);
         if (!dimInfo.isExtraCoord) {
             const coordDim = dimInfo.coordDim;
@@ -1174,7 +1417,7 @@ function wrapEncodeDef(data: List<CustomSeriesModel>): Dictionary<number[]> {
     return encodeDef;
 }
 
-function createOrUpdate(
+function createOrUpdateItemEl(
     el: Element,
     dataIndex: number,
     elOption: CustomElementOption,
@@ -1198,14 +1441,11 @@ function doCreateOrUpdate(
 ): Element {
 
     // [Rule]
-    // By default, follow merge mode.
-    //     (It probably brings benifit for performance in some cases of large data, where
-    //     user program can be optimized to that only updated props needed to be re-calculated,
-    //     or according to `actionType` some calculation can be skipped.)
     // If `renderItem` returns `null`/`undefined`/`false`, remove the previous el if existing.
     //     (It seems that violate the "merge" principle, but most of users probably intuitively
     //     regard "return;" as "show nothing element whatever", so make a exception to meet the
     //     most cases.)
+    // The rule or "merge" see [STRATEGY_MERGE].
 
     // If `elOption` is `null`/`undefined`/`false` (when `renderItem` returns nothing).
     if (!elOption) {
@@ -1287,12 +1527,12 @@ function doesElNeedRecreate(el: Element, elOption: CustomElementOption): boolean
             && getPathData(elOptionShape) !== elInner.customPathData
         )
         || (elOptionType === 'image'
-            && zrUtil.hasOwn(elOptionStyle, 'image')
+            && hasOwn(elOptionStyle, 'image')
             && (elOptionStyle as CustomImageOption['style']).image !== elInner.customImagePath
         )
         // // FIXME test and remove this restriction?
         // || (elOptionType === 'text'
-        //     && zrUtil.hasOwn(elOptionStyle, 'text')
+        //     && hasOwn(elOptionStyle, 'text')
         //     && (elOptionStyle as TextStyleProps).text !== elInner.customText
         // )
     );
@@ -1322,7 +1562,7 @@ function doCreateOrUpdateClipPath(
         if (!clipPath) {
             clipPath = createEl(clipPathOpt) as graphicUtil.Path;
             if (__DEV__) {
-                zrUtil.assert(
+                assert(
                     clipPath instanceof graphicUtil.Path,
                     'Only any type of `path` can be used in `clipPath`, rather than ' + clipPath.type + '.'
                 );
@@ -1436,7 +1676,7 @@ function processTxInfo(
         !txConOptNormal.type && (txConOptNormal.type = 'text');
         if (__DEV__) {
             // Do not tolerate incorret type for forward compat.
-            txConOptNormal.type !== 'text' && zrUtil.assert(
+            txConOptNormal.type !== 'text' && assert(
                 txConOptNormal.type === 'text',
                 'textContent.type must be "text"'
             );
@@ -1458,7 +1698,7 @@ function retrieveStyleOptionOnState(
     stateOptionNormal: CustomElementOption,
     stateOption: CustomElementOptionOnState,
     state: DisplayStateNonNormal
-): StyleOption {
+): CustomElementOptionOnState['style'] {
     let style = stateOption && stateOption.style;
     if (style == null && state === EMPHASIS && stateOptionNormal) {
         style = stateOptionNormal.styleEmphasis;
@@ -1529,7 +1769,7 @@ function mergeChildren(
         );
     }
     if (__DEV__) {
-        zrUtil.assert(
+        assert(
             !notMerge || el.childCount() === index,
             'MUST NOT contain empty item in children array when `group.$mergeChildren` is `false`.'
         );
@@ -1583,7 +1823,7 @@ function processAddUpdate(
 
 function applyExtraAfter(itemStyle: ZRStyleProps, extra: ZRStyleProps): void {
     for (const key in extra) {
-        if (zrUtil.hasOwn(extra, key)) {
+        if (hasOwn(extra, key)) {
             (itemStyle as any)[key] = (extra as any)[key];
         }
     }
@@ -1595,6 +1835,23 @@ function processRemove(this: DataDiffer<DiffGroupContext>, oldIndex: number): vo
     child && context.group.remove(child);
 }
 
+function removeItemEl(
+    el: Element,
+    seriesModel: CustomSeriesModel,
+    group: ViewRootGroup
+): void {
+    if (el) {
+        const leaveToProps = inner(el).leaveToProps;
+        leaveToProps
+            ? graphicUtil.updateProps(el, leaveToProps, seriesModel, {
+                cb: function () {
+                    group.remove(el);
+                }
+            })
+            : group.remove(el);
+    }
+}
+
 /**
  * @return SVG Path data.
  */
@@ -1604,6 +1861,6 @@ function getPathData(shape: CustomSVGPathOption['shape']): string {
 }
 
 function hasOwnPathData(shape: CustomSVGPathOption['shape']): boolean {
-    return shape && (zrUtil.hasOwn(shape, 'pathData') || zrUtil.hasOwn(shape, 'd'));
+    return shape && (hasOwn(shape, 'pathData') || hasOwn(shape, 'd'));
 }
 
diff --git a/src/component/visualMap/PiecewiseModel.ts b/src/component/visualMap/PiecewiseModel.ts
index 085157f..ab806aa 100644
--- a/src/component/visualMap/PiecewiseModel.ts
+++ b/src/component/visualMap/PiecewiseModel.ts
@@ -185,7 +185,7 @@ class PiecewiseModel extends VisualMapModel<PiecewiseVisualMapOption> {
         const isCategory = this.isCategory();
 
         zrUtil.each(option.pieces, function (piece) {
-            zrUtil.each(visualTypes, function (visualType) {
+            zrUtil.each(visualTypes, function (visualType: BuiltinVisualProperty) {
                 if (piece.hasOwnProperty(visualType)) {
                     visualTypesInPieces[visualType] = 1;
                 }
diff --git a/src/util/graphic.ts b/src/util/graphic.ts
index 13db4b5..21d6aa2 100644
--- a/src/util/graphic.ts
+++ b/src/util/graphic.ts
@@ -71,7 +71,8 @@ import {
     trim,
     isArrayLike,
     map,
-    defaults
+    defaults,
+    isObject
 } from 'zrender/src/core/util';
 
 
@@ -1041,6 +1042,12 @@ export function getFont(
     ].join(' '));
 }
 
+type AnimateOrSetPropsOption = {
+    dataIndex?: number;
+    cb?: () => void;
+    isFrom?: boolean;
+};
+
 function animateOrSetProps<Props>(
     isUpdate: boolean,
     el: Element<Props>,
@@ -1048,13 +1055,19 @@ function animateOrSetProps<Props>(
     animatableModel?: Model<AnimationOptionMixin> & {
         getAnimationDelayParams?: (el: Element<Props>, dataIndex: number) => AnimationDelayCallbackParam
     },
-    dataIndex?: number | (() => void),
-    cb?: () => void
+    dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption,
+    cb?: AnimateOrSetPropsOption['cb']
 ) {
+    let isFrom = false;
     if (typeof dataIndex === 'function') {
         cb = dataIndex;
         dataIndex = null;
     }
+    else if (isObject(dataIndex)) {
+        cb = dataIndex.cb;
+        isFrom = dataIndex.isFrom;
+        dataIndex = dataIndex.dataIndex;
+    }
     // Do not check 'animation' property directly here. Consider this case:
     // animation model is an `itemModel`, whose does not have `isAnimationEnabled`
     // but its parent model (`seriesModel`) does.
@@ -1083,13 +1096,23 @@ function animateOrSetProps<Props>(
         }
 
         duration > 0
-            ? el.animateTo(props, {
-                duration,
-                delay: animationDelay || 0,
-                easing: animationEasing,
-                done: cb,
-                force: !!cb
-            })
+            ? (
+                isFrom
+                    ? el.animateFrom(props, {
+                        duration,
+                        delay: animationDelay || 0,
+                        easing: animationEasing,
+                        done: cb,
+                        force: !!cb
+                    })
+                    : el.animateTo(props, {
+                        duration,
+                        delay: animationDelay || 0,
+                        easing: animationEasing,
+                        done: cb,
+                        force: !!cb
+                    })
+            )
             : (el.stopAnimation(), el.attr(props), cb && cb());
     }
     else {
@@ -1120,8 +1143,8 @@ function updateProps<Props>(
     props: Props,
     // TODO: TYPE AnimatableModel
     animatableModel?: Model<AnimationOptionMixin>,
-    dataIndex?: number | (() => void),
-    cb?: () => void
+    dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption,
+    cb?: AnimateOrSetPropsOption['cb']
 ) {
     animateOrSetProps(true, el, props, animatableModel, dataIndex, cb);
 }
@@ -1140,8 +1163,8 @@ export function initProps<Props>(
     el: Element<Props>,
     props: Props,
     animatableModel?: Model<AnimationOptionMixin>,
-    dataIndex?: number | (() => void),
-    cb?: () => void
+    dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption,
+    cb?: AnimateOrSetPropsOption['cb']
 ) {
     animateOrSetProps(false, el, props, animatableModel, dataIndex, cb);
 }
diff --git a/test/custom-transition-texture.js b/test/custom-transition-texture.js
new file mode 100644
index 0000000..61f9d8f
--- /dev/null
+++ b/test/custom-transition-texture.js
@@ -0,0 +1 @@
+window.BAR_ROUND_GRADIENT_TEXTURE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAgAElEQVR4Xu1debwUxbX+qudeLiLigktEDChClICCoOKSEBQiEteYEBV34xKNJmp88cUY4tNs5kUTNSbuiRpN0OeC4h4lxjXigqK4oKIibriggCx3ut6vZqYvNXVPVZ3q6Zk7997pf+BOd1dXnTrf+b5TXV0l0DiqZIELWoClXwHibQDZAuR7AfLLgPgCINcBZD9A5gAJQK5ZqkQM5CP1A4BWIG4CEAN4D4gjQL4HYC4QvwpgBRAtAOIPgOh5YM23gLNaq9SYblus6LYtz6zhP+8FrHEggH5ArJx+AoCBmvOXnqR8Xvl6ctj+n7dcY/tdlaPKVoApAOhpQM4C4nlA/t/An9/OrKndsKAGQII6fWoT0GcsEI0EVu0NYAcAzYAUJ [...]
\ No newline at end of file
diff --git a/test/custom-transition.html b/test/custom-transition.html
index 2d8c718..e1a22ff 100644
--- a/test/custom-transition.html
+++ b/test/custom-transition.html
@@ -36,108 +36,16 @@ under the License.
         </style>
 
 
-        <div id="init-animation-additive"></div>
+        <!-- <div id="texture-bar-texture-maker"></div> -->
         <div id="spiral-fixed-extent"></div>
         <div id="spiral-dynamic-extent"></div>
         <div id="texture-bar-by-clipPath"></div>
-        <!-- <div id="texture-bar-texture-maker"></div> -->
-
-
-
-
-        <script>
-
-            require(['echarts'], function (echarts) {
-
-                var animationDuration = 5000;
-                var animationDurationUpdate = 4000;
-                var option = {
-                    xAxis: {},
-                    yAxis: {},
-                    dataZoom: [
-                        { type: 'slider' },
-                        { type: 'inside' }
-                    ],
-                    animationDuration: animationDuration,
-                    animationDurationUpdate: animationDurationUpdate,
-                    series: [{
-                        type: 'custom',
-                        renderItem: function (params, api) {
-                            return {
-                                type: 'group',
-                                position: api.coord([api.value(0), api.value(1)]),
-                                children: [{
-                                    type: 'rect',
-                                    shape: {
-                                        x: -50,
-                                        y: 50,
-                                        width: 100,
-                                        height: 150,
-                                        r: 10
-                                    },
-                                    style: {
-                                        fill: 'rgba(102,241,98,0.9)'
-                                    }
-                                }, {
-                                    type: 'circle',
-                                    shape: {
-                                        cx: -50,
-                                        cy: 50,
-                                        r: 30
-                                    },
-                                    style: {
-                                        fill: 'blue'
-                                    },
-                                    textContent: {
-                                        text: 'data',
-                                        style: {
-                                            fill: '#fff'
-                                        }
-                                    }
-                                }]
-                            };
-                        },
-                        data: [[121, 333], [29, 312]]
-                    }]
-                };
-
-                var chart = testHelper.create(echarts, 'init-animation-additive', {
-                    title: [
-                        'Style merge:',
-                        '(1) dataZoom hide a data item, and then show it, ensure the fade in animation normal.',
-                        '(2) click button to setOption merge.'
-                    ],
-                    option: option,
-                    info: {
-                        animationDuration: animationDuration,
-                        animationDurationUpdate: animationDurationUpdate
-                    },
-                    buttons: [{
-                        text: 'merge style: border become blue, but background not changed',
-                        onclick: function () {
-                            chart.setOption({
-                                type: 'custom',
-                                renderItem: function (params, api) {
-                                    return {
-                                        type: 'group',
-                                        children: [{
-                                            type: 'rect',
-                                            style: {
-                                                stroke: 'red',
-                                                lineWidth: 5
-                                            }
-                                        }]
-                                    };
-                                }
-                            });
-                        }
-                    }]
-                });
-            });
-
-        </script>
-
-
+        <div id="enter-animation"></div>
+        <div id="enter-animation2"></div>
+        <div id="style-animation"></div>
+        <div id="transform-animation"></div>
+        <div id="transform-animation-disabled"></div>
+        <div id="leave-animation"></div>
 
 
 
@@ -213,7 +121,8 @@ under the License.
                         type: 'polygon',
                         shape: {
                             points: makeShapePoints(api, valOnRadius, valOnAngle),
-                            valOnAngle: valOnAngle
+                            valOnAngle: valOnAngle,
+                            $transition: 'valOnAngle'
                         },
                         style: {
                             lineWidth: 1,
@@ -252,7 +161,8 @@ under the License.
                         x: point[0],
                         y: point[1],
                         shape: {
-                            valOnAngle: valOnAngle
+                            valOnAngle: valOnAngle,
+                            $transition: 'valOnAngle'
                         },
                         style: {
                             text: getText(valOnAngle),
@@ -440,7 +350,8 @@ under the License.
                             points: makeShapePoints(params, widthRadius, startRadius, endRadian),
                             widthRadius: widthRadius,
                             startRadius: startRadius,
-                            endRadian: endRadian
+                            endRadian: endRadian,
+                            $transition: ['widthRadius', 'startRadius', 'endRadian']
                         },
                         style: {
                             lineWidth: 1,
@@ -492,7 +403,8 @@ under the License.
                         shape: {
                             startRadius: startRadius,
                             endRadian: endRadian,
-                            widthRadius: widthRadius
+                            widthRadius: widthRadius,
+                            $transition: ['startRadius', 'endRadian', 'widthRadius']
                         },
                         style: {
                             text: makeText(endRadian),
@@ -626,10 +538,6 @@ under the License.
 
 
 
-
-
-
-
         <script>
             require(['echarts'], function (echarts) {
                 var _animationDuration = 1000;
@@ -679,8 +587,10 @@ under the License.
                                     startAngle: 0,
                                     // polor: anticlockwise-positive radian
                                     // sector: clockwise-positive radian
-                                    endAngle: -polarEndRadian
-                                },
+                                    endAngle: -polarEndRadian,
+                                    $transition: 'endAngle',
+                                    $enterFrom: { endAngle: 0 }
+                                }
                             }
                         }, {
                             type: 'image',
@@ -689,7 +599,9 @@ under the License.
                                 type: 'polygon',
                                 shape: {
                                     points: makePionterPoints(params, polarEndRadian),
-                                    polarEndRadian: polarEndRadian
+                                    polarEndRadian: polarEndRadian,
+                                    $transition: 'polarEndRadian',
+                                    $enterFrom: { polarEndRadian: 0 }
                                 },
                                 during: function (elProps) {
                                     elProps.shape.points = makePionterPoints(params, elProps.shape.polarEndRadian);
@@ -712,7 +624,9 @@ under the License.
                         }, {
                             type: 'text',
                             shape: {
-                                valOnRadian: valOnRadian
+                                valOnRadian: valOnRadian,
+                                $transition: 'valOnRadian',
+                                $enterFrom: { valOnRadian: 0 }
                             },
                             style: {
                                 text: makeText(valOnRadian),
@@ -722,6 +636,7 @@ under the License.
                                 fill: 'rgb(0,50,190)',
                                 align: 'center',
                                 verticalAlign: 'middle',
+                                $enterFrom: { opacity: 0 }
                             },
                             during: function (elProps) {
                                 elProps.style.text = makeText(elProps.shape.valOnRadian);
@@ -746,10 +661,15 @@ under the License.
                 }
 
                 function makeText(valOnRadian) {
+                    // Validate additive animation calc.
+                    if (valOnRadian < -10) {
+                        alert('illegal during val: ' + valOnRadian);
+                    }
                     return (valOnRadian / _valOnRadianMax * 100).toFixed(0) + '%'
                 }
 
                 var option = {
+                    animationEasing: _animationEasingUpdate,
                     animationDuration: _animationDuration,
                     animationDurationUpdate: _animationDurationUpdate,
                     animationEasingUpdate: _animationEasingUpdate,
@@ -862,5 +782,476 @@ under the License.
 
 
 
+        <script>
+            require(['echarts'], function (echarts) {
+
+                var animationDuration = 5000;
+                var animationDurationUpdate = 4000;
+                var option = {
+                    animationDuration: animationDuration,
+                    animationDurationUpdate: animationDurationUpdate,
+                    xAxis: {
+                    },
+                    yAxis: {
+                    },
+                    dataZoom: [
+                        { type: 'slider' },
+                        { type: 'inside' }
+                    ],
+                    series: [{
+                        type: 'custom',
+                        renderItem: function (params, api) {
+                            var pos = api.coord([api.value(0), api.value(1)]);
+                            return {
+                                type: 'group',
+                                x: pos[0],
+                                y: pos[1],
+                                children: [{
+                                    type: 'rect',
+                                    shape: {
+                                        x: -50,
+                                        y: 50,
+                                        width: 100,
+                                        height: 50,
+                                        r: 10
+                                    },
+                                    style: {
+                                        fill: 'blue',
+                                        $enterFrom: { opacity: 0 }
+                                    }
+                                }, {
+                                    type: 'circle',
+                                    shape: {
+                                        cx: -50,
+                                        cy: 50,
+                                        r: 30
+                                    },
+                                    style: {
+                                        fill: 'green',
+                                        $enterFrom: { opacity: 0 }
+                                    },
+                                    textConfig: {
+                                        position: 'bottom'
+                                    },
+                                    textContent: {
+                                        style: {
+                                            text: 'xxxx',
+                                            fill: 'black',
+                                            $enterFrom: { opacity: 0 }
+                                        }
+                                    }
+                                }]
+                            };
+                        },
+                        data: [[121, 333], [29, 312]]
+                    }]
+                };
+
+                var chart = testHelper.create(echarts, 'enter-animation', {
+                    title: [
+                        '(1) Move dataZoom, position should have transition animation.',
+                        '(2) Use dataZoom hide a data item, and then show it, ensure the **fade in** animation not be interupted.',
+                        '(3) click button to setOption merge.'
+                    ],
+                    height: 300,
+                    option: option,
+                    buttons: [{
+                        text: 'replace style: border become red, and background become black',
+                        onclick: function () {
+                            chart.setOption({
+                                series: {
+                                    type: 'custom',
+                                    renderItem: function (params, api) {
+                                        return {
+                                            type: 'group',
+                                            children: [{
+                                                type: 'rect',
+                                                style: {
+                                                    stroke: 'red',
+                                                    lineWidth: 5
+                                                }
+                                            }]
+                                        };
+                                    },
+                                    data: [[121, 333], [29, 312]]
+                                }
+                            });
+                        }
+                    }]
+                });
+            });
+        </script>
+
+
+
+
+
+
+
+
+
+
+
+        <script>
+            require(['echarts'], function (echarts) {
+                var weatherIcons = {
+                    'Sunny': './data/weather/sunny_128.png',
+                    'Cloudy': './data/weather/cloudy_128.png',
+                    'Showers': './data/weather/showers_128.png'
+                };
+
+                var animationDuration = 5000;
+                var animationDurationUpdate = 4000;
+                var option = {
+                    animationDuration: animationDuration,
+                    animationDurationUpdate: animationDurationUpdate,
+                    xAxis: {
+                        max: 500
+                    },
+                    yAxis: {
+                    },
+                    dataZoom: [
+                        { type: 'slider', end: 60 },
+                        { type: 'inside', end: 60 }
+                    ],
+                    series: [{
+                        type: 'custom',
+                        renderItem: function (params, api) {
+                            var pos = api.coord([api.value(0), api.value(1)]);
+                            return {
+                                type: 'group',
+                                x: pos[0],
+                                y: pos[1],
+                                $enterFrom: {
+                                    y: 0
+                                },
+                                children: [{
+                                    type: 'image',
+                                    style: {
+                                        image: weatherIcons.Cloudy,
+                                        height: 50
+                                    }
+                                }]
+                            };
+                        },
+                        data: [[121, 133], [129, 312]]
+                    }]
+                };
+
+                var chart = testHelper.create(echarts, 'enter-animation2', {
+                    title: [
+                        '(1) Move dataZoom, position should have transition animation.',
+                        '(2) Use dataZoom hide a data item, and then show it, ensure the **drop** animation not be interupted.',
+                    ],
+                    height: 300,
+                    option: option
+                });
+            });
+        </script>
+
+
+
+
+
+
+
+
+
+
+        <script>
+            require(['echarts'], function (echarts) {
+                var weatherIcons = {
+                    'Sunny': './data/weather/sunny_128.png',
+                    'Cloudy': './data/weather/cloudy_128.png',
+                    'Showers': './data/weather/showers_128.png'
+                };
+
+                var animationDuration = 2000;
+                var animationDurationUpdate = 1000;
+                var option = {
+                    animationDuration: animationDuration,
+                    animationDurationUpdate: animationDurationUpdate,
+                    xAxis: {
+                        max: 10000
+                    },
+                    yAxis: {
+                    },
+                    dataZoom: [
+                        { type: 'slider', start: 20, end: 50 },
+                        { type: 'inside', start: 20, end: 50 }
+                    ],
+                    series: [{
+                        type: 'custom',
+                        renderItem: function (params, api) {
+                            var pos = api.coord([api.value(0), api.value(1)]);
+                             var width = Math.abs(pos[0] % 200 - 100) + 20;
+                            return {
+                                type: 'image',
+                                x: pos[0],
+                                y: pos[1],
+                                style: {
+                                    image: weatherIcons.Showers,
+                                    width: width,
+                                    $transition: 'width'
+                                }
+                            };
+                        },
+                        data: [[3321, 333], [4129, 312]]
+                    }]
+                };
+
+                var chart = testHelper.create(echarts, 'style-animation', {
+                    title: [
+                        'Move dataZoom, make sure the "raining-cloud" size animation smooth.',
+                    ],
+                    height: 300,
+                    option: option
+                });
+            });
+        </script>
+
+
+
+
+
+
+
+
+        <script>
+            require(['echarts'], function (echarts) {
+
+                var animationDuration = 4000;
+                var animationDurationUpdate = 1000;
+                var option = {
+                    animationDuration: animationDuration,
+                    animationDurationUpdate: animationDurationUpdate,
+                    xAxis: {
+                        min: -200,
+                        max: 400
+                    },
+                    yAxis: {
+                    },
+                    dataZoom: [
+                        { type: 'slider', start: 20, end: 70 },
+                        { type: 'inside', start: 20, end: 70 }
+                    ],
+                    series: [{
+                        type: 'custom',
+                        renderItem: function (params, api) {
+                            var pos = api.coord([api.value(0), api.value(1)]);
+                            return {
+                                type: 'group',
+                                x: pos[0],
+                                y: pos[1],
+                                rotation: pos[0] / 500 * Math.PI,
+                                $transition: ['rotation'],
+                                originX: -50,
+                                originY: 50,
+                                children: [{
+                                    type: 'rect',
+                                    shape: {
+                                        x: -50,
+                                        y: 50,
+                                        width: 100,
+                                        height: 50,
+                                        r: 10
+                                    },
+                                    style: {
+                                        fill: 'green',
+                                        $enterFrom: { opacity: 0 }
+                                    }
+                                }, {
+                                    type: 'circle',
+                                    shape: {
+                                        cx: -50,
+                                        cy: 50,
+                                        r: 30
+                                    },
+                                    style: {
+                                        fill: 'blue',
+                                        $enterFrom: { opacity: 0 }
+                                    },
+                                    textConfig: {
+                                        position: 'bottom'
+                                    },
+                                    textContent: {
+                                        style: {
+                                            text: 'xxxx',
+                                            fill: 'black',
+                                            $enterFrom: { opacity: 0 }
+                                        }
+                                    }
+                                }]
+                            };
+                        },
+                        data: [[121, 333], [29, 333]]
+                    }]
+                };
+
+                var chart = testHelper.create(echarts, 'transform-animation', {
+                    height: 300,
+                    title: [
+                        'Move dataZoom:',
+                        'position should **no** transition animation.',
+                        'rotatino should **has** transition animation.',
+                    ],
+                    option: option
+                });
+            });
+        </script>
+
+
+
+
+
+
+        <script>
+            require(['echarts'], function (echarts) {
+
+                var animationDuration = 4000;
+                var animationDurationUpdate = 1000;
+                var option = {
+                    animationDuration: animationDuration,
+                    animationDurationUpdate: animationDurationUpdate,
+                    xAxis: {
+                        min: -200,
+                        max: 400
+                    },
+                    yAxis: {
+                    },
+                    dataZoom: [
+                        { type: 'slider', start: 20, end: 70 },
+                        { type: 'inside', start: 20, end: 70 }
+                    ],
+                    series: [{
+                        type: 'custom',
+                        renderItem: function (params, api) {
+                            var pos = api.coord([api.value(0), api.value(1)]);
+                            return {
+                                type: 'rect',
+                                position: pos,
+                                $transition: [],
+                                shape: {
+                                    x: -50,
+                                    y: 50,
+                                    width: 100,
+                                    height: 50,
+                                    r: 10
+                                },
+                                style: {
+                                    fill: 'green',
+                                    $enterFrom: { opacity: 0 }
+                                }
+                            };
+                        },
+                        data: [[121, 333], [29, 333]]
+                    }]
+                };
+
+                var chart = testHelper.create(echarts, 'transform-animation-disabled', {
+                    height: 230,
+                    title: [
+                        'Move dataZoom:',
+                        'transform animation should has been **disabled**.',
+                    ],
+                    option: option
+                });
+            });
+        </script>
+
+
+
+
+
+
+
+
+        <script>
+            require(['echarts'], function (echarts) {
+
+                var animationDuration = 4000;
+                var animationDurationUpdate = 1000;
+                var option = {
+                    animationDuration: animationDuration,
+                    animationDurationUpdate: animationDurationUpdate,
+                    xAxis: {
+                        min: -200,
+                        max: 400
+                    },
+                    yAxis: {
+                    },
+                    dataZoom: [
+                        { type: 'slider', start: 20, end: 70 },
+                        { type: 'inside', start: 20, end: 70 }
+                    ],
+                    series: [{
+                        type: 'custom',
+                        renderItem: function (params, api) {
+                            var pos = api.coord([api.value(0), api.value(1)]);
+                            return {
+                                type: 'group',
+                                x: pos[0],
+                                y: pos[1],
+                                rotation: pos[0] / 500 * Math.PI,
+                                $transition: ['x', 'y', 'rotation'],
+                                originX: -50,
+                                originY: 50,
+                                $leaveTo: {scaleX: 0, scaleY: 0},
+                                children: [{
+                                    type: 'rect',
+                                    shape: {
+                                        x: -50,
+                                        y: 50,
+                                        width: 100,
+                                        height: 50,
+                                        r: 10
+                                    },
+                                    style: {
+                                        fill: 'green',
+                                        $enterFrom: { opacity: 0 }
+                                    }
+                                }, {
+                                    type: 'circle',
+                                    shape: {
+                                        cx: -50,
+                                        cy: 50,
+                                        r: 30
+                                    },
+                                    style: {
+                                        fill: 'blue',
+                                        $enterFrom: { opacity: 0 }
+                                    },
+                                    textConfig: {
+                                        position: 'bottom'
+                                    },
+                                    textContent: {
+                                        style: {
+                                            text: 'xxxx',
+                                            fill: 'black',
+                                            $enterFrom: { opacity: 0 }
+                                        }
+                                    }
+                                }]
+                            };
+                        },
+                        data: [[121, 333], [29, 333]]
+                    }]
+                };
+
+                var chart = testHelper.create(echarts, 'leave-animation', {
+                    height: 300,
+                    title: [
+                        'Move dataZoom to "out" a item:',
+                        '**Leave animation** on scale should be performed on rect and circle, but not on text.',
+                        'The item should **finally be removed**.',
+                    ],
+                    option: 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