You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by sh...@apache.org on 2021/06/10 14:12:12 UTC

[echarts] branch enhance-morph updated: feat: add universal transition feature

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

shenyi pushed a commit to branch enhance-morph
in repository https://gitbox.apache.org/repos/asf/echarts.git


The following commit(s) were added to refs/heads/enhance-morph by this push:
     new 3112145  feat: add universal transition feature
3112145 is described below

commit 311214537e7cafb4b97bfe19483c1ea43602a13a
Author: pissang <bm...@gmail.com>
AuthorDate: Thu Jun 10 22:09:54 2021 +0800

    feat: add universal transition feature
---
 .gitignore                                         |   2 +
 build/pre-publish.js                               |   4 +-
 .../template/features.d.ts                         |   4 +-
 .../template/features.js                           |   4 +-
 src/animation/morphTransitionHelper.ts             |   6 +-
 src/animation/universalTransition.ts               |  85 ++++++++++-
 src/chart/custom/CustomView.ts                     |   3 +-
 src/core/echarts.ts                                | 165 ++++++++++++---------
 src/echarts.all.ts                                 |  14 +-
 .../universalTransition.ts => export/features.ts}  |   4 +-
 src/model/Global.ts                                |   2 +-
 src/util/types.ts                                  |  13 +-
 test/universalTransition.html                      | 163 ++++++++++++++++++++
 13 files changed, 382 insertions(+), 87 deletions(-)

diff --git a/.gitignore b/.gitignore
index 5787067..ba75419 100644
--- a/.gitignore
+++ b/.gitignore
@@ -203,4 +203,6 @@ todo
 /components.d.ts
 /renderers.js
 /renderers.d.ts
+/features.js
+/features.d.ts
 *.tgz
