Try to support animation from and custom-series animation.
diff --git a/src/chart/custom.js b/src/chart/custom.js
index 98f7ead..ce7ae3a 100644
--- a/src/chart/custom.js
+++ b/src/chart/custom.js
@@ -42,6 +42,10 @@
 // which will cause weird udpate animation.
 var GROUP_DIFF_PREFIX = 'e\0\0';
 
+var IMAGE_TRANSITION_STYLES = ['x', 'y', 'width', 'height', 'opacity'];
+var TEXT_TRANSITION_STYLES = ['x', 'y', 'opacity'];
+var PATH_TRANSITION_STYLES = ['opacity', 'lineWidth'];
+
 /**
  * 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
@@ -252,54 +256,78 @@
 }
 
 function updateEl(el, dataIndex, elOption, animatableModel, data, isInit, isRoot) {
-    var transitionProps = {};
-    var elOptionStyle = elOption.style || {};
+    var elType = el.type;
+    var isGroup = el.isGroup;
+    var isImage = elType === 'image';
+    var isText = elType === 'text';
+    var notCopy = elOption.copy === false;
 
-    elOption.shape && (transitionProps.shape = zrUtil.clone(elOption.shape));
-    elOption.position && (transitionProps.position = elOption.position.slice());
-    elOption.scale && (transitionProps.scale = elOption.scale.slice());
-    elOption.origin && (transitionProps.origin = elOption.origin.slice());
-    elOption.rotation && (transitionProps.rotation = elOption.rotation);
+    var userInitState = elOption.initState;
+    var styleToBeSet = elOption.style;
 
-    if (el.type === 'image' && elOption.style) {
-        var targetStyle = transitionProps.style = {};
-        zrUtil.each(['x', 'y', 'width', 'height'], function (prop) {
-            prepareStyleTransition(prop, targetStyle, elOptionStyle, el.style, isInit);
-        });
+    // Make `elOptionValid` and `initStateValid`.
+    var elOptionValid = fetchDisplayableProps(elOption, notCopy);
+    var initStateValid = isInit && userInitState && fetchDisplayableProps(userInitState, notCopy);
+
+    // Prepare `styleTransValues`.
+    var styleTransValues;
+    if (isInit && !userInitState && !isGroup) {
+        // Default init animation.
+        styleTransValues = {opacity: 0};
     }
-
-    if (el.type === 'text' && elOption.style) {
-        var targetStyle = transitionProps.style = {};
-        zrUtil.each(['x', 'y'], function (prop) {
-            prepareStyleTransition(prop, targetStyle, elOptionStyle, el.style, isInit);
-        });
-        // Compatible with previous: both support
-        // textFill and fill, textStroke and stroke in 'text' element.
-        !elOptionStyle.hasOwnProperty('textFill') && elOptionStyle.fill && (
-            elOptionStyle.textFill = elOptionStyle.fill
-        );
-        !elOptionStyle.hasOwnProperty('textStroke') && elOptionStyle.stroke && (
-            elOptionStyle.textStroke = elOptionStyle.stroke
-        );
-    }
-
-    if (el.type !== 'group') {
-        el.useStyle(elOptionStyle);
-
-        // Init animation.
-        if (isInit) {
-            el.style.opacity = 0;
-            var targetOpacity = elOptionStyle.opacity;
-            targetOpacity == null && (targetOpacity = 1);
-            graphicUtil.initProps(el, {style: {opacity: targetOpacity}}, animatableModel, dataIndex);
+    else {
+        var styleTransCandidates = isInit ? (userInitState && userInitState.style) : styleToBeSet;
+        // Enable style transition.
+        var styleTransProps = isImage
+            ? IMAGE_TRANSITION_STYLES
+            : isText
+            ? TEXT_TRANSITION_STYLES
+            : !isGroup
+            ? PATH_TRANSITION_STYLES
+            : null;
+        if (styleTransProps && styleTransCandidates) {
+            for (var i = 0; i < styleTransProps.length; i++) {
+                var prop = styleTransProps[i];
+                if (styleTransCandidates[prop] != null) {
+                    // Condider animation performance, do not give until necessary.
+                    (styleTransValues = styleTransValues || {})[prop] = styleTransCandidates[prop];
+                    !isInit && (styleToBeSet[prop] = el.style[prop]);
+                }
+            }
         }
     }
 
+    // Compatible with previous: both support
+    // textFill and fill, textStroke and stroke in 'text' element.
+    if (isText) {
+        !styleToBeSet.hasOwnProperty('textFill') && styleToBeSet.fill && (
+            styleToBeSet.textFill = styleToBeSet.fill
+        );
+        !styleToBeSet.hasOwnProperty('textStroke') && styleToBeSet.stroke && (
+            styleToBeSet.textStroke = styleToBeSet.stroke
+        );
+    }
+
+    if (!isGroup) {
+        // Follow the convention and consider performance, merge by default.
+        // If `el.merge` is false, here the `el` is a new element just created.
+        (!isInit && elOption.mergeStyle === false)
+            ? el.useStyle(styleToBeSet || {})
+            : (styleToBeSet && el.setStyle(styleToBeSet));
+    }
+
     if (isInit) {
-        el.attr(transitionProps);
+        elOptionValid && el.attr(elOptionValid);
+        styleTransValues && ((initStateValid = initStateValid || {}).style = styleTransValues);
+        // When "init", the current props represents the final state, while the `initState` is a subset of
+        // props. When "update", the current props represents the current state, while the final state is
+        // a subset of props. So we do not use `graphicUtil.initProps`. See the comment of
+        // `graphicUtil.initFromProps` for more details.
+        initStateValid && graphicUtil.initFromProps(el, initStateValid, animatableModel, dataIndex);
     }
     else {
-        graphicUtil.updateProps(el, transitionProps, animatableModel, dataIndex);
+        styleTransValues && ((elOptionValid = elOptionValid || {}).style = styleTransValues);
+        elOptionValid && graphicUtil.updateProps(el, elOptionValid, animatableModel, dataIndex);
     }
 
     // Merge by default.
@@ -313,6 +341,7 @@
     // Update them only when user specified, otherwise, remain.
     elOption.hasOwnProperty('info') && el.attr('info', elOption.info);
 
+    // Set `styleEmphasis`.
     // If `elOption.styleEmphasis` is `false`, remove hover style. The
     // logic is ensured by `graphicUtil.setElementHoverStyle`.
     var styleEmphasis = elOption.styleEmphasis;
@@ -330,11 +359,19 @@
     isRoot && graphicUtil.setAsHoverStyleTrigger(el, !disableStyleEmphasis);
 }
 
-function prepareStyleTransition(prop, targetStyle, elOptionStyle, oldElStyle, isInit) {
-    if (elOptionStyle[prop] != null && !isInit) {
-        targetStyle[prop] = elOptionStyle[prop];
-        elOptionStyle[prop] = oldElStyle[prop];
-    }
+function fetchDisplayableProps(option, notCopy) {
+    // Condider performance of `graphic.initFromProps/updateProps`, do not give until necessary.
+    var props;
+    var v;
+    // Consider performance of large shape, enable user to disable copy.
+    (v = option.shape) && ((props = props || {}).shape = notCopy ? v : zrUtil.clone(v));
+    // Transform attributes will be copied inside `el.attr()` and `graphic.updateProps()`.
+    (v = option.position) && ((props = props || {}).position = v);
+    (v = option.scale) && ((props = props || {}).scale = v);
+    (v = option.origin) && ((props = props || {}).origin = v);
+    (v = option.rotation) != null && ((props = props || {}).rotation = v);
+
+    return props;
 }
 
 function makeRenderItem(customSeries, data, ecModel, api) {
@@ -620,25 +657,25 @@
 }
 
 // Usage:
-// (1) By default, `elOption.$mergeChildren` is `'byIndex'`, which indicates that
+// (1) By default, `elOption.mergeChildren` is `'byIndex'`, which indicates that
 //     the existing children will not be removed, and enables the feature that
 //     update some of the props of some of the children simply by construct
 //     the returned children of `renderItem` like:
 //     `var children = group.children = []; children[3] = {opacity: 0.5};`
-// (2) If `elOption.$mergeChildren` is `'byName'`, add/update/remove children
+// (2) If `elOption.mergeChildren` is `'byName'`, add/update/remove children
 //     by child.name. But that might be lower performance.
-// (3) If `elOption.$mergeChildren` is `false`, the existing children will be
+// (3) If `elOption.mergeChildren` is `false`, the existing children will be
 //     replaced totally.
 // (4) If `!elOption.children`, following the "merge" principle, nothing will happen.
 //
 // For implementation simpleness, do not provide a direct way to remove sinlge
 // child (otherwise the total indicies of the children array have to be modified).
 // User can remove a single child by set its `ignore` as `true` or replace
-// it by another element, where its `$merge` can be set as `true` if necessary.
+// it by another element, where its `merge` can be set as `true` if necessary.
 function mergeChildren(el, dataIndex, elOption, animatableModel, data) {
     var newChildren = elOption.children;
     var newLen = newChildren ? newChildren.length : 0;
-    var mergeChildren = elOption.$mergeChildren;
+    var mergeChildren = elOption.mergeChildren;
     // `diffChildrenByName` has been deprecated.
     var byName = mergeChildren === 'byName' || elOption.diffChildrenByName;
     var notMerge = mergeChildren === false;
@@ -678,7 +715,7 @@
     if (__DEV__) {
         zrUtil.assert(
             !notMerge || el.childCount() === index,
-            'MUST NOT contain empty item in children array when `group.$mergeChildren` is `false`.'
+            'MUST NOT contain empty item in children array when `group.mergeChildren` is `false`.'
         );
     }
 }
diff --git a/src/chart/line/LineView.js b/src/chart/line/LineView.js
index 501f026..97a59ed 100644
--- a/src/chart/line/LineView.js
+++ b/src/chart/line/LineView.js
@@ -733,7 +733,7 @@
             shape: {
                 points: next
             }
-        }, seriesModel);
+        }, seriesModel, null, {during: duringAnimation});
 
         if (polygon) {
             polygon.setShape({
@@ -758,19 +758,17 @@
                 if (el) {
                     updatedDataInfo.push({
                         el: el,
-                        ptIdx: i    // Index of points
+                        ptIdx: i // Index of points
                     });
                 }
             }
         }
 
-        if (polyline.animators && polyline.animators.length) {
-            polyline.animators[0].during(function () {
-                for (var i = 0; i < updatedDataInfo.length; i++) {
-                    var el = updatedDataInfo[i].el;
-                    el.attr('position', polyline.shape.__points[updatedDataInfo[i].ptIdx]);
-                }
-            });
+        function duringAnimation() {
+            for (var i = 0; i < updatedDataInfo.length; i++) {
+                var el = updatedDataInfo[i].el;
+                el.attr('position', polyline.shape.__points[updatedDataInfo[i].ptIdx]);
+            }
         }
     },
 
diff --git a/src/component/axis/AxisBuilder.js b/src/component/axis/AxisBuilder.js
index 1f0ff7e..5a5254d 100644
--- a/src/component/axis/AxisBuilder.js
+++ b/src/component/axis/AxisBuilder.js
@@ -612,6 +612,40 @@
             z2: 2,
             silent: true
         }));
+
+// if (window.__basesss == null) {
+//     window.__basesss = 1;
+// }
+// (function () {
+//     var uuid = window.__basesss++;
+//     Object.defineProperty(tickEl.shape, 'uuid', {
+//         get() {
+//             return uuid;
+//         },
+//         set() {
+//             debugger;
+//         }
+//     });
+// })();
+
+// Object.defineProperty(tickEl.shape, 'aaaa', {
+//     get() {
+//         return 1;
+//     },
+//     enumerable: true,
+//     set(val) {
+//         console.log(val, '???????????????????????');
+//         debugger;
+//     }
+// });
+// if (!window.__xxx) {
+//     console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
+//     window.__xxx = tickEl;
+//     // tickEl.shape.aaaa = 1;
+//     console.log(tickEl.shape.aaaa);
+//     console.log(tickEl.shape.uuid, '=++++++++++++++');
+// }
+
         axisBuilder.group.add(tickEl);
         tickEls.push(tickEl);
     }
diff --git a/src/export.js b/src/export.js
index dded75c..0542ab0 100644
--- a/src/export.js
+++ b/src/export.js
@@ -69,7 +69,7 @@
         'extendShape', 'extendPath', 'makePath', 'makeImage',
         'mergePath', 'resizePath', 'createIcon',
         'setHoverStyle', 'setLabelStyle', 'setTextStyle', 'setText',
-        'getFont', 'updateProps', 'initProps', 'getTransform',
+        'getFont', 'updateProps', 'initProps', 'initFromProps', 'getTransform',
         'clipPointsByRect', 'clipRectByRect',
         'Group',
         'Image',
diff --git a/src/processor/dataStack.js b/src/processor/dataStack.js
index 22b9202..9ca423e 100644
--- a/src/processor/dataStack.js
+++ b/src/processor/dataStack.js
@@ -19,11 +19,11 @@
 
 import {createHashMap, each} from 'zrender/src/core/util';
 
-// (1) [Caution]: the logic is correct based on the premises:
+// (1) [Caution]: the logic is correct based on the promises:
 //     data processing stage is blocked in stream.
 //     See <module:echarts/stream/Scheduler#performDataProcessorTasks>
 // (2) Only register once when import repeatly.
-//     Should be executed before after series filtered and before stack calculation.
+//     Should be executed after series filtered and before stack calculation.
 export default function (ecModel) {
     var stackInfoMap = createHashMap();
     ecModel.eachSeries(function (seriesModel) {
diff --git a/src/util/graphic.js b/src/util/graphic.js
index f44b844..71befb7 100644
--- a/src/util/graphic.js
+++ b/src/util/graphic.js
@@ -886,7 +886,7 @@
     ].join(' '));
 }
 
-function animateOrSetProps(isUpdate, el, props, animatableModel, dataIndex, cb) {
+function animateOrSetProps(isUpdate, isFrom, el, props, animatableModel, dataIndex, cb) {
     if (typeof dataIndex === 'function') {
         cb = dataIndex;
         dataIndex = null;
@@ -914,11 +914,14 @@
         }
 
         duration > 0
-            ? el.animateTo(props, duration, animationDelay || 0, animationEasing, cb, !!cb)
+            ? el[isFrom ? 'animateFrom' : 'animateTo'](
+                props, duration, animationDelay || 0, animationEasing, cb, !!cb
+            )
+            // Consider that the new `props` maybe different from the last `props`,
+            // forward to last to ensure the last settings work.
             : (el.stopAnimation(), el.attr(props), cb && cb());
     }
-    else {
-        el.stopAnimation();
+    else if (!isFrom) {
         el.attr(props);
         cb && cb();
     }
@@ -936,7 +939,7 @@
  * @param {Object} props
  * @param {module:echarts/model/Model} [animatableModel]
  * @param {number} [dataIndex]
- * @param {Function} [cb]
+ * @param {Function|Object} [cb] Function: done, Object: {done: function, during: function}
  * @example
  *     graphic.updateProps(el, {
  *         position: [100, 100]
@@ -947,7 +950,7 @@
  *     }, seriesModel, function () { console.log('Animation done!'); });
  */
 export function updateProps(el, props, animatableModel, dataIndex, cb) {
-    animateOrSetProps(true, el, props, animatableModel, dataIndex, cb);
+    animateOrSetProps(true, false, el, props, animatableModel, dataIndex, cb);
 }
 
 /**
@@ -962,10 +965,34 @@
  * @param {Object} props
  * @param {module:echarts/model/Model} [animatableModel]
  * @param {number} [dataIndex]
- * @param {Function} cb
+ * @param {Function|Object} [cb] Function: done, Object: {done: function, during: function}
  */
 export function initProps(el, props, animatableModel, dataIndex, cb) {
-    animateOrSetProps(false, el, props, animatableModel, dataIndex, cb);
+    animateOrSetProps(false, false, el, props, animatableModel, dataIndex, cb);
+}
+
+/**
+ * Animate from the given props to el current state with "init" animation settings.
+ * The rest definitions are the same as `initPorps`.
+ *
+ * Consider that only props that existing in `initState` can be animated, and sometimes
+ * the target state is the complete set of props but the initial state is just a subset,
+ * using `initFromProps` is simpler. For example:
+ *
+ * Use `initProps` to make init animation:
+ * ```js
+ * var {targetStateNotInInitState, targetStateInInitState} = split(targetState, initState);
+ * el.attr(targetStateNotInInitState);
+ * graphicUtil.initProps(elInInitState, targetStateInInitState);
+ * ```
+ *
+ * Use `initFromProps` to make init animation.
+ * ```js
+ * graphicUtil.initFromProps(elInTargetState, initState, ...)`;
+ * ```
+ */
+export function initFromProps(el, props, animatableModel, dataIndex, cb) {
+    animateOrSetProps(false, true, el, props, animatableModel, dataIndex, cb);
 }
 
 /**
@@ -1066,6 +1093,7 @@
         if (!el.isGroup && el.anid) {
             var oldEl = elMap1[el.anid];
             if (oldEl) {
+                // ??? animateFrom
                 var newProp = getAnimatableProps(el);
                 el.attr(getAnimatableProps(oldEl));
                 updateProps(el, newProp, animatableModel, el.dataIndex);
diff --git a/test/custom-feature.html b/test/custom-feature.html
index 7c568b1..c27d75b 100644
--- a/test/custom-feature.html
+++ b/test/custom-feature.html
@@ -42,7 +42,7 @@
         <div id="main0"></div>
         <div id="main2"></div>
         <div id="main3"></div>
-        <!-- <div id="main1"></div> -->
+        <div id="main4"></div>
 
 
         <script>
@@ -453,5 +453,104 @@
 
 
 
+
+
+
+
+        <script>
+
+            require(['echarts'/*, 'map/js/china' */], function (echarts) {
+
+                var animationDuration = 5000;
+                var animationDurationUpdate = 4000;
+                var option = {
+                    xAxis: {},
+                    yAxis: {},
+                    dataZoom: [
+                        {
+                        },
+                        // {
+                        //     type: 'inside'
+                        // }
+                    ],
+                    animationDuration: animationDuration,
+                    animationDurationUpdate: animationDurationUpdate,
+                    // animation: false,
+                    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: {
+                                        text: 'data',
+                                        textFill: '#fff',
+                                        fill: 'blue'
+                                    }
+                                }]
+                            };
+                        },
+                        data: [[121, 333], [29, 312]]
+                    }]
+                };
+
+                var chart = testHelper.create(echarts, 'main4', {
+                    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>
+
+
+
+
     </body>
 </html>
\ No newline at end of file
diff --git a/test/tmp-base.html b/test/tmp-base.html
index 3f3693c..dbb80e6 100644
--- a/test/tmp-base.html
+++ b/test/tmp-base.html
@@ -48,9 +48,7 @@
             var myChart;
             var option;
 
-            require([
-                'echarts'/*, 'map/js/china' */
-            ], function (echarts) {
+            require(['echarts'/*, 'map/js/china' */], function (echarts) {
 
                 // $.getJSON('./data/nutrients.json', function (data) {
                 // });