diff --git a/build/pre-publish.js b/build/pre-publish.js
index 0c5e350..cca4b54 100644
--- a/build/pre-publish.js
+++ b/build/pre-publish.js
@@ -345,7 +345,7 @@ async function bundleDTS() {
 
     // Bundle chunks.
     const parts = [
-        'core', 'charts', 'components', 'renderers', 'option'
+        'core', 'charts', 'components', 'renderers', 'option', 'features'
     ];
     const inputs = {};
     parts.forEach(partName => {
@@ -387,7 +387,7 @@ function readTSConfig() {
 
 
 function generateEntries() {
-    ['charts', 'components', 'renderers', 'core'].forEach(entryName => {
+    ['charts', 'components', 'renderers', 'core', 'features'].forEach(entryName => {
         if (entryName !== 'option') {
             const jsCode = fs.readFileSync(nodePath.join(__dirname, `template/${entryName}.js`), 'utf-8');
             fs.writeFileSync(nodePath.join(__dirname, `../${entryName}.js`), jsCode, 'utf-8');
diff --git a/src/animation/universalTransition.ts b/build/template/features.d.ts
similarity index 88%
copy from src/animation/universalTransition.ts
copy to build/template/features.d.ts
index af2cfa0..0e9a6fc 100644
--- a/src/animation/universalTransition.ts
+++ b/build/template/features.d.ts
@@ -17,6 +17,4 @@
 * under the License.
 */
 
-// Universal transitions that can animate between any shapes and any properties in any amounts.
-
-
+export * from './types/dist/features';
\ No newline at end of file
diff --git a/src/animation/universalTransition.ts b/build/template/features.js
similarity index 88%
copy from src/animation/universalTransition.ts
copy to build/template/features.js
index af2cfa0..862d600 100644
--- a/src/animation/universalTransition.ts
+++ b/build/template/features.js
@@ -17,6 +17,4 @@
 * under the License.
 */
 
-// Universal transitions that can animate between any shapes and any properties in any amounts.
-
-
+export * from './lib/export/features';
\ No newline at end of file
diff --git a/src/animation/morphTransitionHelper.ts b/src/animation/morphTransitionHelper.ts
index 50e057b..a0993b7 100644
--- a/src/animation/morphTransitionHelper.ts
+++ b/src/animation/morphTransitionHelper.ts
@@ -93,7 +93,7 @@ export function applyMorphAnimation(
     to: DescendentPaths | DescendentPaths[],
     seriesModel: SeriesModel,
     dataIndex: number,
-    updateOtherProps: (fromIndividual: Path, toIndividual: Path, rawFrom: Path, rawTo: Path) => void
+    animateOtherProps: (fromIndividual: Path, toIndividual: Path, rawFrom: Path, rawTo: Path) => void
 ) {
     if (!from.length || !to.length) {
         return;
@@ -136,7 +136,7 @@ export function applyMorphAnimation(
             }
             else {
                 morphPath(batchFrom, batchTo, animationCfg);
-                updateOtherProps(batchFrom, batchTo, batchFrom, batchTo);
+                animateOtherProps(batchFrom, batchTo, batchFrom, batchTo);
             }
         }
         else {
@@ -148,7 +148,7 @@ export function applyMorphAnimation(
                 : separateMorph(batchOne, batchMany, animationCfgWithSplitPath);
 
             for (let k = 0; k < fromIndividuals.length; k++) {
-                updateOtherProps(
+                animateOtherProps(
                     fromIndividuals[k],
                     toIndividuals[k],
                     fromIsMany ? batchMany[k] : batch.one,
diff --git a/src/animation/universalTransition.ts b/src/animation/universalTransition.ts
index af2cfa0..6d96f4e 100644
--- a/src/animation/universalTransition.ts
+++ b/src/animation/universalTransition.ts
@@ -17,6 +17,89 @@
 * under the License.
 */
 
-// Universal transitions that can animate between any shapes and any properties in any amounts.
+import ExtensionAPI from '../core/ExtensionAPI';
+import SeriesModel from '../model/Series';
+import {createHashMap, each, filter} from 'zrender/src/core/util';
+import Element from 'zrender/src/Element';
+import { applyMorphAnimation, getPathList } from './morphTransitionHelper';
+import { getECData } from '../util/innerStore';
+import Path from 'zrender/src/graphic/Path';
+import { EChartsExtensionInstallRegisters } from '../extension';
+// Universal transitions that can animate between any shapes(series) and any properties in any amounts.
 
 
+export function transitionBetweenSeries(
+    oldSeriesModel: SeriesModel,
+    newSeriesModel: SeriesModel,
+    api: ExtensionAPI
+) {
+    const oldData = oldSeriesModel.getData();
+    const newData = newSeriesModel.getData();
+
+    // No data or data are in the same series.
+    if (!oldData || !newData || oldData === newData) {
+        return;
+    }
+
+    let sourceElements = oldData.mapArray(idx => {
+        return oldData.getItemGraphicEl(idx);
+    });
+
+    let targetElements = newData.mapArray(idx => {
+        return newData.getItemGraphicEl(idx);
+    });
+
+    // Align the two arrays.
+    function alignNullElements(arr1: unknown[], arr2: unknown[]) {
+        for (let i = 0; i < arr1.length; i++) {
+            if (!arr2[i]) {
+                arr1[i] = null;
+            }
+        }
+    }
+    alignNullElements(sourceElements, targetElements);
+    alignNullElements(targetElements, sourceElements);
+
+    sourceElements = filter(sourceElements, el => !!el);
+    targetElements = filter(targetElements, el => !!el);
+
+    function updateMorphingPathProps(
+        from: Path, to: Path,
+        rawFrom: Path, rawTo: Path
+    ) {
+        // TODO
+    }
+
+    for (let i = 0; i < sourceElements.length; i++) {
+        const dataIndex = getECData(targetElements[i]).dataIndex;
+        sourceElements[i].traverse(el => el.stopAnimation());
+        targetElements[i].traverse(el => el.stopAnimation());
+        applyMorphAnimation(
+            getPathList(sourceElements[i]),
+            getPathList(targetElements[i]),
+            newSeriesModel,
+            dataIndex,
+            updateMorphingPathProps
+        );
+    }
+}
+
+export function installUniversalTransition(registers: EChartsExtensionInstallRegisters) {
+    registers.registerPostUpdate((ecModel, api, params) => {
+        if (params.oldSeries && params.newSeries) {
+            const oldSeriesMap = createHashMap<SeriesModel>();
+            each(params.oldSeries, series => {
+                oldSeriesMap.set(series.id, series);
+            });
+            each(params.newSeries, series => {
+                if (series.get(['universalAnimation', 'enabled'])) {
+                    // Only transition between series with same id.
+                    const oldSeries = oldSeriesMap.get(series.id);
+                    if (oldSeries) {
+                        transitionBetweenSeries(oldSeries, series, api);
+                    }
+                }
+            });
+        }
+    });
+}
\ No newline at end of file
diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts
index 335e8b0..31372f4 100644
--- a/src/chart/custom/CustomView.ts
+++ b/src/chart/custom/CustomView.ts
@@ -212,7 +212,8 @@ export default class CustomChartView extends ChartView {
 
         function updateMorphingPathProps(
             dataIndex: number,
-            from: graphicUtil.Path, to: graphicUtil.Path, rawFrom: graphicUtil.Path, rawTo: graphicUtil.Path
+            from: graphicUtil.Path, to: graphicUtil.Path,
+            rawFrom: graphicUtil.Path, rawTo: graphicUtil.Path
         ) {
             // element has been updated if rawTo is same with to.
             const elOption = customInnerStore(rawTo).option;
diff --git a/src/core/echarts.ts b/src/core/echarts.ts
index beb395d..92eed07 100644
--- a/src/core/echarts.ts
+++ b/src/core/echarts.ts
@@ -17,7 +17,26 @@
 * under the License.
 */
 import * as zrender from 'zrender/src/zrender';
-import * as zrUtil from 'zrender/src/core/util';
+import {
+    assert,
+    each,
+    isFunction,
+    isObject,
+    indexOf,
+    bind,
+    clone,
+    setAsPrimitive,
+    createCanvas,
+    extend,
+    HashMap,
+    createHashMap,
+    map,
+    defaults,
+    isDom,
+    isArray,
+    $override,
+    noop
+} from 'zrender/src/core/util';
 import * as colorTool from 'zrender/src/tool/color';
 import env from 'zrender/src/core/env';
 import timsort from 'zrender/src/core/timsort';
@@ -90,7 +109,8 @@ import {
     ScaleDataValue,
     ZRElementEventName,
     ECElementEvent,
-    AnimationOption
+    AnimationOption,
+    PostUpdateParams
 } from '../util/types';
 import Displayable from 'zrender/src/graphic/Displayable';
 import IncrementalDisplayable from 'zrender/src/graphic/IncrementalDisplayable';
@@ -114,12 +134,6 @@ declare let global: any;
 
 type ModelFinder = modelUtil.ModelFinder;
 
-const assert = zrUtil.assert;
-const each = zrUtil.each;
-const isFunction = zrUtil.isFunction;
-const isObject = zrUtil.isObject;
-const indexOf = zrUtil.indexOf;
-
 const hasWindow = typeof window !== 'undefined';
 
 export const version = '5.1.1';
@@ -182,7 +196,7 @@ export const PRIORITY = {
 // This flag is used to carry out this rule.
 // All events will be triggered out side main process (i.e. when !this[IN_MAIN_PROCESS]).
 const IN_MAIN_PROCESS_KEY = '__flagInMainProcess' as const;
-const OPTION_UPDATED_KEY = '__optionUpdated' as const;
+const PENDING_UPDATE = '__pendingUpdate' as const;
 const STATUS_NEEDS_UPDATE_KEY = '__needsUpdateStatus' as const;
 const ACTION_REG = /^[a-zA-Z0-9_]+$/;
 
@@ -264,10 +278,10 @@ let prepareView: (ecIns: ECharts, isComponent: boolean) => void;
 let updateDirectly: (
     ecIns: ECharts, method: string, payload: Payload, mainType: ComponentMainType, subType?: ComponentSubType
 ) => void;
-type UpdateMethod = (this: ECharts, payload?: Payload) => void;
+type UpdateMethod = (this: ECharts, payload?: Payload, postUpdateParams?: PostUpdateParams) => void;
 let updateMethods: {
     prepareAndUpdate: UpdateMethod,
-    update: UpdateMethod,
+    update: (this: ECharts, payload: Payload, postUpdateParams: PostUpdateParams) => void,
     updateTransform: UpdateMethod,
     updateView: UpdateMethod,
     updateVisual: UpdateMethod,
@@ -297,7 +311,7 @@ let renderSeries: (
     payload: Payload | 'remain',
     dirtyMap?: {[uid: string]: any}
 ) => void;
-let performPostUpdateFuncs: (ecModel: GlobalModel, api: ExtensionAPI) => void;
+let performPostUpdateFuncs: (ecModel: GlobalModel, api: ExtensionAPI, params: PostUpdateParams) => void;
 let createExtensionAPI: (ecIns: ECharts) => ExtensionAPI;
 let enableConnect: (ecIns: ECharts) => void;
 let setTransitionOpt: (
@@ -372,7 +386,10 @@ class ECharts extends Eventful<ECEventDefinition> {
     private _labelManager: LabelManager;
 
 
-    private [OPTION_UPDATED_KEY]: boolean | {silent: boolean};
+    private [PENDING_UPDATE]: {
+        silent: boolean
+        postUpdateParams: PostUpdateParams
+    };
     private [IN_MAIN_PROCESS_KEY]: boolean;
     private [CONNECT_STATUS_KEY]: ConnectStatus;
     private [STATUS_NEEDS_UPDATE_KEY]: boolean;
@@ -426,9 +443,9 @@ class ECharts extends Eventful<ECEventDefinition> {
         });
 
         // Expect 60 fps.
-        this._throttledZrFlush = throttle(zrUtil.bind(zr.flush, zr), 17);
+        this._throttledZrFlush = throttle(bind(zr.flush, zr), 17);
 
-        theme = zrUtil.clone(theme);
+        theme = clone(theme);
         theme && backwardCompat(theme as ECUnitOption, true);
 
         this._theme = theme;
@@ -456,7 +473,7 @@ class ECharts extends Eventful<ECEventDefinition> {
         this._initEvents();
 
         // In case some people write `window.onresize = chart.resize`
-        this.resize = zrUtil.bind(this.resize, this);
+        this.resize = bind(this.resize, this);
 
         zr.animation.on('frame', this._onframe, this);
 
@@ -465,7 +482,7 @@ class ECharts extends Eventful<ECEventDefinition> {
         bindMouseEvent(zr, this);
 
         // ECharts instance can be used as value.
-        zrUtil.setAsPrimitive(this);
+        setAsPrimitive(this);
     }
 
     private _onframe(): void {
@@ -478,13 +495,13 @@ class ECharts extends Eventful<ECEventDefinition> {
         const scheduler = this._scheduler;
 
         // Lazy update
-        if (this[OPTION_UPDATED_KEY]) {
-            const silent = (this[OPTION_UPDATED_KEY] as any).silent;
+        if (this[PENDING_UPDATE]) {
+            const silent = (this[PENDING_UPDATE] as any).silent;
 
             this[IN_MAIN_PROCESS_KEY] = true;
 
             prepare(this);
-            updateMethods.update.call(this);
+            updateMethods.update.call(this, null, this[PENDING_UPDATE].postUpdateParams);
 
             // At present, in each frame, zrender performs:
             //   (1) animation step forward.
@@ -496,7 +513,7 @@ class ECharts extends Eventful<ECEventDefinition> {
 
             this[IN_MAIN_PROCESS_KEY] = false;
 
-            this[OPTION_UPDATED_KEY] = false;
+            this[PENDING_UPDATE] = null;
 
             flushPendingActions.call(this, silent);
 
@@ -596,6 +613,8 @@ class ECharts extends Eventful<ECEventDefinition> {
 
         this[IN_MAIN_PROCESS_KEY] = true;
 
+        // Try getting from old model if notMerge is enabled.
+        let oldSeriesModels = this._model && this._model.getSeries();
         if (!this._model || notMerge) {
             const optionManager = new OptionManager(this._api);
             const theme = this._theme;
@@ -603,13 +622,25 @@ class ECharts extends Eventful<ECEventDefinition> {
             ecModel.scheduler = this._scheduler;
             ecModel.init(null, null, null, theme, this._locale, optionManager);
         }
+        else {
+            oldSeriesModels = this._model.getSeries();
+        }
 
         this._model.setOption(option as ECBasicOption, { replaceMerge }, optionPreprocessorFuncs);
+        const newSeriesModels = this._model.getSeries();
+
+        const postUpdateParams = {
+            oldSeries: oldSeriesModels,
+            newSeries: newSeriesModels
+        };
 
         setTransitionOpt(this, transitionOpt);
 
         if (lazyUpdate) {
-            this[OPTION_UPDATED_KEY] = {silent: silent};
+            this[PENDING_UPDATE] = {
+                silent: silent,
+                postUpdateParams
+            };
             this[IN_MAIN_PROCESS_KEY] = false;
 
             // `setOption(option, {lazyMode: true})` may be called when zrender has been slept.
@@ -619,13 +650,13 @@ class ECharts extends Eventful<ECEventDefinition> {
         else {
             prepare(this);
 
-            updateMethods.update.call(this);
+            updateMethods.update.call(this, null, postUpdateParams);
 
             // Ensure zr refresh sychronously, and then pixel in canvas can be
             // fetched after `setOption`.
             this._zr.flush();
 
-            this[OPTION_UPDATED_KEY] = false;
+            this[PENDING_UPDATE] = null;
             this[IN_MAIN_PROCESS_KEY] = false;
 
             flushPendingActions.call(this, silent);
@@ -691,7 +722,7 @@ class ECharts extends Eventful<ECEventDefinition> {
         const zr = this._zr;
         const list = zr.storage.getDisplayList();
         // Stop animations
-        zrUtil.each(list, function (el: Element) {
+        each(list, function (el: Element) {
             el.stopAnimation(null, true);
         });
 
@@ -771,11 +802,11 @@ class ECharts extends Eventful<ECEventDefinition> {
             const canvasList: {dom: HTMLCanvasElement | string, left: number, top: number}[] = [];
             const dpr = (opts && opts.pixelRatio) || this.getDevicePixelRatio();
 
-            zrUtil.each(instances, function (chart, id) {
+            each(instances, function (chart, id) {
                 if (chart.group === groupId) {
                     const canvas = isSvg
                         ? (chart.getZr().painter as SVGPainter).getSvgDom().innerHTML
-                        : chart.getRenderedCanvas(zrUtil.clone(opts));
+                        : chart.getRenderedCanvas(clone(opts));
                     const boundingRect = chart.getDom().getBoundingClientRect();
                     left = mathMin(boundingRect.left, left);
                     top = mathMin(boundingRect.top, top);
@@ -795,7 +826,7 @@ class ECharts extends Eventful<ECEventDefinition> {
             bottom *= dpr;
             const width = right - left;
             const height = bottom - top;
-            const targetCanvas = zrUtil.createCanvas();
+            const targetCanvas = createCanvas();
             const zr = zrender.init(targetCanvas, {
                 renderer: isSvg ? 'svg' : 'canvas'
             });
@@ -893,8 +924,8 @@ class ECharts extends Eventful<ECEventDefinition> {
 
         const findResult = modelUtil.parseFinder(ecModel, finder);
 
-        zrUtil.each(findResult, function (models, key) {
-            key.indexOf('Models') >= 0 && zrUtil.each(models as ComponentModel[], function (model) {
+        each(findResult, function (models, key) {
+            key.indexOf('Models') >= 0 && each(models as ComponentModel[], function (model) {
                 const coordSys = (model as CoordinateSystemHostModel).coordinateSystem;
                 if (coordSys && coordSys.containPoint) {
                     result = result || !!coordSys.containPoint(value);
@@ -1005,7 +1036,7 @@ class ECharts extends Eventful<ECEventDefinition> {
                         }
                         // If element has custom eventData of components
                         else if (ecData.eventData) {
-                            params = zrUtil.extend({}, ecData.eventData) as ECElementEvent;
+                            params = extend({}, ecData.eventData) as ECElementEvent;
                             return true;
                         }
                     }, true);
@@ -1158,12 +1189,12 @@ class ECharts extends Eventful<ECEventDefinition> {
         // There is some real cases that:
         // chart.setOption(option, { lazyUpdate: true });
         // chart.resize();
-        if (this[OPTION_UPDATED_KEY]) {
+        if (this[PENDING_UPDATE]) {
             if (silent == null) {
-                silent = (this[OPTION_UPDATED_KEY] as any).silent;
+                silent = (this[PENDING_UPDATE] as any).silent;
             }
             needPrepare = true;
-            this[OPTION_UPDATED_KEY] = false;
+            this[PENDING_UPDATE] = null;
         }
 
         this[IN_MAIN_PROCESS_KEY] = true;
@@ -1172,11 +1203,11 @@ class ECharts extends Eventful<ECEventDefinition> {
 
         updateMethods.update.call(this, {
             type: 'resize',
-            animation: zrUtil.extend({
+            animation: extend({
                 // Disable animation
                 duration: 0
             }, opts && opts.animation)
-        });
+        }, {});
 
         this[IN_MAIN_PROCESS_KEY] = false;
 
@@ -1232,7 +1263,7 @@ class ECharts extends Eventful<ECEventDefinition> {
     }
 
     makeActionFromEvent(eventObj: ECActionEvent): Payload {
-        const payload = zrUtil.extend({}, eventObj) as Payload;
+        const payload = extend({}, eventObj) as Payload;
         payload.type = eventActionMap[eventObj.type];
         return payload;
     }
@@ -1471,9 +1502,9 @@ class ECharts extends Eventful<ECEventDefinition> {
             subType && (condition.subType = subType); // subType may be '' by parseClassType;
 
             const excludeSeriesId = payload.excludeSeriesId;
-            let excludeSeriesIdMap: zrUtil.HashMap<true, string>;
+            let excludeSeriesIdMap: HashMap<true, string>;
             if (excludeSeriesId != null) {
-                excludeSeriesIdMap = zrUtil.createHashMap();
+                excludeSeriesIdMap = createHashMap();
                 each(modelUtil.normalizeToArray(excludeSeriesId), id => {
                     const modelId = modelUtil.convertOptionIdName(id, null);
                     if (modelId != null) {
@@ -1541,12 +1572,10 @@ class ECharts extends Eventful<ECEventDefinition> {
 
             prepareAndUpdate(this: ECharts, payload: Payload): void {
                 prepare(this);
-                updateMethods.update.call(this, payload);
+                updateMethods.update.call(this, payload, {});
             },
 
-            update(this: ECharts, payload: Payload): void {
-                // console.profile && console.profile('update');
-
+            update(this: ECharts, payload: Payload, postUpdateParams: PostUpdateParams): void {
                 const ecModel = this._model;
                 const api = this._api;
                 const zr = this._zr;
@@ -1611,9 +1640,7 @@ class ECharts extends Eventful<ECEventDefinition> {
                     }
                 }
 
-                performPostUpdateFuncs(ecModel, api);
-
-                // console.profile && console.profileEnd('update');
+                performPostUpdateFuncs(ecModel, api, postUpdateParams);
             },
 
             updateTransform(this: ECharts, payload: Payload): void {
@@ -1647,7 +1674,7 @@ class ECharts extends Eventful<ECEventDefinition> {
                     }
                 });
 
-                const seriesDirtyMap = zrUtil.createHashMap();
+                const seriesDirtyMap = createHashMap();
                 ecModel.eachSeries((seriesModel) => {
                     const chartView = this._chartsMap[seriesModel.__viewId];
                     if (chartView.updateTransform) {
@@ -1670,7 +1697,7 @@ class ECharts extends Eventful<ECEventDefinition> {
                 // renderComponents(ecIns, ecModel, api, payload, componentDirtyList);
                 renderSeries(this, ecModel, api, payload, seriesDirtyMap);
 
-                performPostUpdateFuncs(ecModel, this._api);
+                performPostUpdateFuncs(ecModel, this._api, {});
             },
 
             updateView(this: ECharts, payload: Payload): void {
@@ -1692,7 +1719,7 @@ class ECharts extends Eventful<ECEventDefinition> {
 
                 render(this, this._model, this._api, payload);
 
-                performPostUpdateFuncs(ecModel, this._api);
+                performPostUpdateFuncs(ecModel, this._api, {});
             },
 
             updateVisual(this: ECharts, payload: Payload): void {
@@ -1733,11 +1760,11 @@ class ECharts extends Eventful<ECEventDefinition> {
                     chartView.updateVisual(seriesModel, ecModel, this._api, payload);
                 });
 
-                performPostUpdateFuncs(ecModel, this._api);
+                performPostUpdateFuncs(ecModel, this._api, {});
             },
 
             updateLayout(this: ECharts, payload: Payload): void {
-                updateMethods.update.call(this, payload);
+                updateMethods.update.call(this, payload, {});
             }
         };
 
@@ -1799,8 +1826,8 @@ class ECharts extends Eventful<ECEventDefinition> {
             // Batch action
             if (payload.batch) {
                 batched = true;
-                payloads = zrUtil.map<Payload['batch'][0], Payload, unknown>(payload.batch, function (item) {
-                    item = zrUtil.defaults(zrUtil.extend({}, item), payload);
+                payloads = map<Payload['batch'][0], Payload, unknown>(payload.batch, function (item) {
+                    item = defaults(extend({}, item), payload);
                     item.batch = null;
                     return item as Payload;
                 });
@@ -1816,7 +1843,7 @@ class ECharts extends Eventful<ECEventDefinition> {
                 // Action can specify the event by return it.
                 eventObj = actionWrap.action(batchItem, this._model, this._api) as ECActionEvent;
                 // Emit event outside
-                eventObj = eventObj || zrUtil.extend({} as ECActionEvent, batchItem);
+                eventObj = eventObj || extend({} as ECActionEvent, batchItem);
                 // Convert type to eventType
                 eventObj.type = actionInfo.event || eventObj.type;
                 eventObjBatch.push(eventObj);
@@ -1841,10 +1868,10 @@ class ECharts extends Eventful<ECEventDefinition> {
 
             if (updateMethod !== 'none' && !isHighDown && !isSelectChange && !cptType) {
                 // Still dirty
-                if (this[OPTION_UPDATED_KEY]) {
+                if (this[PENDING_UPDATE]) {
                     prepare(this);
-                    updateMethods.update.call(this, payload);
-                    this[OPTION_UPDATED_KEY] = false;
+                    updateMethods.update.call(this, payload, {});
+                    this[PENDING_UPDATE] = null;
                 }
                 else {
                     updateMethods[updateMethod as keyof typeof updateMethods].call(this, payload);
@@ -1921,7 +1948,7 @@ class ECharts extends Eventful<ECEventDefinition> {
                     // and this checking is called on frame, we also check
                     // animation finished for robustness.
                     zr.animation.isFinished()
-                    && !ecIns[OPTION_UPDATED_KEY]
+                    && !ecIns[PENDING_UPDATE]
                     && !ecIns._scheduler.unfinished
                     && !ecIns._pendingActions.length
                 ) {
@@ -2075,9 +2102,9 @@ class ECharts extends Eventful<ECEventDefinition> {
             updateHoverLayerStatus(ecIns, ecModel);
         };
 
-        performPostUpdateFuncs = function (ecModel: GlobalModel, api: ExtensionAPI): void {
+        performPostUpdateFuncs = function (ecModel: GlobalModel, api: ExtensionAPI, params: PostUpdateParams): void {
             each(postUpdateFuncs, function (func) {
-                func(ecModel, api);
+                func(ecModel, api, params);
             });
         };
 
@@ -2411,7 +2438,7 @@ class ECharts extends Eventful<ECEventDefinition> {
         ): void {
             const ecModel = chart._model;
 
-            zrUtil.each(modelUtil.normalizeToArray(transitionOpt), transOpt => {
+            each(modelUtil.normalizeToArray(transitionOpt), transOpt => {
                 let errMsg;
                 const fromOpt = transOpt.from;
                 const toOpt = transOpt.to;
@@ -2583,7 +2610,7 @@ export function init(
     }
 
     if (__DEV__) {
-        if (zrUtil.isDom(dom)
+        if (isDom(dom)
             && dom.nodeName.toUpperCase() !== 'CANVAS'
             && (
                 (!dom.clientWidth && (!opts || opts.width == null))
@@ -2631,7 +2658,7 @@ export function init(
  */
 export function connect(groupId: string | EChartsType[]): string {
     // Is array of charts
-    if (zrUtil.isArray(groupId)) {
+    if (isArray(groupId)) {
         const charts = groupId;
         groupId = null;
         // If any chart has group
@@ -2891,7 +2918,7 @@ export function registerLoading(
  *     });
  */
 export function setCanvasCreator(creator: () => HTMLCanvasElement): void {
-    zrUtil.$override('createCanvas', creator);
+    $override('createCanvas', creator);
 }
 
 /**
@@ -2948,31 +2975,31 @@ registerAction({
     type: HIGHLIGHT_ACTION_TYPE,
     event: HIGHLIGHT_ACTION_TYPE,
     update: HIGHLIGHT_ACTION_TYPE
-}, zrUtil.noop);
+}, noop);
 
 registerAction({
     type: DOWNPLAY_ACTION_TYPE,
     event: DOWNPLAY_ACTION_TYPE,
     update: DOWNPLAY_ACTION_TYPE
-}, zrUtil.noop);
+}, noop);
 
 registerAction({
     type: SELECT_ACTION_TYPE,
     event: SELECT_ACTION_TYPE,
     update: SELECT_ACTION_TYPE
-}, zrUtil.noop);
+}, noop);
 
 registerAction({
     type: UNSELECT_ACTION_TYPE,
     event: UNSELECT_ACTION_TYPE,
     update: UNSELECT_ACTION_TYPE
-}, zrUtil.noop);
+}, noop);
 
 registerAction({
     type: TOGGLE_SELECT_ACTION_TYPE,
     event: TOGGLE_SELECT_ACTION_TYPE,
     update: TOGGLE_SELECT_ACTION_TYPE
-}, zrUtil.noop);
+}, noop);
 
 // Default theme
 registerTheme('light', lightTheme);
diff --git a/src/echarts.all.ts b/src/echarts.all.ts
index e66f8b9..95ab6d7 100644
--- a/src/echarts.all.ts
+++ b/src/echarts.all.ts
@@ -87,6 +87,10 @@ import {
     TransformComponent
 } from './export/components';
 
+import {
+    UniversalTransition
+} from './export/features';
+
 
 // -----------------
 // Render engines
@@ -332,4 +336,12 @@ use(AriaComponent);
 // });
 use(TransformComponent);
 
-use(DatasetComponent);
\ No newline at end of file
+use(DatasetComponent);
+
+// universal transition
+// chart.setOption({
+//     series: {
+//         universalAnimation: { enabled: true }
+//     }
+// })
+use(UniversalTransition);
\ No newline at end of file
diff --git a/src/animation/universalTransition.ts b/src/export/features.ts
similarity index 84%
copy from src/animation/universalTransition.ts
copy to src/export/features.ts
index af2cfa0..b9b3c45 100644
--- a/src/animation/universalTransition.ts
+++ b/src/export/features.ts
@@ -17,6 +17,6 @@
 * under the License.
 */
 
-// Universal transitions that can animate between any shapes and any properties in any amounts.
-
+// Module that exports complex but fancy features.
 
+export {installUniversalTransition as UniversalTransition} from '../animation/universalTransition';
\ No newline at end of file
diff --git a/src/model/Global.ts b/src/model/Global.ts
index 0a06d5b..32582b0 100644
--- a/src/model/Global.ts
+++ b/src/model/Global.ts
@@ -758,7 +758,7 @@ echarts.use([${seriesImportName}]);`);
      */
     getSeries(): SeriesModel[] {
         return filter(
-            this._componentsMap.get('series').slice() as SeriesModel[],
+            this._componentsMap.get('series') as SeriesModel[],
             oneSeries => !!oneSeries
         );
     }
diff --git a/src/util/types.ts b/src/util/types.ts
index 1d11e52..6dd0e3a 100644
--- a/src/util/types.ts
+++ b/src/util/types.ts
@@ -233,8 +233,12 @@ export interface OptionPreprocessor {
     (option: ECUnitOption, isTheme: boolean): void
 }
 
+export interface PostUpdateParams {
+    oldSeries?: SeriesModel[]
+    newSeries?: SeriesModel[]
+}
 export interface PostUpdater {
-    (ecModel: GlobalModel, api: ExtensionAPI): void;
+    (ecModel: GlobalModel, api: ExtensionAPI, params: PostUpdateParams): void;
 }
 
 export interface StageHandlerReset {
@@ -1594,6 +1598,13 @@ export interface SeriesOption<
     stateAnimation?: AnimationOption
 
     /**
+     * If enabled universal animation cross series.
+     */
+    universalAnimation?: {
+        enabled: boolean
+    }
+
+    /**
      * Map of selected data
      * key is name or index of data.
      */
diff --git a/test/universalTransition.html b/test/universalTransition.html
new file mode 100644
index 0000000..00386e6
--- /dev/null
+++ b/test/universalTransition.html
@@ -0,0 +1,163 @@
+<!DOCTYPE html>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+
+<html>
+    <head>
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <script src="lib/simpleRequire.js"></script>
+        <script src="lib/config.js"></script>
+        <script src="lib/jquery.min.js"></script>
+        <script src="lib/facePrint.js"></script>
+        <script src="lib/testHelper.js"></script>
+        <!-- <script src="ut/lib/canteen.js"></script> -->
+        <link rel="stylesheet" href="lib/reset.css" />
+    </head>
+    <body>
+        <style>
+        </style>
+
+
+
+        <div id="main0"></div>
+
+
+
+
+
+
+        <script>
+        require([
+            'echarts'
+        ], function (echarts) {
+            var option;
+
+            option = {
+                title: {
+                    text: 'Bar'
+                },
+                dataset: [{
+                    dimensions: ['name', 'age', 'profession', 'score', 'date'],
+                    source: [
+                        ['Hannah Krause ', 41, 'Engineer', 314, '2011-02-12'],
+                        ['Zhao Qian ', 20, 'Teacher', 351, '2011-03-01'],
+                        ['Jasmin Krause ', 52, 'Musician', 287, '2011-02-14'],
+                        ['Li Lei', 37, 'Teacher', 219, '2011-02-18'],
+                        ['Karle Neumann ', 25, 'Engineer', 253, '2011-04-02'],
+                        ['Adrian Groß', 19, 'Teacher', null, '2011-01-16'],
+                        ['Mia Neumann', 71, 'Engineer', 165, '2011-03-19'],
+                        ['Böhm Fuchs', 36, 'Musician', 318, '2011-02-24'],
+                        ['Han Meimei ', 67, 'Engineer', 366, '2011-03-12'],
+                    ]
+                }, {
+                    transform: {
+                        type: 'sort',
+                        config: { dimension: 'score', order: 'desc' }
+                    }
+                }],
+                xAxis: {
+                    type: 'category',
+                    axisLabel: { interval: 0, rotate: 30 },
+                },
+                yAxis: {},
+                series: {
+                    type: 'bar',
+                    id: 'main',
+                    encode: { x: 'name', y: 'score', itemName: 'name' },
+                    datasetIndex: 1
+                }
+            };
+
+            var chart = testHelper.create(echarts, 'main0', {
+                title: [
+                    'Convert between pie, bar, scatter, line'
+                ],
+                // height: 300,
+                buttons: [{
+                    text: 'Pie',
+                    onclick: function () {
+                        chart.setOption({
+                            title: {
+                                text: 'Bar'
+                            },
+                            dataset: [{
+                                dimensions: ['name', 'age', 'profession', 'score', 'date'],
+                                source: [
+                                    ['Hannah Krause ', 41, 'Engineer', 314, '2011-02-12'],
+                                    ['Zhao Qian ', 20, 'Teacher', 351, '2011-03-01'],
+                                    ['Jasmin Krause ', 52, 'Musician', 287, '2011-02-14'],
+                                    ['Li Lei', 37, 'Teacher', 219, '2011-02-18'],
+                                    ['Karle Neumann ', 25, 'Engineer', 253, '2011-04-02'],
+                                    ['Adrian Groß', 19, 'Teacher', null, '2011-01-16'],
+                                    ['Mia Neumann', 71, 'Engineer', 165, '2011-03-19'],
+                                    ['Böhm Fuchs', 36, 'Musician', 318, '2011-02-24'],
+                                    ['Han Meimei ', 67, 'Engineer', 366, '2011-03-12'],
+                                ]
+                            }, {
+                                transform: {
+                                    type: 'sort',
+                                    config: { dimension: 'score', order: 'desc' }
+                                }
+                            }],
+                            series: {
+                                id: 'main',
+                                type: 'pie',
+                                encode: { x: 'name', y: 'score', itemName: 'name' },
+                                datasetIndex: 1,
+                                universalAnimation: {
+                                    enabled: true
+                                }
+                            }
+                        }, true);
+                    }
+                }, {
+                    text: 'Scatter',
+                    onclick: function () {
+                        chart.setOption({
+                            title: { text: 'Scatter' },
+                            xAxis: {
+                                show: true
+                            },
+                            yAxis: {
+                                show: true
+                            },
+                            series: {
+                                id: 'main',
+                                type: 'scatter',
+                                symbolSize: 30,
+                                encode: { x: 'name', y: 'score', itemName: 'name' },
+                                datasetIndex: 1,
+                                universalAnimation: {
+                                    enabled: true
+                                }
+                            }
+                        }, true);
+                    }
+                }],
+                option: option
+            });
+        });
+        </script>
+
+
+    </body>
+</html>
+

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