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/09/27 17:46:51 UTC

[incubator-echarts] branch custom-morph2 created (now 82f62ca)

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

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


      at 82f62ca  test: add experimental transform.

This branch includes the following new commits:

     new b7b941a  fix: fix task clear when setOption in the second time and reuse series.
     new 8233187  feature: (1) Support custom series morphing with split/merge effect. (2) Add API: setOption(option, { transition: { ... } }) to indicate the transition relationship. (3) Fix underlying schedule framework buggy disopose when setOption at the second time and change data.
     new 58397cd  fix: enhance transition morphing API.
     new bc1f8a0  enhance: [data transform] (1) Clarify the rule of when need to return the dimension. (2) Clarify the API name and behavior of data transform. (3) Try to freeze the raw data exposed to data transform.
     new 82f62ca  test: add experimental transform.

The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



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


[incubator-echarts] 03/05: fix: enhance transition morphing API.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 58397cd61297beafe27e178e41d12203f08d1ee2
Author: 100pah <su...@gmail.com>
AuthorDate: Fri Sep 25 03:40:15 2020 +0800

    fix: enhance transition morphing API.
---
 src/chart/custom.ts              | 125 +++++----
 src/data/DataDiffer.ts           |   6 +-
 src/data/List.ts                 |   2 +-
 src/data/helper/transform.ts     |  51 +++-
 src/echarts.ts                   |  44 ++--
 src/model/Series.ts              |   2 +
 test/custom-shape-morphing2.html | 532 ++++++++++++++++++++++++++++-----------
 7 files changed, 517 insertions(+), 245 deletions(-)

diff --git a/src/chart/custom.ts b/src/chart/custom.ts
index ca87fb4..fed727d 100644
--- a/src/chart/custom.ts
+++ b/src/chart/custom.ts
@@ -19,7 +19,7 @@
 
 import {
     hasOwn, assert, isString, retrieve2, retrieve3, defaults, each,
-    keys, isArrayLike, bind, isFunction, eqNaN, isArray
+    keys, isArrayLike, bind, isFunction, eqNaN, noop
 } from 'zrender/src/core/util';
 import * as graphicUtil from '../util/graphic';
 import { setDefaultStateProxy, enableHoverEmphasis } from '../util/states';
@@ -27,7 +27,7 @@ import * as labelStyleHelper from '../label/labelStyle';
 import {getDefaultLabel} from './helper/labelHelper';
 import createListFromArray from './helper/createListFromArray';
 import {getLayoutOnAxis} from '../layout/barGrid';
-import DataDiffer from '../data/DataDiffer';
+import DataDiffer, { DataDiffCallbackMode } from '../data/DataDiffer';
 import SeriesModel from '../model/Series';
 import Model from '../model/Model';
 import ChartView from '../view/Chart';
@@ -462,56 +462,69 @@ class CustomSeriesView extends ChartView {
         // roam or data zoom according to `actionType`.
 
         const transOpt = customSeries.__transientTransitionOpt;
+        const cbMode: DataDiffCallbackMode = transOpt ? 'multiple' : 'single';
 
-        (new DataDiffer(
-            oldData ? oldData.getIndices() : [],
-            data.getIndices(),
-            createGetKey(oldData, transOpt && transOpt.from),
-            createGetKey(data, transOpt && transOpt.to),
-            null,
-            transOpt ? 'multiple' : 'single'
-        ))
-        .add(function (newIdx) {
-            createOrUpdateItem(
-                null, newIdx, renderItem(newIdx, payload), customSeries, group, data, null
-            );
-        })
-        .update(function (newIdx, oldIdx) {
-            createOrUpdateItem(
-                oldData.getItemGraphicEl(oldIdx),
-                newIdx, renderItem(newIdx, payload), customSeries, group, data, null
-            );
-        })
-        .remove(function (oldIdx) {
-            doRemoveEl(oldData.getItemGraphicEl(oldIdx), customSeries, group);
-        })
-        .updateManyToOne(function (newIdx, oldIndices) {
-            const oldElsToMerge: graphicUtil.Path[] = [];
-            for (let i = 0; i < oldIndices.length; i++) {
-                const oldEl = oldData.getItemGraphicEl(oldIndices[i]);
-                if (elCanMorph(oldEl)) {
-                    oldElsToMerge.push(oldEl);
+        if (transOpt && (transOpt.from == null || transOpt.to == null)) {
+            oldData && oldData.each(function (oldIdx) {
+                doRemoveEl(oldData.getItemGraphicEl(oldIdx), customSeries, group);
+            });
+            data.each(function (newIdx) {
+                createOrUpdateItem(
+                    null, newIdx, renderItem(newIdx, payload), customSeries, group, data, null
+                );
+            });
+        }
+        else {
+            (new DataDiffer(
+                oldData ? oldData.getIndices() : [],
+                data.getIndices(),
+                createGetKey(oldData, cbMode, transOpt && transOpt.from),
+                createGetKey(data, cbMode, transOpt && transOpt.to),
+                null,
+                cbMode
+            ))
+            .add(function (newIdx) {
+                createOrUpdateItem(
+                    null, newIdx, renderItem(newIdx, payload), customSeries, group, data, null
+                );
+            })
+            .update(function (newIdx, oldIdx) {
+                createOrUpdateItem(
+                    oldData.getItemGraphicEl(oldIdx),
+                    newIdx, renderItem(newIdx, payload), customSeries, group, data, null
+                );
+            })
+            .remove(function (oldIdx) {
+                doRemoveEl(oldData.getItemGraphicEl(oldIdx), customSeries, group);
+            })
+            .updateManyToOne(function (newIdx, oldIndices) {
+                const oldElsToMerge: graphicUtil.Path[] = [];
+                for (let i = 0; i < oldIndices.length; i++) {
+                    const oldEl = oldData.getItemGraphicEl(oldIndices[i]);
+                    if (elCanMorph(oldEl)) {
+                        oldElsToMerge.push(oldEl);
+                    }
+                    removeElementDirectly(oldEl, group);
                 }
-                removeElementDirectly(oldEl, group);
-            }
-            createOrUpdateItem(
-                null, newIdx, renderItem(newIdx, payload), customSeries,
-                group, data, oldElsToMerge
-            );
-        })
-        .updateOneToMany(function (newIndices, oldIdx) {
-            const newLen = newIndices.length;
-            const oldEl = oldData.getItemGraphicEl(oldIdx);
-            const oldElSplitted = elCanMorph(oldEl) ? splitShapeForMorphingFrom(oldEl, newLen) : [];
-            removeElementDirectly(oldEl, group);
-            for (let i = 0; i < newLen; i++) {
                 createOrUpdateItem(
-                    null, newIndices[i], renderItem(newIndices[i], payload), customSeries,
-                    group, data, oldElSplitted[i]
+                    null, newIdx, renderItem(newIdx, payload), customSeries,
+                    group, data, oldElsToMerge
                 );
-            }
-        })
-        .execute();
+            })
+            .updateOneToMany(function (newIndices, oldIdx) {
+                const newLen = newIndices.length;
+                const oldEl = oldData.getItemGraphicEl(oldIdx);
+                const oldElSplitted = elCanMorph(oldEl) ? splitShapeForMorphingFrom(oldEl, newLen) : [];
+                removeElementDirectly(oldEl, group);
+                for (let i = 0; i < newLen; i++) {
+                    createOrUpdateItem(
+                        null, newIndices[i], renderItem(newIndices[i], payload), customSeries,
+                        group, data, oldElSplitted[i]
+                    );
+                }
+            })
+            .execute();
+        }
 
         // Do clipping
         const clipPath = customSeries.get('clip', true)
@@ -582,20 +595,24 @@ class CustomSeriesView extends ChartView {
 ChartView.registerClass(CustomSeriesView);
 
 
-function createGetKey(data: List, dimension: DimensionLoose) {
+function createGetKey(
+    data: List,
+    cbMode: DataDiffCallbackMode,
+    dimension: DimensionLoose
+) {
     if (!data) {
         return;
     }
 
-    const diffBy = data.getDimension(dimension);
-
-    if (diffBy == null) {
+    if (cbMode === 'single') {
         return function (rawIdx: number, dataIndex: number) {
             return data.getId(dataIndex);
         };
     }
 
-    const dimInfo = data.getDimensionInfo(diffBy);
+    const diffByDimName = data.getDimension(dimension);
+    const dimInfo = data.getDimensionInfo(diffByDimName);
+
     if (!dimInfo) {
         let errMsg = '';
         if (__DEV__) {
@@ -605,7 +622,7 @@ function createGetKey(data: List, dimension: DimensionLoose) {
     }
     const ordinalMeta = dimInfo.ordinalMeta;
     return function (rawIdx: number, dataIndex: number) {
-        let key = data.get(diffBy, dataIndex);
+        let key = data.get(diffByDimName, dataIndex);
         if (ordinalMeta) {
             key = ordinalMeta.categories[key as number];
         }
diff --git a/src/data/DataDiffer.ts b/src/data/DataDiffer.ts
index 43baad1..4b56017 100644
--- a/src/data/DataDiffer.ts
+++ b/src/data/DataDiffer.ts
@@ -266,12 +266,12 @@ class DataDiffer<CTX = unknown> {
         keyArr: string[],
         keyGetterName: '_oldKeyGetter' | '_newKeyGetter'
     ): void {
-        const cbModeByKey = this._cbModeMultiple;
+        const cbModeMultiple = this._cbModeMultiple;
 
         for (let i = 0; i < arr.length; i++) {
             // Add prefix to avoid conflict with Object.prototype.
             const key = '_ec_' + this[keyGetterName](arr[i], i);
-            if (!cbModeByKey) {
+            if (!cbModeMultiple) {
                 keyArr[i] = key;
             }
             if (!map) {
@@ -285,7 +285,7 @@ class DataDiffer<CTX = unknown> {
                 // Simple optimize: in most cases, one index has one key,
                 // do not need array.
                 map[key] = i;
-                if (cbModeByKey) {
+                if (cbModeMultiple) {
                     keyArr.push(key);
                 }
             }
diff --git a/src/data/List.ts b/src/data/List.ts
index 1f0d0a8..34b2736 100644
--- a/src/data/List.ts
+++ b/src/data/List.ts
@@ -2100,7 +2100,7 @@ class List<
             dimensions: ItrParamDims
         ): Array<DimensionLoose> {
             if (!zrUtil.isArray(dimensions)) {
-                dimensions = [dimensions];
+                dimensions = dimensions != null ? [dimensions] : [];
             }
             return dimensions;
         };
diff --git a/src/data/helper/transform.ts b/src/data/helper/transform.ts
index 68715a8..bad0de4 100644
--- a/src/data/helper/transform.ts
+++ b/src/data/helper/transform.ts
@@ -47,15 +47,6 @@ export interface DataTransformOption {
     print?: boolean;
 }
 
-export interface DataTransformResult {
-    source: Source;
-}
-
-export interface DataTransform {
-    (sourceList: Source[], config: DataTransformConfig): {
-    }
-}
-
 export interface ExternalDataTransform<TO extends DataTransformOption = DataTransformOption> {
     // Must include namespace like: 'ecStat:regression'
     type: string,
@@ -73,7 +64,21 @@ interface ExternalDataTransformParam<TO extends DataTransformOption = DataTransf
 }
 export interface ExternalDataTransformResultItem {
     data: OptionSourceData;
+    /**
+     * A `transform` can optionally return a dimensions definition.
+     * If the `transform` make sure the dimensions of the result data, it can make that return.
+     * Otherwise, it's recommanded not to make such a `dimensions`. In this case, echarts will
+     * inherit dimensions definition from the upstream. If there is no dimensions definition
+     * of the upstream, the echarts will left it undefined.
+     * Notice: return a incorrect dimensions definition will cause the downstream can not use
+     * the values under that dimensions correctly.
+     *
+     * @see also `source.isDimensionsDefined`.
+     */
     dimensions?: DimensionDefinitionLoose[];
+    /**
+     * Similar to `dimensions`, a `transform` can return that optionally.
+     */
     sourceHeader?: OptionSourceHeader;
 }
 interface ExternalDimensionDefinition extends Partial<DimensionDefinition> {
@@ -97,14 +102,33 @@ class ExternalSource {
     sourceFormat: SourceFormat;
     sourceHeaderCount: number;
 
+    /**
+     * @return If dimension not found, return null/undefined.
+     */
     getDimensionInfo(dim: DimensionLoose): ExternalDimensionDefinition {
         return;
     }
 
+    /**
+     * If dimensions are defined (see `isDimensionsDefined`), `dimensionInfoAll` is corresponding to
+     * the defined dimensions.
+     * Otherwise, `dimensionInfoAll` is determined by data columns.
+     * @return Always return an array (even empty array).
+     */
     getDimensionInfoAll(): ExternalDimensionDefinition[] {
         return;
     }
 
+    /**
+     * dimensions defined if and only if:
+     * (a) dataset.dimensions are declared.
+     * or
+     * (b) dataset data include dimensions definitions in data (detected or via specified `sourceHeader`)
+     */
+    isDimensionsDefined(): boolean {
+        return;
+    }
+
     getRawDataItem(dataIndex: number): OptionDataItem {
         return;
     }
@@ -210,6 +234,7 @@ function createExternalSource(internalSource: Source): ExternalSource {
 
     extSource.getDimensionInfo = bind(getDimensionInfo, null, dimensions, dimsByName);
     extSource.getDimensionInfoAll = bind(getDimensionInfoAll, null, dimensions);
+    extSource.isDimensionsDefined = bind(isDimensionsDefined, null, !!dimsDef);
 
     return extSource;
 }
@@ -235,12 +260,14 @@ function getDimensionInfo(
     }
 }
 
-function getDimensionInfoAll(
-    dimensions: ExternalDimensionDefinition[]
-): ExternalDimensionDefinition[] {
+function getDimensionInfoAll(dimensions: ExternalDimensionDefinition[]): ExternalDimensionDefinition[] {
     return dimensions;
 }
 
+function isDimensionsDefined(defined: boolean): boolean {
+    return defined;
+}
+
 
 
 const externalTransformMap = createHashMap<ExternalDataTransform, string>();
diff --git a/src/echarts.ts b/src/echarts.ts
index c27a65b..9930fe1 100644
--- a/src/echarts.ts
+++ b/src/echarts.ts
@@ -187,16 +187,20 @@ interface SetOptionOpts {
     // Rule: only `id` mapped will be merged,
     // other components of the certain `mainType` will be removed.
     replaceMerge?: GlobalModelSetOptionOpts['replaceMerge'];
-    transition?: SetOptionTransitionOptItem | SetOptionTransitionOptItem[];
+    transition?: SetOptionTransitionOpt
 };
 
 interface SetOptionTransitionOptItem {
-    from: SetOptionTransitionOptFinder | DimensionLoose;
-    to: SetOptionTransitionOptFinder | DimensionLoose;
+    // If `from` not given, it means that do not make series transition mandatorily.
+    // There might be transition mapping dy default. Sometimes we do not need them,
+    // which might bring about misleading.
+    from?: SetOptionTransitionOptFinder;
+    to: SetOptionTransitionOptFinder;
 }
 interface SetOptionTransitionOptFinder extends modelUtil.ModelFinderObject {
     dimension: DimensionLoose;
 }
+type SetOptionTransitionOpt = SetOptionTransitionOptItem | SetOptionTransitionOptItem[];
 
 
 type EventMethodName = 'on' | 'off';
@@ -267,7 +271,7 @@ let createExtensionAPI: (ecIns: ECharts) => ExtensionAPI;
 let enableConnect: (ecIns: ECharts) => void;
 let setTransitionOpt: (
     chart: ECharts,
-    transitionOpt: SetOptionTransitionOptItem | SetOptionTransitionOptItem[]
+    transitionOpt: SetOptionTransitionOpt
 ) => void;
 
 let markStatusToUpdate: (ecIns: ECharts) => void;
@@ -518,7 +522,7 @@ class ECharts extends Eventful {
 
         let silent;
         let replaceMerge;
-        let transitionOpt;
+        let transitionOpt: SetOptionTransitionOpt;
         if (isObject(notMerge)) {
             lazyUpdate = notMerge.lazyUpdate;
             silent = notMerge.silent;
@@ -2284,24 +2288,18 @@ class ECharts extends Eventful {
 
         setTransitionOpt = function (
             chart: ECharts,
-            transitionOpt: SetOptionTransitionOptItem | SetOptionTransitionOptItem[]
+            transitionOpt: SetOptionTransitionOpt
         ): void {
             const ecModel = chart._model;
-            zrUtil.each(modelUtil.normalizeToArray(transitionOpt), transOpt => {
-
-                function normalizeFromTo(fromTo: DimensionLoose | SetOptionTransitionOptFinder) {
-                    return (zrUtil.isString(fromTo) || zrUtil.isNumber(fromTo))
-                        ? { dimension: fromTo }
-                        : fromTo;
-                }
 
+            zrUtil.each(modelUtil.normalizeToArray(transitionOpt), transOpt => {
                 let errMsg;
-                const fromOpt = normalizeFromTo(transOpt.from);
-                const toOpt = normalizeFromTo(transOpt.to);
+                const fromOpt = transOpt.from;
+                const toOpt = transOpt.to;
 
-                if (fromOpt == null || toOpt == null) {
+                if (toOpt == null) {
                     if (__DEV__) {
-                        errMsg = '`transition.from` and `transition.to` must be specified.';
+                        errMsg = '`transition.to` must be specified.';
                     }
                     throwError(errMsg);
                 }
@@ -2312,7 +2310,7 @@ class ECharts extends Eventful {
                     enableAll: false,
                     enableNone: false
                 };
-                const fromResult = modelUtil.parseFinder(ecModel, fromOpt, finderOpt);
+                const fromResult = fromOpt ? modelUtil.parseFinder(ecModel, fromOpt, finderOpt) : null;
                 const toResult = modelUtil.parseFinder(ecModel, toOpt, finderOpt);
                 const toSeries = toResult.seriesModel;
 
@@ -2322,25 +2320,19 @@ class ECharts extends Eventful {
                         errMsg = '`transition` is only supported on series.';
                     }
                 }
-                if (fromResult.seriesModel !== toSeries) {
+                if (fromResult && fromResult.seriesModel !== toSeries) {
                     errMsg = '';
                     if (__DEV__) {
                         errMsg = '`transition.from` and `transition.to` must be specified to the same series.';
                     }
                 }
-                if (fromOpt.dimension == null || toOpt.dimension == null) {
-                    errMsg = '';
-                    if (__DEV__) {
-                        errMsg = '`dimension` must be specified in `transition`.';
-                    }
-                }
                 if (errMsg != null) {
                     throwError(errMsg);
                 }
 
                 // Just a temp solution: mount them on series.
                 toSeries.__transientTransitionOpt = {
-                    from: fromOpt.dimension,
+                    from: fromOpt ? fromOpt.dimension : null,
                     to: toOpt.dimension
                 };
             });
diff --git a/src/model/Series.ts b/src/model/Series.ts
index 3a0f774..78a6776 100644
--- a/src/model/Series.ts
+++ b/src/model/Series.ts
@@ -135,6 +135,8 @@ class SeriesModel<Opt extends SeriesOption = SeriesOption> extends ComponentMode
         // [MEMO] Currently only support single "from". If intending to
         // support multiple "from", if not hard to implement "merge morph",
         // but correspondingly not easy to implement "split morph".
+
+        // Both from and to can be null/undefined, which meams no transform mapping.
         from: DimensionLoose;
         to: DimensionLoose;
     };
diff --git a/test/custom-shape-morphing2.html b/test/custom-shape-morphing2.html
index 01e8bc2..e5a8cb7 100644
--- a/test/custom-shape-morphing2.html
+++ b/test/custom-shape-morphing2.html
@@ -22,10 +22,11 @@ under the License.
     <head>
         <meta charset='utf-8'>
         <meta name="viewport" content="width=device-width, initial-scale=1" />
-        <script src='lib/esl.js'></script>
-        <script src='lib/config.js'></script>
         <script src='lib/jquery.min.js'></script>
+        <script src="../dist/echarts.js"></script>
         <script src="lib/testHelper.js"></script>
+        <!-- <script src="lib/ecStat.min.js"></script> -->
+        <script src="../../echarts-stat/dist/ecStat.js"></script>
         <link rel="stylesheet" href="lib/reset.css" />
     </head>
     <body>
@@ -38,15 +39,121 @@ under the License.
 
         <script>
 
-            require([
-                'echarts'
-            ], function (echarts) {
+            /**
+             * @usage
+             *
+             * dataset: [{
+             *     source: [
+             *         ['aa', 'bb', 'cc', 'tag'],
+             *         [12, 0.33, 5200, 'AA'],
+             *         [21, 0.65, 8100, 'AA'],
+             *         ...
+             *     ]
+             * }, {
+             *     transform: {
+             *         type: 'my:aggregate',
+             *         config: {
+             *             resultDimensions: [
+             *                 // by default, use the same name with `from`.
+             *                 { from: 'aa', method: 'sum' },
+             *                 { from: 'bb', method: 'count' },
+             *                 { from: 'cc' }, // method by default: use the first value.
+             *                 { from: 'tag' }
+             *             ],
+             *             groupBy: 'tag'
+             *         }
+             *     }
+             * }]
+             */
+            var sumTransform = {
+                type: 'my:aggregate',
+                transform: function (params) {
+                    var source = params.source;
+                    var config = params.config;
+                    var resultDimensionsConfig = config.resultDimensions;
+                    var groupBy = config.groupBy;
+
+                    var calcMap = {};
+                    var resultData = [];
+
+                    var groupByDimInfo = source.getDimensionInfo(groupBy);
+                    var resultDimInfoList = [];
+                    var resultDimensions = [];
+                    for (var i = 0; i < resultDimensionsConfig.length; i++) {
+                        var resultDimInfoConfig = resultDimensionsConfig[i];
+                        var resultDimInfo = source.getDimensionInfo(resultDimInfoConfig.from);
+                        resultDimInfo.method = resultDimInfoConfig.method;
+                        resultDimInfoList.push(resultDimInfo);
+                        if (resultDimInfoConfig.name != null) {
+                            resultDimInfo.name = resultDimInfoConfig.name;
+                        }
+                        resultDimensions.push(resultDimInfo.name);
+                    }
 
-                const COLORS = [
-                    '#37A2DA', '#e06343', '#37a354', '#b55dba', '#b5bd48', '#8378EA', '#96BFFF'
-                ];
-                var COUNT = 50;
-                var CONTENT_COLOR = '#37A2DA';
+                    for (var i = 0; i < source.count(); i++) {
+                        var line = source.getRawDataItem(i);
+                        var groupByVal = source.retrieveItemValue(line, groupByDimInfo.index);
+
+                        if (!calcMap.hasOwnProperty(groupByVal)) {
+                            var newLine = [];
+                            calcMap[groupByVal] = newLine;
+                            resultData.push(newLine);
+                            for (var j = 0; j < resultDimInfoList.length; j++) {
+                                var resultDimInfo = resultDimInfoList[j];
+                                var method = resultDimInfo.method;
+                                newLine[j] = resultDimInfo.index === groupByDimInfo.index
+                                    ? groupByVal
+                                    : (method === 'sum' || method === 'count')
+                                    ? 0
+                                    // By default, method: 'first'
+                                    : source.retrieveItemValue(line, resultDimInfo.index);
+                            }
+                        }
+                        else {
+                            var newLine = calcMap[groupByVal];
+                            for (var j = 0; j < resultDimInfoList.length; j++) {
+                                var resultDimInfo = resultDimInfoList[j];
+                                var method = resultDimInfo.method;
+                                if (resultDimInfo.index !== groupByDimInfo.index) {
+                                    if (method === 'sum') {
+                                        newLine[j] += line[resultDimInfo.index];
+                                    }
+                                    else if (method === 'count') {
+                                        newLine[j] += 1;
+                                    }
+                                }
+                            }
+                        }
+                    }
+
+                    return {
+                        dimensions: resultDimensions,
+                        data: resultData
+                    };
+                }
+            };
+
+
+            function initRawData(count) {
+                var M_TAG_LIST = ['MA', 'MB', 'MC', 'MD'];
+                var Z_TAG_LIST = ['ZA', 'ZB', 'ZC', 'ZD', 'ZE'];
+                var data = [];
+                var currDate = +new Date(2015, 2, 1);
+                var ONE_DAY = 3600 * 24 * 1000;
+                for (var i = 0; i < count; i++, currDate += ONE_DAY) {
+                    data.push([
+                        currDate,
+                        makeRandomValue([10, 40], 0),
+                        makeRandomValue([0.01, 0.99], 2),
+                        makeRandomValue([1, 10], 1),
+                        // makeRandomValue([1000, 9000], 0),
+                        // makeRandomValue([1000, 9000], 0),
+                        // makeRandomValue([10, 90], 0),
+                        M_TAG_LIST[makeRandomValue([0, M_TAG_LIST.length - 1], 0)],
+                        Z_TAG_LIST[makeRandomValue([0, Z_TAG_LIST.length - 1], 0)],
+                        'P' + i,
+                    ]);
+                }
 
                 function makeRandomValue(range, precision) {
                     return +(
@@ -54,89 +161,119 @@ under the License.
                     ).toFixed(precision);
                 }
 
-                var M_TAG_LIST = ['MA', 'MB', 'MC', 'MD'];
-                var Z_TAG_LIST = ['ZA', 'ZB', 'ZC', 'ZD', 'ZE'];
+                return data;
+            }
+        </script>
+
+
+
+        <script>
+            (function () {
+
+                echarts.registerTransform(sumTransform);
+                echarts.registerTransform(ecStat.transform.clustering);
+
+
+                const COLORS = [
+                    '#37A2DA', '#e06343', '#37a354', '#b55dba', '#b5bd48', '#8378EA', '#96BFFF'
+                ];
+                var COUNT = 50;
+                var CONTENT_COLOR = '#37A2DA';
+
                 var ANIMATION_DURATION_UPDATE = 1500;
 
-                function initRawData() {
-                    var DIMENSION = {
-                        DATE: 0,
-                        ATA: 1,
-                        STE: 2,
-                        CTZ: 3,
-                        M_TAG: 4,
-                        Z_TAG: 5,
-                        ID: 6
-                    };
-                    var data = [];
-                    var currDate = +new Date(2015, 2, 1);
-                    var ONE_DAY = 3600 * 24 * 1000;
-                    for (var i = 0; i < COUNT; i++, currDate += ONE_DAY) {
-                        var line = [];
-                        data.push(line);
-                        line[DIMENSION.DATE] = currDate;
-                        line[DIMENSION.ATA] = makeRandomValue([10, 40], 0);
-                        line[DIMENSION.STE] = makeRandomValue([0.01, 0.99], 2);
-                        line[DIMENSION.CTZ] = makeRandomValue([1, 10], 1);
-                        line[DIMENSION.M_TAG] = M_TAG_LIST[makeRandomValue([0, M_TAG_LIST.length - 1], 0)];
-                        line[DIMENSION.Z_TAG] = Z_TAG_LIST[makeRandomValue([0, Z_TAG_LIST.length - 1], 0)];
-                        line[DIMENSION.ID] = 'P' + i;
+                // var rawData = initRawData(COUNT);
+                // console.log(JSON.stringify(rawData));
+                var rawData = [[1425139200000,34,0.13,2,"MD","ZD","P0"],[1425225600000,28,0.71,1.5,"MB","ZD","P1"],[1425312000000,23,0.9,2.8,"MA","ZC","P2"],[1425398400000,21,0.58,6,"MB","ZC","P3"],[1425484800000,14,0.1,1.6,"MC","ZA","P4"],[1425571200000,21,0.6,7.7,"MC","ZA","P5"],[1425657600000,23,0.31,2.6,"MC","ZC","P6"],[1425744000000,34,0.74,2.4,"MD","ZE","P7"],[1425830400000,14,0.59,2.3,"MB","ZD","P8"],[1425916800000,18,0.85,5.1,"MB","ZB","P9"],[1426003200000,36,0.96,1.2,"MC","ZC"," [...]
+
+                var RAW_DATA_DIMENSIONS = ['DATE', 'ATA', 'STE', 'CTZ', 'M_TAG', 'Z_TAG', 'ID'];
+                var M_TAG_SUM_DIMENSIONS = ['ATA', 'STE', 'CTZ', 'M_TAG'];
+                var RAW_CLUSTER_DIMENSIONS = ['DATE', 'ATA', 'STE', 'CTZ', 'M_TAG', 'Z_TAG', 'ID', 'CLUSTER_IDX', 'CLUSTER_CENTER_ATA', 'CLUSTER_CENTER_STE'];
+                var RAW_CLUSTER_CENTERS_DIMENSIONS = ['COUNT', 'CLUSTER_IDX', 'CLUSTER_CENTER_ATA', 'CLUSTER_CENTER_STE'];
+
+                // Key: datasetId
+                var TRANSITION_INFO = {
+                    raw: {
+                        dimensions: RAW_DATA_DIMENSIONS
+                    },
+                    mTagSum: {
+                        dimensions: M_TAG_SUM_DIMENSIONS,
+                        uniqueKeyDimension: 'M_TAG',
+                    },
+                    rawClusters: {
+                        dimensions: RAW_CLUSTER_DIMENSIONS
+                    },
+                    rawClusterCenters: {
+                        dimensions: RAW_CLUSTER_CENTERS_DIMENSIONS,
+                        uniqueKeyDimension: 'CLUSTER_IDX'
                     }
-                    return {
-                        DIMENSION: DIMENSION,
-                        data: data
-                    };
-                }
-                function aggregateSum(rawDataWrap, byDimProp, RESULT_DIMENSION) {
-                    var map = {};
-                    var result = [];
-                    var data = rawDataWrap.data;
-
-                    for (var i = 0; i < data.length; i++) {
-                        var line = data[i];
-                        var byVal = line[rawDataWrap.DIMENSION[byDimProp]];
-                        if (!map.hasOwnProperty(byVal)) {
-                            var newLine = [];
-                            map[byVal] = newLine;
-                            result.push(newLine);
-                            newLine[RESULT_DIMENSION.ATA] = 0;
-                            newLine[RESULT_DIMENSION.STE] = 0;
-                            newLine[RESULT_DIMENSION.CTZ] = 0;
-                            newLine[RESULT_DIMENSION[byDimProp]] = byVal;
+                };
+
+                var baseOption = {
+                    dataset: [{
+                        id: 'raw',
+                        dimensions: RAW_DATA_DIMENSIONS,
+                        source: rawData
+                    }, {
+                        id: 'mTagSum',
+                        fromDatasetId: 'raw',
+                        transform: {
+                            type: 'my:aggregate',
+                            config: {
+                                resultDimensions: [
+                                    { from: 'ATA', method: 'sum' },
+                                    { from: 'STE', method: 'sum' },
+                                    { from: 'CTZ', method: 'sum' },
+                                    { from: 'M_TAG' }
+                                ],
+                                groupBy: 'M_TAG'
+                            }
                         }
-                        else {
-                            var newLine = map[byVal];
-                            newLine[RESULT_DIMENSION.ATA] += line[rawDataWrap.DIMENSION.ATA];
-                            newLine[RESULT_DIMENSION.STE] += line[rawDataWrap.DIMENSION.STE];
-                            newLine[RESULT_DIMENSION.CTZ] += line[rawDataWrap.DIMENSION.CTZ];
+                    }, {
+                        id: 'rawClusters',
+                        fromDatasetId: 'raw',
+                        transform: {
+                            type: 'ecStat:clustering',
+                            print: true,
+                            config: {
+                                clusterCount: 4,
+                                dimensions: ['ATA', 'STE'],
+                                outputClusterIndexDimension: {
+                                    index: RAW_CLUSTER_DIMENSIONS.indexOf('CLUSTER_IDX'),
+                                    name: 'CLUSTER_IDX'
+                                },
+                                outputCentroidDimensions: [{
+                                    index: RAW_CLUSTER_DIMENSIONS.indexOf('CLUSTER_CENTER_ATA'),
+                                    name: 'CLUSTER_CENTER_ATA'
+                                }, {
+                                    index: RAW_CLUSTER_DIMENSIONS.indexOf('CLUSTER_CENTER_STE'),
+                                    name: 'CLUSTER_CENTER_STE'
+                                }]
+                            }
                         }
-                    }
-
-                    return {
-                        DIMENSION: RESULT_DIMENSION,
-                        data: result,
-                        uniqueKey: 'M_TAG'
-                    };
-                }
-
-                var rawDataWrap = initRawData();
-                // console.log(JSON.stringify(rawDataWrap.data));
-                rawDataWrap.data = [[1425139200000,34,0.13,2,"MD","ZD","P0"],[1425225600000,28,0.71,1.5,"MB","ZD","P1"],[1425312000000,23,0.9,2.8,"MA","ZC","P2"],[1425398400000,21,0.58,6,"MB","ZC","P3"],[1425484800000,14,0.1,1.6,"MC","ZA","P4"],[1425571200000,21,0.6,7.7,"MC","ZA","P5"],[1425657600000,23,0.31,2.6,"MC","ZC","P6"],[1425744000000,34,0.74,2.4,"MD","ZE","P7"],[1425830400000,14,0.59,2.3,"MB","ZD","P8"],[1425916800000,18,0.85,5.1,"MB","ZB","P9"],[1426003200000,36,0.96,1.2,"MC"," [...]
-                var mTagSumDataWrap = aggregateSum(rawDataWrap, 'M_TAG', {
-                    ATA: 0,
-                    STE: 1,
-                    CTZ: 2,
-                    M_TAG: 3
-                });
-                var zTagSumDataWrap = aggregateSum(rawDataWrap, 'Z_TAG', {
-                    ATA: 0,
-                    STE: 1,
-                    CTZ: 2,
-                    Z_TAG: 3
-                });
+                    }, {
+                        id: 'rawClusterCenters',
+                        fromDatasetId: 'rawClusters',
+                        transform: {
+                            type: 'my:aggregate',
+                            print: true,
+                            config: {
+                                resultDimensions: [
+                                    { name: 'COUNT', from: 'ATA', method: 'count' },
+                                    { from: 'CLUSTER_CENTER_ATA' },
+                                    { from: 'CLUSTER_CENTER_STE' },
+                                    { from: 'CLUSTER_IDX' }
+                                ],
+                                groupBy: 'CLUSTER_IDX'
+                            }
+                        }
+                    }]
+                };
 
 
                 function create_Scatter_ATA_STE() {
+                    // var datasetId = 'raw';
+                    var datasetId = 'rawClusters';
                     var option = {
                         tooltip: {},
                         grid: {
@@ -157,26 +294,23 @@ under the License.
                             type: 'custom',
                             coordinateSystem: 'cartesian2d',
                             animationDurationUpdate: ANIMATION_DURATION_UPDATE,
-                            data: rawDataWrap.data,
+                            datasetId: datasetId,
                             encode: {
-                                itemName: rawDataWrap.DIMENSION.ID,
-                                x: rawDataWrap.DIMENSION.STE,
-                                y: rawDataWrap.DIMENSION.ATA,
-                                tooltip: [rawDataWrap.DIMENSION.STE, rawDataWrap.DIMENSION.ATA]
+                                itemName: 'ID',
+                                x: 'STE',
+                                y: 'ATA',
+                                tooltip: ['STE', 'ATA']
                             },
                             renderItem: function (params, api) {
                                 var pos = api.coord([
-                                    api.value(rawDataWrap.DIMENSION.STE),
-                                    api.value(rawDataWrap.DIMENSION.ATA)
+                                    api.value('STE'),
+                                    api.value('ATA')
                                 ]);
+                                // var clusterIndex = api.value('CLUSTER_IDX');
                                 return {
                                     type: 'circle',
-                                    // x: pos[0],
-                                    // y: pos[1],
                                     morph: true,
                                     shape: {
-                                        // cx: 0,
-                                        // cy: 0,
                                         cx: pos[0],
                                         cy: pos[1],
                                         r: 10,
@@ -185,6 +319,7 @@ under the License.
                                     style: {
                                         transition: 'lineWidth',
                                         fill: CONTENT_COLOR,
+                                        // fill: COLORS[clusterIndex],
                                         stroke: '#555',
                                         lineWidth: 1
                                     }
@@ -195,11 +330,12 @@ under the License.
 
                     return {
                         option: option,
-                        dataWrap: rawDataWrap
+                        datasetId: datasetId
                     };
                 }
 
-                function create_Bar_mSum_ATA(mTagSumDataWrap) {
+                function create_Bar_mSum_ATA() {
+                    var datasetId = 'mTagSum';
                     var option = {
                         tooltip: {},
                         grid: {
@@ -219,15 +355,15 @@ under the License.
                             type: 'custom',
                             coordinateSystem: 'cartesian2d',
                             animationDurationUpdate: ANIMATION_DURATION_UPDATE,
-                            data: mTagSumDataWrap.data,
+                            datasetId: datasetId,
                             encode: {
-                                x: mTagSumDataWrap.DIMENSION.M_TAG,
-                                y: mTagSumDataWrap.DIMENSION.ATA,
-                                tooltip: [mTagSumDataWrap.DIMENSION.M_TAG, mTagSumDataWrap.DIMENSION.ATA]
+                                x: 'M_TAG',
+                                y: 'ATA',
+                                tooltip: ['M_TAG', 'ATA']
                             },
                             renderItem: function (params, api) {
-                                var mTagVal = api.value(mTagSumDataWrap.DIMENSION.M_TAG);
-                                var ataVal = api.value(mTagSumDataWrap.DIMENSION.ATA);
+                                var mTagVal = api.value('M_TAG');
+                                var ataVal = api.value('ATA');
                                 var tarPos = api.coord([mTagVal, ataVal]);
                                 var zeroPos = api.coord([mTagVal, 0]);
                                 var size = api.size([mTagVal, ataVal]);
@@ -255,37 +391,45 @@ under the License.
 
                     return {
                         option: option,
-                        dataWrap: mTagSumDataWrap
+                        datasetId: datasetId
                     };
                 }
 
-                function create_Pie_mSum_ATA(mTagSumDataWrap) {
-                    var totalValue = mTagSumDataWrap.data.reduce(function (val, item) {
-                        return val + item[mTagSumDataWrap.DIMENSION.ATA];
-                    }, 0);
-                    let angles = [];
-                    let currentAngle = -Math.PI / 2;
-                    for (let i = 0; i < mTagSumDataWrap.data.length; i++) {
-                        const angle = mTagSumDataWrap.data[i][mTagSumDataWrap.DIMENSION.ATA] / totalValue * Math.PI * 2;
-                        angles.push([currentAngle, angle + currentAngle]);
-                        currentAngle += angle;
-                    }
 
+                function create_Pie_mSum_ATA() {
+                    var datasetId = 'mTagSum';
                     var option = {
                         tooltip: {},
                         series: {
                             type: 'custom',
                             coordinateSystem: 'none',
                             animationDurationUpdate: ANIMATION_DURATION_UPDATE,
-                            data: mTagSumDataWrap.data,
+                            datasetId: datasetId,
                             encode: {
-                                itemName: mTagSumDataWrap.DIMENSION.M_TAG,
-                                value: mTagSumDataWrap.DIMENSION.ATA,
-                                tooltip: [mTagSumDataWrap.DIMENSION.ATA]
+                                itemName: 'M_TAG',
+                                value: 'ATA',
+                                tooltip: 'ATA'
                             },
                             renderItem: function (params, api) {
-                                const width = chart.getWidth();
-                                const height = chart.getHeight();
+                                var context = params.context;
+                                if (!context.layout) {
+                                    context.layout = true;
+                                    var totalValue = 0;
+                                    for (var i = 0; i < params.dataInsideLength; i++) {
+                                        totalValue += api.value('ATA', i);
+                                    }
+                                    var angles = [];
+                                    var currentAngle = -Math.PI / 2;
+                                    for (var i = 0; i < params.dataInsideLength; i++) {
+                                        var angle = api.value('ATA', i) / totalValue * Math.PI * 2;
+                                        angles.push([currentAngle, angle + currentAngle]);
+                                        currentAngle += angle;
+                                    }
+                                    context.angles = angles;
+                                }
+
+                                var width = chart.getWidth();
+                                var height = chart.getHeight();
                                 return {
                                     type: 'sector',
                                     morph: true,
@@ -294,8 +438,8 @@ under the License.
                                         cy: height / 2,
                                         r: Math.min(width, height) / 3,
                                         r0: Math.min(width, height) / 5,
-                                        startAngle: angles[params.dataIndex][0],
-                                        endAngle: angles[params.dataIndex][1],
+                                        startAngle: context.angles[params.dataIndex][0],
+                                        endAngle: context.angles[params.dataIndex][1],
                                         clockwise: true
                                     },
                                     style: {
@@ -310,31 +454,111 @@ under the License.
 
                     return {
                         option: option,
-                        dataWrap: mTagSumDataWrap
+                        datasetId: datasetId
                     };
                 }
 
-                function createScatter_zSum_ATA(zTagSumDataWrap) {
-                }
+                function create_Scatter_ATA_STE_Cluster_Center() {
+                    var datasetId = 'rawClusterCenters';
+                    var option = {
+                        tooltip: {},
+                        grid: {
+                            containLabel: true
+                        },
+                        xAxis: {
+                            name: 'STE'
+                        },
+                        yAxis: {
+                            name: 'ATA'
+                        },
+                        dataZoom: [{
+                            type: 'slider',
+                        }, {
+                            type: 'inside'
+                        }],
+                        series: {
+                            type: 'custom',
+                            coordinateSystem: 'cartesian2d',
+                            animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                            datasetId: datasetId,
+                            encode: {
+                                x: 'CLUSTER_CENTER_STE',
+                                y: 'CLUSTER_CENTER_ATA',
+                                tooltip: ['CLUSTER_CENTER_STE', 'CLUSTER_CENTER_ATA']
+                            },
+                            renderItem: function (params, api) {
+                                var context = params.context;
+                                if (!context.layout) {
+                                    context.layout = true;
+                                    context.totalCount = 0;
+                                    for (var i = 0; i < params.dataInsideLength; i++) {
+                                        context.totalCount += api.value('COUNT', i);
+                                    }
+                                }
 
+                                var pos = api.coord([
+                                    api.value('CLUSTER_CENTER_STE'),
+                                    api.value('CLUSTER_CENTER_ATA')
+                                ]);
+                                var count = api.value('COUNT');
+                                var radius = count / context.totalCount * 100 + 10;
+                                return {
+                                    type: 'circle',
+                                    morph: true,
+                                    shape: {
+                                        cx: pos[0],
+                                        cy: pos[1],
+                                        r: radius,
+                                        transition: ['cx', 'cy', 'r']
+                                    },
+                                    style: {
+                                        transition: 'lineWidth',
+                                        fill: CONTENT_COLOR,
+                                        stroke: '#555',
+                                        lineWidth: 0
+                                    }
+                                };
+                            }
+                        }
+                    };
+
+                    return {
+                        option: option,
+                        datasetId: datasetId
+                    };
+                }
 
-                var currOptionName = 'Scatter_ATA_STE';
                 var optionInfoList = {
-                    'Scatter_ATA_STE': create_Scatter_ATA_STE(rawDataWrap),
-                    'Bar_mTagSum_ATA': create_Bar_mSum_ATA(mTagSumDataWrap),
-                    'Pie_mTagSum_ATA': create_Pie_mSum_ATA(mTagSumDataWrap),
+                    'Scatter_ATA_STE': create_Scatter_ATA_STE(),
+                    'Bar_mTagSum_ATA': create_Bar_mSum_ATA(),
+                    'Pie_mTagSum_ATA': create_Pie_mSum_ATA(),
+                    'Scatter_ATA_STE_Cluster_Center': create_Scatter_ATA_STE_Cluster_Center()
                 };
 
+                var currOptionName;
                 function next(nextOptionName) {
-                    const lastOptionInfo = optionInfoList[currOptionName];
-                    const nextOptionInfo = optionInfoList[nextOptionName];
+                    var lastOptionInfo = optionInfoList[currOptionName];
+                    var nextOptionInfo = optionInfoList[nextOptionName];
+                    var transitionOpt = {
+                        to: { seriesIndex: 0 }
+                    };
 
-                    const commonDimension = findCommonDimension(lastOptionInfo, nextOptionInfo)
-                        || findCommonDimension(nextOptionInfo, lastOptionInfo);
-                    const fromDimension = lastOptionInfo.dataWrap.DIMENSION[commonDimension];
-                    const toDimension = nextOptionInfo.dataWrap.DIMENSION[commonDimension];
-                    const transitionOpt = (fromDimension != null && toDimension != null)
-                        ? { from: fromDimension, to: toDimension } : null;
+                    if (lastOptionInfo) {
+                        var commonDimension = findCommonDimension(lastOptionInfo, nextOptionInfo)
+                            || findCommonDimension(nextOptionInfo, lastOptionInfo);
+                        if (commonDimension != null) {
+                            transitionOpt = {
+                                from: {
+                                    seriesIndex: 0,
+                                    dimension: commonDimension
+                                },
+                                to: {
+                                    seriesIndex: 0,
+                                    dimension: commonDimension
+                                }
+                            };
+                        }
+                    }
 
                     currOptionName = nextOptionName;
 
@@ -342,15 +566,17 @@ under the License.
                         replaceMerge: ['xAxis', 'yAxis'],
                         transition: transitionOpt
                     });
-                }
 
-                function findCommonDimension(optionInfoA, optionInfoB) {
-                    var uniqueKey = optionInfoB.dataWrap.uniqueKey;
-                    if (uniqueKey != null && optionInfoA.dataWrap.DIMENSION[uniqueKey] != null) {
-                        return uniqueKey;
+                    function findCommonDimension(optionInfoA, optionInfoB) {
+                        var metaA = TRANSITION_INFO[optionInfoA.datasetId];
+                        var metaB = TRANSITION_INFO[optionInfoB.datasetId];
+                        if (metaA.dimensions.indexOf(metaB.uniqueKeyDimension) >= 0) {
+                            return metaB.uniqueKeyDimension;
+                        }
                     }
                 }
 
+
                 var chart = testHelper.create(echarts, 'main0', {
                     title: [
                         'Test: buttons, should morph animation merge/split.',
@@ -358,27 +584,35 @@ under the License.
                         'Test: click buttons **twice**, should no blink.',
                         'Test: use dataZoom, update animation should exist'
                     ],
-                    option: optionInfoList[currOptionName].option,
+                    option: baseOption,
+                    lazyUpdate: true,
                     height: 600,
                     buttons: [{
-                        text: 'ToBar',
+                        text: 'Bar_mTagSum_ATA',
                         onclick: function () {
                             next('Bar_mTagSum_ATA');
                         }
                     }, {
-                        text: 'ToScatter',
+                        text: 'Scatter_ATA_STE',
                         onclick: function () {
                             next('Scatter_ATA_STE');
                         }
                     }, {
-                        text: 'ToPie',
+                        text: 'Pie_mTagSum_ATA',
                         onclick: function () {
                             next('Pie_mTagSum_ATA');
                         }
+                    }, {
+                        text: 'Scatter_ATA_STE_Cluster_Center',
+                        onclick: function () {
+                            next('Scatter_ATA_STE_Cluster_Center');
+                        }
                     }]
                 });
 
-            });
+                next('Scatter_ATA_STE');
+
+            })();
 
         </script>
 


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


[incubator-echarts] 04/05: enhance: [data transform] (1) Clarify the rule of when need to return the dimension. (2) Clarify the API name and behavior of data transform. (3) Try to freeze the raw data exposed to data transform.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit bc1f8a0133b9b49ae24fab713c29350a271ad44f
Author: 100pah <su...@gmail.com>
AuthorDate: Sun Sep 27 23:36:28 2020 +0800

    enhance: [data transform] (1) Clarify the rule of when need to return the dimension. (2) Clarify the API name and behavior of data transform. (3) Try to freeze the raw data exposed to data transform.
---
 src/chart/boxplot/boxplotTransform.ts      |  17 +-
 src/chart/custom.ts                        |   2 +-
 src/component/transform/filterTransform.ts |  16 +-
 src/component/transform/sortTransform.ts   |  46 +-
 src/data/List.ts                           |   2 +-
 src/data/Source.ts                         |  59 ++-
 src/data/helper/sourceHelper.ts            |  29 +-
 src/data/helper/sourceManager.ts           |  13 +-
 src/data/helper/transform.ts               | 177 ++++----
 src/echarts.ts                             |   1 -
 src/util/types.ts                          |   6 +-
 test/custom-shape-morphing2.html           | 682 ++++++++++++-----------------
 test/data-transform-ecStat.html            |  19 +-
 test/lib/config.js                         |   4 +-
 14 files changed, 497 insertions(+), 576 deletions(-)

diff --git a/src/chart/boxplot/boxplotTransform.ts b/src/chart/boxplot/boxplotTransform.ts
index bd92e7a..3084f81 100644
--- a/src/chart/boxplot/boxplotTransform.ts
+++ b/src/chart/boxplot/boxplotTransform.ts
@@ -19,8 +19,8 @@
 
 import { DataTransformOption, ExternalDataTransform } from '../../data/helper/transform';
 import prepareBoxplotData, { PrepareBoxplotDataOpt } from './prepareBoxplotData';
-import { isArray } from 'zrender/src/core/util';
 import { throwError, makePrintable } from '../../util/log';
+import { SOURCE_FORMAT_ARRAY_ROWS } from '../../util/types';
 
 
 export interface BoxplotTransformOption extends DataTransformOption {
@@ -33,28 +33,25 @@ export const boxplotTransform: ExternalDataTransform<BoxplotTransformOption> = {
     type: 'echarts:boxplot',
 
     transform: function transform(params) {
-        const source = params.source;
-        const sourceData = source.data;
-        if (
-            !isArray(sourceData)
-            || (sourceData[0] && !isArray(sourceData[0]))
-        ) {
+        const upstream = params.upstream;
+
+        if (upstream.sourceFormat !== SOURCE_FORMAT_ARRAY_ROWS) {
             let errMsg = '';
             if (__DEV__) {
                 errMsg = makePrintable(
-                    'source data is not applicable for this boxplot transform. Expect number[][].',
-                    'But actually', sourceData
+                    'source data is not applicable for this boxplot transform. Expect number[][].'
                 );
             }
             throwError(errMsg);
         }
 
         const result = prepareBoxplotData(
-            source.data as number[][],
+            upstream.getRawData() as number[][],
             params.config
         );
 
         return [{
+            dimensions: ['ItemName', 'Low', 'Q1', 'Q2', 'Q3', 'High'],
             data: result.boxData
         }, {
             data: result.outliers
diff --git a/src/chart/custom.ts b/src/chart/custom.ts
index fed727d..e37e1d7 100644
--- a/src/chart/custom.ts
+++ b/src/chart/custom.ts
@@ -19,7 +19,7 @@
 
 import {
     hasOwn, assert, isString, retrieve2, retrieve3, defaults, each,
-    keys, isArrayLike, bind, isFunction, eqNaN, noop
+    keys, isArrayLike, bind, isFunction, eqNaN
 } from 'zrender/src/core/util';
 import * as graphicUtil from '../util/graphic';
 import { setDefaultStateProxy, enableHoverEmphasis } from '../util/states';
diff --git a/src/component/transform/filterTransform.ts b/src/component/transform/filterTransform.ts
index 28da17b..cb47cc6 100644
--- a/src/component/transform/filterTransform.ts
+++ b/src/component/transform/filterTransform.ts
@@ -40,7 +40,7 @@ export const filterTransform: ExternalDataTransform<FilterTransformOption> = {
         // For example, if no condition specified by mistake, return an empty result
         // is better than return the entire raw soruce for user to find the mistake.
 
-        const source = params.source;
+        const upstream = params.upstream;
         let rawItem: OptionDataItem;
 
         const condition = parseConditionalExpression<{ dimIdx: DimensionIndex }>(params.config, {
@@ -60,12 +60,12 @@ export const filterTransform: ExternalDataTransform<FilterTransformOption> = {
                     throwError(errMsg);
                 }
 
-                const dimInfo = source.getDimensionInfo(dimLoose);
+                const dimInfo = upstream.getDimensionInfo(dimLoose);
                 if (!dimInfo) {
                     if (__DEV__) {
                         errMsg = makePrintable(
                             'Can not find dimension info via: ' + dimLoose + '.\n',
-                            'Existing dimensions: ', source.getDimensionInfoAll(), '.\n',
+                            'Existing dimensions: ', upstream.cloneAllDimensionInfo(), '.\n',
                             'Illegal condition:', exprOption, '.\n'
                         );
                     }
@@ -76,17 +76,13 @@ export const filterTransform: ExternalDataTransform<FilterTransformOption> = {
             },
 
             getValue: function (param) {
-                return source.retrieveItemValue(rawItem, param.dimIdx);
+                return upstream.retrieveValueFromItem(rawItem, param.dimIdx);
             }
         });
 
-        const sourceHeaderCount = source.sourceHeaderCount;
         const resultData = [];
-        for (let i = 0; i < sourceHeaderCount; i++) {
-            resultData.push(source.getRawHeaderItem(i));
-        }
-        for (let i = 0, len = source.count(); i < len; i++) {
-            rawItem = source.getRawDataItem(i);
+        for (let i = 0, len = upstream.count(); i < len; i++) {
+            rawItem = upstream.getRawDataItem(i);
             if (condition.evaluate()) {
                 resultData.push(rawItem);
             }
diff --git a/src/component/transform/sortTransform.ts b/src/component/transform/sortTransform.ts
index 3e77274..9d26944 100644
--- a/src/component/transform/sortTransform.ts
+++ b/src/component/transform/sortTransform.ts
@@ -19,10 +19,10 @@
 
 import { DataTransformOption, ExternalDataTransform } from '../../data/helper/transform';
 import {
-    DimensionLoose, SOURCE_FORMAT_KEYED_COLUMNS, DimensionIndex, OptionDataValue
+    DimensionLoose, DimensionIndex, OptionDataValue, SOURCE_FORMAT_ARRAY_ROWS, SOURCE_FORMAT_OBJECT_ROWS
 } from '../../util/types';
 import { makePrintable, throwError } from '../../util/log';
-import { isArray, each } from 'zrender/src/core/util';
+import { each } from 'zrender/src/core/util';
 import { normalizeToArray } from '../../util/model';
 import {
     RawValueParserType, getRawValueParser, SortOrderComparator
@@ -77,7 +77,7 @@ export const sortTransform: ExternalDataTransform<SortTransformOption> = {
     type: 'echarts:sort',
 
     transform: function transform(params) {
-        const source = params.source;
+        const upstream = params.upstream;
         const config = params.config;
         let errMsg = '';
 
@@ -134,12 +134,12 @@ export const sortTransform: ExternalDataTransform<SortTransformOption> = {
                 throwError(errMsg);
             }
 
-            const dimInfo = source.getDimensionInfo(dimLoose);
+            const dimInfo = upstream.getDimensionInfo(dimLoose);
             if (!dimInfo) {
                 if (__DEV__) {
                     errMsg = makePrintable(
                         'Can not find dimension info via: ' + dimLoose + '.\n',
-                        'Existing dimensions: ', source.getDimensionInfoAll(), '.\n',
+                        'Existing dimensions: ', upstream.cloneAllDimensionInfo(), '.\n',
                         'Illegal config:', orderExpr, '.\n'
                     );
                 }
@@ -165,39 +165,27 @@ export const sortTransform: ExternalDataTransform<SortTransformOption> = {
         });
 
         // TODO: support it?
-        if (!isArray(source.data)) {
+        const sourceFormat = upstream.sourceFormat;
+        if (sourceFormat !== SOURCE_FORMAT_ARRAY_ROWS
+            && sourceFormat !== SOURCE_FORMAT_OBJECT_ROWS
+        ) {
             if (__DEV__) {
-                errMsg = source.sourceFormat === SOURCE_FORMAT_KEYED_COLUMNS
-                    ? 'sourceFormat ' + SOURCE_FORMAT_KEYED_COLUMNS + ' is not supported yet'
-                    : source.data == null
-                    ? 'Upstream source data is null/undefined'
-                    : 'Unsupported source format.';
+                errMsg = 'sourceFormat "' + sourceFormat + '" is not supported yet';
             }
             throwError(errMsg);
         }
 
-        // Other source format are all array.
-        const sourceHeaderCount = source.sourceHeaderCount;
+        // Other upstream format are all array.
         const resultData = [];
-        const headerPlaceholder = {};
-        for (let i = 0; i < sourceHeaderCount; i++) {
-            resultData.push(headerPlaceholder);
-        }
-        for (let i = 0, len = source.count(); i < len; i++) {
-            resultData.push(source.getRawDataItem(i));
+        for (let i = 0, len = upstream.count(); i < len; i++) {
+            resultData.push(upstream.getRawDataItem(i));
         }
 
         resultData.sort(function (item0, item1) {
-            if (item0 === headerPlaceholder) {
-                return -1;
-            }
-            if (item1 === headerPlaceholder) {
-                return 1;
-            }
             for (let i = 0; i < orderDefList.length; i++) {
                 const orderDef = orderDefList[i];
-                let val0 = source.retrieveItemValue(item0, orderDef.dimIdx);
-                let val1 = source.retrieveItemValue(item1, orderDef.dimIdx);
+                let val0 = upstream.retrieveValueFromItem(item0, orderDef.dimIdx);
+                let val1 = upstream.retrieveValueFromItem(item1, orderDef.dimIdx);
                 if (orderDef.parser) {
                     val0 = orderDef.parser(val0) as OptionDataValue;
                     val1 = orderDef.parser(val1) as OptionDataValue;
@@ -210,10 +198,6 @@ export const sortTransform: ExternalDataTransform<SortTransformOption> = {
             return 0;
         });
 
-        for (let i = 0; i < sourceHeaderCount; i++) {
-            resultData[i] = source.getRawHeaderItem(i);
-        }
-
         return {
             data: resultData
         };
diff --git a/src/data/List.ts b/src/data/List.ts
index 34b2736..87f5977 100644
--- a/src/data/List.ts
+++ b/src/data/List.ts
@@ -1963,7 +1963,7 @@ class List<
                 // Performance sensitive, do not use modelUtil.getDataItemValue.
                 // If dataItem is an plain object with no value field, the let `value`
                 // will be assigned with the object, but it will be tread correctly
-                // in the `convertDataValue`.
+                // in the `convertValue`.
                 const value = dataItem && (dataItem.value == null ? dataItem : dataItem.value);
 
                 // If any dataItem is like { value: 10 }
diff --git a/src/data/Source.ts b/src/data/Source.ts
index b319403..c3a929f 100644
--- a/src/data/Source.ts
+++ b/src/data/Source.ts
@@ -19,7 +19,7 @@
 
 import {
     isTypedArray, HashMap, clone, createHashMap, isArray, isObject, isArrayLike,
-    hasOwn, assert, each, extend, map, isNumber, isString
+    hasOwn, assert, each, map, isNumber, isString, isFunction
 } from 'zrender/src/core/util';
 import {
     SourceFormat, SeriesLayoutBy, DimensionDefinition,
@@ -146,6 +146,8 @@ class SourceImpl {
      */
     readonly metaRawOption: SourceMetaRawOption;
 
+    readonly frozen: boolean;
+
 
     constructor(fields: {
         data: OptionSourceData,
@@ -180,6 +182,35 @@ class SourceImpl {
         this.metaRawOption = fields.metaRawOption;
     }
 
+    /**
+     * When expose the source to thrid-party transform, it probably better to
+     * freeze to make sure immutability.
+     * If a third-party transform modify the raw upstream data structure, it might bring about
+     * "uncertain effect" when using multiple transforms with different combinations.
+     *
+     * [Caveat]
+     * `OptionManager.ts` have perform `clone` in `setOption`.
+     * The original user input object should better not be frozen in case they
+     * make other usages.
+     */
+    freeze() {
+        assert(sourceFormatCanBeExposed(this));
+
+        const data = this.data as OptionSourceDataArrayRows;
+        if (this.frozen || !data || !isFunction(Object.freeze)) {
+            return;
+        }
+        // @ts-ignore
+        this.frozen = true;
+        // PENDING:
+        // There is a flaw that there might be non-primitive values like `Date`.
+        // Is it worth handling that?
+        for (let i = 0; i < data.length; i++) {
+            Object.freeze(data[i]);
+        }
+        Object.freeze(data);
+    }
+
 }
 
 export function isSourceInstance(val: unknown): val is Source {
@@ -194,10 +225,11 @@ export function createSource(
     encodeDefine: OptionEncode  // can be null
 ): Source {
     sourceFormat = sourceFormat || detectSourceFormat(sourceData);
+    const seriesLayoutBy = thisMetaRawOption.seriesLayoutBy;
     const determined = determineSourceDimensions(
         sourceData,
         sourceFormat,
-        thisMetaRawOption.seriesLayoutBy,
+        seriesLayoutBy,
         thisMetaRawOption.sourceHeader,
         thisMetaRawOption.dimensions
     );
@@ -205,7 +237,7 @@ export function createSource(
         data: sourceData,
         sourceFormat: sourceFormat,
 
-        seriesLayoutBy: thisMetaRawOption.seriesLayoutBy,
+        seriesLayoutBy: seriesLayoutBy,
         dimensionsDefine: determined.dimensionsDefine,
         startIndex: determined.startIndex,
         dimensionsDetectedCount: determined.dimensionsDetectedCount,
@@ -244,6 +276,15 @@ export function cloneSourceShallow(source: Source): Source {
     });
 }
 
+export function sourceFormatCanBeExposed(source: Source): boolean {
+    const sourceFormat = source.sourceFormat;
+    const data = source.data;
+    return sourceFormat === SOURCE_FORMAT_ARRAY_ROWS
+        || sourceFormat === SOURCE_FORMAT_OBJECT_ROWS
+        || !data
+        || (isArray(data) && !data.length);
+}
+
 function makeEncodeDefine(
     encodeDefine: OptionEncode | HashMap<OptionEncodeValue, DimensionName>
 ): HashMap<OptionEncodeValue, DimensionName> {
@@ -425,13 +466,19 @@ function normalizeDimensionsOption(dimensionsDefine: DimensionDefinitionLoose[])
         return;
     }
     const nameMap = createHashMap<{ count: number }, string>();
-    return map(dimensionsDefine, function (item, index) {
-        item = extend({}, isObject(item) ? item : {name: item});
+    return map(dimensionsDefine, function (rawItem, index) {
+        rawItem = isObject(rawItem) ? rawItem : { name: rawItem };
+        // Other fields will be discarded.
+        const item: DimensionDefinition = {
+            name: rawItem.name,
+            displayName: rawItem.displayName,
+            type: rawItem.type
+        };
 
         // User can set null in dimensions.
         // We dont auto specify name, othewise a given name may
         // cause it be refered unexpectedly.
-        if (item.name == null) {
+        if (name == null) {
             return item;
         }
 
diff --git a/src/data/helper/sourceHelper.ts b/src/data/helper/sourceHelper.ts
index 44b6f87..2406d0f 100644
--- a/src/data/helper/sourceHelper.ts
+++ b/src/data/helper/sourceHelper.ts
@@ -27,7 +27,7 @@ import {
     isObject,
     isTypedArray,
     HashMap,
-    defaults
+    retrieve2
 } from 'zrender/src/core/util';
 import { SourceMetaRawOption, Source } from '../Source';
 
@@ -83,11 +83,28 @@ export function resetSourceDefaulter(ecModel: GlobalModel): void {
 }
 
 // See [DIMENSION_INHERIT_RULE] in `sourceManager.ts`.
-export function inheritSourceMetaRawOption(opt: {
-    parent: SourceMetaRawOption, // Can be null/undefined
-    thisNew: SourceMetaRawOption // Must be object
-}) {
-    return defaults(opt.thisNew, opt.parent);
+export function inheritSourceMetaRawOption(
+    upstream: Source, // Can be null/undefined
+    newMetaRawOption: SourceMetaRawOption // Can NOT be null/undefined
+): SourceMetaRawOption {
+    const parentMetaRawOption = upstream ? upstream.metaRawOption : null;
+    const seriesLayoutBy = retrieve2(
+        newMetaRawOption.seriesLayoutBy,
+        parentMetaRawOption ? parentMetaRawOption.seriesLayoutBy : null
+    );
+    // sourceHeader and dimensions should use the "detected result" rather than "meta raw".
+    // Consider the case: transform return only "data" but no "dimensions", that should means inherit
+    // dimensions definition from upstream. But the returned data does not contain header line and can not
+    // be used as dimension-detection. In this case we should use "detected dimensions" of upstream directly.
+    const sourceHeader = retrieve2(
+        newMetaRawOption.sourceHeader,
+        upstream ? upstream.startIndex : null
+    );
+    const dimensions = retrieve2(
+        newMetaRawOption.dimensions,
+        upstream ? upstream.dimensionsDefine : null
+    );
+    return { seriesLayoutBy, sourceHeader, dimensions };
 }
 
 /**
diff --git a/src/data/helper/sourceManager.ts b/src/data/helper/sourceManager.ts
index 46d9ec2..1e88f7b 100644
--- a/src/data/helper/sourceManager.ts
+++ b/src/data/helper/sourceManager.ts
@@ -63,7 +63,6 @@ import { applyDataTransform } from './transform';
  * }, {
  *     transform: { type: 'filter', ... }
  * }]
- *
  * dataset: [{
  *     dimension: ['Product', 'Sales', 'Prise'],
  *     source: [ ['Cookies', 321, 44.21], ...]
@@ -213,10 +212,10 @@ export class SourceManager {
             }
 
             // See [REQUIREMENT_MEMO], merge settings on series and parent dataset if it is root.
-            const thisMetaRawOption = inheritSourceMetaRawOption({
-                parent: upSource ? upSource.metaRawOption : null,
-                thisNew: this._createSourceMetaRawOption()
-            });
+            const thisMetaRawOption = inheritSourceMetaRawOption(
+                upSource,
+                this._getSourceMetaRawOption()
+            );
 
             resultSourceList = [createSource(
                 data,
@@ -239,7 +238,7 @@ export class SourceManager {
                 const sourceData = datasetModel.get('source', true);
                 resultSourceList = [createSource(
                     sourceData,
-                    this._createSourceMetaRawOption(),
+                    this._getSourceMetaRawOption(),
                     null,
                     // Note: dataset option does not have `encode`.
                     null
@@ -361,7 +360,7 @@ export class SourceManager {
         }
     }
 
-    private _createSourceMetaRawOption(): SourceMetaRawOption {
+    private _getSourceMetaRawOption(): SourceMetaRawOption {
         const sourceHost = this._sourceHost;
         let seriesLayoutBy: SeriesLayoutBy;
         let sourceHeader: OptionSourceHeader;
diff --git a/src/data/helper/transform.ts b/src/data/helper/transform.ts
index bad0de4..5d16c8d 100644
--- a/src/data/helper/transform.ts
+++ b/src/data/helper/transform.ts
@@ -18,7 +18,7 @@
 */
 
 import {
-    Dictionary, OptionSourceData, DimensionDefinitionLoose, OptionSourceHeader,
+    Dictionary, OptionSourceData, DimensionDefinitionLoose,
     SourceFormat, DimensionDefinition, OptionDataItem, DimensionIndex,
     OptionDataValue, DimensionLoose, DimensionName, ParsedValue, SERIES_LAYOUT_BY_COLUMN
 } from '../../util/types';
@@ -33,7 +33,7 @@ import {
 import { parseDataValue } from './dataValueHelper';
 import { inheritSourceMetaRawOption } from './sourceHelper';
 import { consoleLog, makePrintable, throwError } from '../../util/log';
-import { createSource, Source } from '../Source';
+import { createSource, Source, sourceFormatCanBeExposed } from '../Source';
 
 
 export type PipedDataTransformOption = DataTransformOption[];
@@ -49,37 +49,34 @@ export interface DataTransformOption {
 
 export interface ExternalDataTransform<TO extends DataTransformOption = DataTransformOption> {
     // Must include namespace like: 'ecStat:regression'
-    type: string,
-    transform?: (
+    type: string;
+    transform: (
         param: ExternalDataTransformParam<TO>
-    ) => ExternalDataTransformResultItem | ExternalDataTransformResultItem[]
+    ) => ExternalDataTransformResultItem | ExternalDataTransformResultItem[];
 }
 
 interface ExternalDataTransformParam<TO extends DataTransformOption = DataTransformOption> {
-    // This is the first source in sourceList. In most cases,
+    // This is the first source in upstreamList. In most cases,
     // there is only one upstream source.
-    source: ExternalSource;
-    sourceList: ExternalSource[];
+    upstream: ExternalSource;
+    upstreamList: ExternalSource[];
     config: TO['config'];
 }
 export interface ExternalDataTransformResultItem {
+    /**
+     * If `data` is null/undefined, inherit upstream data.
+     */
     data: OptionSourceData;
     /**
      * A `transform` can optionally return a dimensions definition.
-     * If the `transform` make sure the dimensions of the result data, it can make that return.
-     * Otherwise, it's recommanded not to make such a `dimensions`. In this case, echarts will
-     * inherit dimensions definition from the upstream. If there is no dimensions definition
-     * of the upstream, the echarts will left it undefined.
-     * Notice: return a incorrect dimensions definition will cause the downstream can not use
-     * the values under that dimensions correctly.
-     *
-     * @see also `source.isDimensionsDefined`.
+     * The rule:
+     * If this `transform result` have different dimensions from the upstream, it should return
+     * a new dimension definition. For example, this transform inherit the upstream data totally
+     * but add a extra dimension.
+     * Otherwise, do not need to return that dimension definition. echarts will inherit dimension
+     * definition from the upstream.
      */
     dimensions?: DimensionDefinitionLoose[];
-    /**
-     * Similar to `dimensions`, a `transform` can return that optionally.
-     */
-    sourceHeader?: OptionSourceHeader;
 }
 interface ExternalDimensionDefinition extends Partial<DimensionDefinition> {
     // Mandatory
@@ -90,17 +87,26 @@ interface ExternalDimensionDefinition extends Partial<DimensionDefinition> {
  * TODO: disable writable.
  * This structure will be exposed to users.
  */
-class ExternalSource {
+export class ExternalSource {
     /**
      * [Caveat]
      * This instance is to be exposed to users.
-     * DO NOT mount private members on this instance directly.
+     * (1) DO NOT mount private members on this instance directly.
      * If we have to use private members, we can make them in closure or use `makeInner`.
+     * (2) "soruce header count" is not provided to transform, because it's complicated to manage
+     * header and dimensions definition in each transfrom. Source header are all normalized to
+     * dimensions definitions in transforms and their downstreams.
      */
 
-    data: OptionSourceData;
     sourceFormat: SourceFormat;
-    sourceHeaderCount: number;
+
+    getRawData(): Source['data'] {
+        return;
+    }
+
+    getRawDataItem(dataIndex: number): OptionDataItem {
+        return;
+    }
 
     /**
      * @return If dimension not found, return null/undefined.
@@ -110,30 +116,15 @@ class ExternalSource {
     }
 
     /**
-     * If dimensions are defined (see `isDimensionsDefined`), `dimensionInfoAll` is corresponding to
+     * dimensions defined if and only if either:
+     * (a) dataset.dimensions are declared.
+     * (b) dataset data include dimensions definitions in data (detected or via specified `sourceHeader`).
+     * If dimensions are defined, `dimensionInfoAll` is corresponding to
      * the defined dimensions.
      * Otherwise, `dimensionInfoAll` is determined by data columns.
      * @return Always return an array (even empty array).
      */
-    getDimensionInfoAll(): ExternalDimensionDefinition[] {
-        return;
-    }
-
-    /**
-     * dimensions defined if and only if:
-     * (a) dataset.dimensions are declared.
-     * or
-     * (b) dataset data include dimensions definitions in data (detected or via specified `sourceHeader`)
-     */
-    isDimensionsDefined(): boolean {
-        return;
-    }
-
-    getRawDataItem(dataIndex: number): OptionDataItem {
-        return;
-    }
-
-    getRawHeaderItem(dataIndex: number): OptionDataItem {
+    cloneAllDimensionInfo(): ExternalDimensionDefinition[] {
         return;
     }
 
@@ -146,21 +137,26 @@ class ExternalSource {
      * No need to support by dimension name in transform function,
      * becuase transform function is not case-specific, no need to use name literally.
      */
-    retrieveItemValue(rawItem: OptionDataItem, dimIndex: DimensionIndex): OptionDataValue {
+    retrieveValue(dataIndex: number, dimIndex: DimensionIndex): OptionDataValue {
+        return;
+    }
+
+    retrieveValueFromItem(dataItem: OptionDataItem, dimIndex: DimensionIndex): OptionDataValue {
         return;
     }
 
-    convertDataValue(rawVal: unknown, dimInfo: ExternalDimensionDefinition): ParsedValue {
+    convertValue(rawVal: unknown, dimInfo: ExternalDimensionDefinition): ParsedValue {
         return parseDataValue(rawVal, dimInfo);
     }
 }
 
-function createExternalSource(internalSource: Source): ExternalSource {
+
+function createExternalSource(internalSource: Source, externalTransform: ExternalDataTransform): ExternalSource {
     const extSource = new ExternalSource();
 
-    const data = extSource.data = internalSource.data;
+    const data = internalSource.data;
     const sourceFormat = extSource.sourceFormat = internalSource.sourceFormat;
-    const sourceHeaderCount = extSource.sourceHeaderCount = internalSource.startIndex;
+    const sourceHeaderCount = internalSource.startIndex;
 
     // [MEMO]
     // Create a new dimensions structure for exposing.
@@ -210,35 +206,52 @@ function createExternalSource(internalSource: Source): ExternalSource {
 
     // Implement public methods:
     const rawItemGetter = getRawSourceItemGetter(sourceFormat, SERIES_LAYOUT_BY_COLUMN);
-    extSource.getRawDataItem = bind(rawItemGetter, null, data, sourceHeaderCount, dimensions);
-    extSource.getRawHeaderItem = function (dataIndex: number) {
-        if (dataIndex < sourceHeaderCount) {
-            return rawItemGetter(data, 0, dimensions, dataIndex);
-        }
+    extSource.getRawDataItem = function (dataIndex) {
+        !internalSource.frozen && (internalSource.freeze());
+        return rawItemGetter(data, sourceHeaderCount, dimensions, dataIndex);
     };
 
+    extSource.getRawData = bind(getRawData, null, internalSource);
+
     const rawCounter = getRawSourceDataCounter(sourceFormat, SERIES_LAYOUT_BY_COLUMN);
     extSource.count = bind(rawCounter, null, data, sourceHeaderCount, dimensions);
 
     const rawValueGetter = getRawSourceValueGetter(sourceFormat);
-    extSource.retrieveItemValue = function (rawItem, dimIndex) {
-        if (rawItem == null) {
+    extSource.retrieveValue = function (dataIndex, dimIndex) {
+        const rawItem = rawItemGetter(data, sourceHeaderCount, dimensions, dataIndex);
+        return retrieveValueFromItem(rawItem, dimIndex);
+    };
+    const retrieveValueFromItem = extSource.retrieveValueFromItem = function (dataItem, dimIndex) {
+        if (dataItem == null) {
             return;
         }
         const dimDef = dimensions[dimIndex];
         // When `dimIndex` is `null`, `rawValueGetter` return the whole item.
         if (dimDef) {
-            return rawValueGetter(rawItem, dimIndex, dimDef.name) as OptionDataValue;
+            return rawValueGetter(dataItem, dimIndex, dimDef.name) as OptionDataValue;
         }
     };
 
     extSource.getDimensionInfo = bind(getDimensionInfo, null, dimensions, dimsByName);
-    extSource.getDimensionInfoAll = bind(getDimensionInfoAll, null, dimensions);
-    extSource.isDimensionsDefined = bind(isDimensionsDefined, null, !!dimsDef);
+    extSource.cloneAllDimensionInfo = bind(cloneAllDimensionInfo, null, dimensions);
 
     return extSource;
 }
 
+function getRawData(upstream: Source): Source['data'] {
+    let errMsg = '';
+    const sourceFormat = upstream.sourceFormat;
+    const upstreamData = upstream.data;
+    if (!sourceFormatCanBeExposed(upstream)) {
+        if (__DEV__) {
+            errMsg = '`getRawData` is not supported in source format ' + sourceFormat;
+        }
+        throwError(errMsg);
+    }
+
+    upstream.freeze();
+    return upstreamData;
+}
 
 function getDimensionInfo(
     dimensions: ExternalDimensionDefinition[],
@@ -260,15 +273,10 @@ function getDimensionInfo(
     }
 }
 
-function getDimensionInfoAll(dimensions: ExternalDimensionDefinition[]): ExternalDimensionDefinition[] {
-    return dimensions;
+function cloneAllDimensionInfo(dimensions: ExternalDimensionDefinition[]): ExternalDimensionDefinition[] {
+    return clone(dimensions);
 }
 
-function isDimensionsDefined(defined: boolean): boolean {
-    return defined;
-}
-
-
 
 const externalTransformMap = createHashMap<ExternalDataTransform, string>();
 
@@ -360,12 +368,12 @@ function applySingleDataTransform(
     }
 
     // Prepare source
-    const sourceList = map(upSourceList, createExternalSource);
+    const extUpSourceList = map(upSourceList, upSource => createExternalSource(upSource, externalTransform));
 
     const resultList = normalizeToArray(
         externalTransform.transform({
-            source: sourceList[0],
-            sourceList: sourceList,
+            upstream: extUpSourceList[0],
+            upstreamList: extUpSourceList,
             config: clone(transOption.config)
         })
     );
@@ -379,9 +387,7 @@ function applySingleDataTransform(
                     '- transform result data:',
                     makePrintable(extSource.data),
                     '- transform result dimensions:',
-                    makePrintable(extSource.dimensions),
-                    '- transform result sourceHeader: ',
-                    makePrintable(extSource.sourceHeader)
+                    makePrintable(extSource.dimensions)
                 ].join('\n');
             }).join('\n');
             consoleLog(printStrArr);
@@ -396,24 +402,31 @@ function applySingleDataTransform(
             }
             throwError(errMsg);
         }
-        if (!isObject(result.data) && !isArrayLike(result.data)) {
-            if (__DEV__) {
-                errMsg = 'Result data should be object or array in data transform.';
+        let resultData = result.data;
+        if (resultData != null) {
+            if (!isObject(resultData) && !isArrayLike(resultData)) {
+                if (__DEV__) {
+                    errMsg = 'Result data should be object or array in data transform.';
+                }
+                throwError(errMsg);
             }
-            throwError(errMsg);
+        }
+        else {
+            // Inherit from upstream[0]
+            resultData = upSourceList[0].data;
         }
 
-        const resultMetaRawOption = inheritSourceMetaRawOption({
-            parent: upSourceList[0].metaRawOption,
-            thisNew: {
+        const resultMetaRawOption = inheritSourceMetaRawOption(
+            upSourceList[0],
+            {
                 seriesLayoutBy: SERIES_LAYOUT_BY_COLUMN,
-                sourceHeader: result.sourceHeader,
+                sourceHeader: 0,
                 dimensions: result.dimensions
             }
-        });
+        );
 
         return createSource(
-            result.data,
+            resultData,
             resultMetaRawOption,
             null,
             null
diff --git a/src/echarts.ts b/src/echarts.ts
index 9930fe1..95d8764 100644
--- a/src/echarts.ts
+++ b/src/echarts.ts
@@ -2305,7 +2305,6 @@ class ECharts extends Eventful {
                 }
 
                 const finderOpt = {
-                    useDefaultMainType: ['series'],
                     includeMainTypes: ['series'],
                     enableAll: false,
                     enableNone: false
diff --git a/src/util/types.ts b/src/util/types.ts
index dbe4d21..330dbc3 100644
--- a/src/util/types.ts
+++ b/src/util/types.ts
@@ -325,7 +325,7 @@ export type TooltipOrderMode = 'valueAsc' | 'valueDesc' | 'seriesAsc' | 'seriesD
 // `Date` will be parsed to timestamp.
 // Ordinal/category data will be parsed to its index if possible, otherwise
 // keep its original string in list._storage.
-// Check `convertDataValue` for more details.
+// Check `convertValue` for more details.
 export type OrdinalRawValue = string | number;
 export type OrdinalNumber = number; // The number mapped from each OrdinalRawValue.
 export type OrdinalSortInfo = {
@@ -531,9 +531,9 @@ export type OptionSourceDataOriginal<
     ORIITEM extends OptionDataItemOriginal<VAL> = OptionDataItemOriginal<VAL>
 > = ArrayLike<ORIITEM>;
 export type OptionSourceDataObjectRows<VAL extends OptionDataValue = OptionDataValue> =
-    ArrayLike<Dictionary<VAL>>;
+    Array<Dictionary<VAL>>;
 export type OptionSourceDataArrayRows<VAL extends OptionDataValue = OptionDataValue> =
-    ArrayLike<ArrayLike<VAL>>;
+    Array<Array<VAL>>;
 export type OptionSourceDataKeyedColumns<VAL extends OptionDataValue = OptionDataValue> =
     Dictionary<ArrayLike<VAL>>;
 export type OptionSourceDataTypedArray = ArrayLike<number>;
diff --git a/test/custom-shape-morphing2.html b/test/custom-shape-morphing2.html
index e5a8cb7..6d5b16d 100644
--- a/test/custom-shape-morphing2.html
+++ b/test/custom-shape-morphing2.html
@@ -22,11 +22,13 @@ under the License.
     <head>
         <meta charset='utf-8'>
         <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <script src="lib/esl.js"></script>
+        <script src="lib/config.js"></script>
         <script src='lib/jquery.min.js'></script>
         <script src="../dist/echarts.js"></script>
         <script src="lib/testHelper.js"></script>
-        <!-- <script src="lib/ecStat.min.js"></script> -->
-        <script src="../../echarts-stat/dist/ecStat.js"></script>
+        <script src="lib/myTransform/aggregate.js"></script>
+        <script src="lib/transitionPlayer.js"></script>
         <link rel="stylesheet" href="lib/reset.css" />
     </head>
     <body>
@@ -39,101 +41,6 @@ under the License.
 
         <script>
 
-            /**
-             * @usage
-             *
-             * dataset: [{
-             *     source: [
-             *         ['aa', 'bb', 'cc', 'tag'],
-             *         [12, 0.33, 5200, 'AA'],
-             *         [21, 0.65, 8100, 'AA'],
-             *         ...
-             *     ]
-             * }, {
-             *     transform: {
-             *         type: 'my:aggregate',
-             *         config: {
-             *             resultDimensions: [
-             *                 // by default, use the same name with `from`.
-             *                 { from: 'aa', method: 'sum' },
-             *                 { from: 'bb', method: 'count' },
-             *                 { from: 'cc' }, // method by default: use the first value.
-             *                 { from: 'tag' }
-             *             ],
-             *             groupBy: 'tag'
-             *         }
-             *     }
-             * }]
-             */
-            var sumTransform = {
-                type: 'my:aggregate',
-                transform: function (params) {
-                    var source = params.source;
-                    var config = params.config;
-                    var resultDimensionsConfig = config.resultDimensions;
-                    var groupBy = config.groupBy;
-
-                    var calcMap = {};
-                    var resultData = [];
-
-                    var groupByDimInfo = source.getDimensionInfo(groupBy);
-                    var resultDimInfoList = [];
-                    var resultDimensions = [];
-                    for (var i = 0; i < resultDimensionsConfig.length; i++) {
-                        var resultDimInfoConfig = resultDimensionsConfig[i];
-                        var resultDimInfo = source.getDimensionInfo(resultDimInfoConfig.from);
-                        resultDimInfo.method = resultDimInfoConfig.method;
-                        resultDimInfoList.push(resultDimInfo);
-                        if (resultDimInfoConfig.name != null) {
-                            resultDimInfo.name = resultDimInfoConfig.name;
-                        }
-                        resultDimensions.push(resultDimInfo.name);
-                    }
-
-                    for (var i = 0; i < source.count(); i++) {
-                        var line = source.getRawDataItem(i);
-                        var groupByVal = source.retrieveItemValue(line, groupByDimInfo.index);
-
-                        if (!calcMap.hasOwnProperty(groupByVal)) {
-                            var newLine = [];
-                            calcMap[groupByVal] = newLine;
-                            resultData.push(newLine);
-                            for (var j = 0; j < resultDimInfoList.length; j++) {
-                                var resultDimInfo = resultDimInfoList[j];
-                                var method = resultDimInfo.method;
-                                newLine[j] = resultDimInfo.index === groupByDimInfo.index
-                                    ? groupByVal
-                                    : (method === 'sum' || method === 'count')
-                                    ? 0
-                                    // By default, method: 'first'
-                                    : source.retrieveItemValue(line, resultDimInfo.index);
-                            }
-                        }
-                        else {
-                            var newLine = calcMap[groupByVal];
-                            for (var j = 0; j < resultDimInfoList.length; j++) {
-                                var resultDimInfo = resultDimInfoList[j];
-                                var method = resultDimInfo.method;
-                                if (resultDimInfo.index !== groupByDimInfo.index) {
-                                    if (method === 'sum') {
-                                        newLine[j] += line[resultDimInfo.index];
-                                    }
-                                    else if (method === 'count') {
-                                        newLine[j] += 1;
-                                    }
-                                }
-                            }
-                        }
-                    }
-
-                    return {
-                        dimensions: resultDimensions,
-                        data: resultData
-                    };
-                }
-            };
-
-
             function initRawData(count) {
                 var M_TAG_LIST = ['MA', 'MB', 'MC', 'MD'];
                 var Z_TAG_LIST = ['ZA', 'ZB', 'ZC', 'ZD', 'ZE'];
@@ -168,9 +75,10 @@ under the License.
 
 
         <script>
-            (function () {
 
-                echarts.registerTransform(sumTransform);
+            require(['echarts', 'ecStat'], function (echarts, ecStat) {
+
+                echarts.registerTransform(window.myTransform.aggregate);
                 echarts.registerTransform(ecStat.transform.clustering);
 
 
@@ -191,23 +99,6 @@ under the License.
                 var RAW_CLUSTER_DIMENSIONS = ['DATE', 'ATA', 'STE', 'CTZ', 'M_TAG', 'Z_TAG', 'ID', 'CLUSTER_IDX', 'CLUSTER_CENTER_ATA', 'CLUSTER_CENTER_STE'];
                 var RAW_CLUSTER_CENTERS_DIMENSIONS = ['COUNT', 'CLUSTER_IDX', 'CLUSTER_CENTER_ATA', 'CLUSTER_CENTER_STE'];
 
-                // Key: datasetId
-                var TRANSITION_INFO = {
-                    raw: {
-                        dimensions: RAW_DATA_DIMENSIONS
-                    },
-                    mTagSum: {
-                        dimensions: M_TAG_SUM_DIMENSIONS,
-                        uniqueKeyDimension: 'M_TAG',
-                    },
-                    rawClusters: {
-                        dimensions: RAW_CLUSTER_DIMENSIONS
-                    },
-                    rawClusterCenters: {
-                        dimensions: RAW_CLUSTER_CENTERS_DIMENSIONS,
-                        uniqueKeyDimension: 'CLUSTER_IDX'
-                    }
-                };
 
                 var baseOption = {
                     dataset: [{
@@ -218,7 +109,7 @@ under the License.
                         id: 'mTagSum',
                         fromDatasetId: 'raw',
                         transform: {
-                            type: 'my:aggregate',
+                            type: 'myTransform:aggregate',
                             config: {
                                 resultDimensions: [
                                     { from: 'ATA', method: 'sum' },
@@ -255,7 +146,7 @@ under the License.
                         id: 'rawClusterCenters',
                         fromDatasetId: 'rawClusters',
                         transform: {
-                            type: 'my:aggregate',
+                            type: 'myTransform:aggregate',
                             print: true,
                             config: {
                                 resultDimensions: [
@@ -267,314 +158,283 @@ under the License.
                                 groupBy: 'CLUSTER_IDX'
                             }
                         }
-                    }]
+                    }],
+                    tooltip: {}
                 };
 
+                var optionCreators = {
 
-                function create_Scatter_ATA_STE() {
-                    // var datasetId = 'raw';
-                    var datasetId = 'rawClusters';
-                    var option = {
-                        tooltip: {},
-                        grid: {
-                            containLabel: true
-                        },
-                        xAxis: {
-                            name: 'STE'
-                        },
-                        yAxis: {
-                            name: 'ATA'
-                        },
-                        dataZoom: [{
-                            type: 'slider',
-                        }, {
-                            type: 'inside'
-                        }],
-                        series: {
-                            type: 'custom',
-                            coordinateSystem: 'cartesian2d',
-                            animationDurationUpdate: ANIMATION_DURATION_UPDATE,
-                            datasetId: datasetId,
-                            encode: {
-                                itemName: 'ID',
-                                x: 'STE',
-                                y: 'ATA',
-                                tooltip: ['STE', 'ATA']
+                    'Scatter_ATA_STE': function (datasetId) {
+                        return {
+                            grid: {
+                                containLabel: true
                             },
-                            renderItem: function (params, api) {
-                                var pos = api.coord([
-                                    api.value('STE'),
-                                    api.value('ATA')
-                                ]);
-                                // var clusterIndex = api.value('CLUSTER_IDX');
-                                return {
-                                    type: 'circle',
-                                    morph: true,
-                                    shape: {
-                                        cx: pos[0],
-                                        cy: pos[1],
-                                        r: 10,
-                                        transition: ['cx', 'cy', 'r']
-                                    },
-                                    style: {
-                                        transition: 'lineWidth',
-                                        fill: CONTENT_COLOR,
-                                        // fill: COLORS[clusterIndex],
-                                        stroke: '#555',
-                                        lineWidth: 1
-                                    }
-                                };
-                            }
-                        }
-                    };
-
-                    return {
-                        option: option,
-                        datasetId: datasetId
-                    };
-                }
-
-                function create_Bar_mSum_ATA() {
-                    var datasetId = 'mTagSum';
-                    var option = {
-                        tooltip: {},
-                        grid: {
-                            containLabel: true
-                        },
-                        xAxis: {
-                            type: 'category'
-                        },
-                        yAxis: {
-                        },
-                        dataZoom: [{
-                            type: 'slider',
-                        }, {
-                            type: 'inside'
-                        }],
-                        series: {
-                            type: 'custom',
-                            coordinateSystem: 'cartesian2d',
-                            animationDurationUpdate: ANIMATION_DURATION_UPDATE,
-                            datasetId: datasetId,
-                            encode: {
-                                x: 'M_TAG',
-                                y: 'ATA',
-                                tooltip: ['M_TAG', 'ATA']
+                            xAxis: {
+                                name: 'STE'
                             },
-                            renderItem: function (params, api) {
-                                var mTagVal = api.value('M_TAG');
-                                var ataVal = api.value('ATA');
-                                var tarPos = api.coord([mTagVal, ataVal]);
-                                var zeroPos = api.coord([mTagVal, 0]);
-                                var size = api.size([mTagVal, ataVal]);
-                                var width = size[0] * 0.4;
-                                return {
-                                    type: 'rect',
-                                    morph: true,
-                                    shape: {
-                                        x: tarPos[0] - width / 2,
-                                        y: tarPos[1],
-                                        height: zeroPos[1] - tarPos[1],
-                                        width: width,
-                                        transition: ['x', 'y', 'width', 'height']
-                                    },
-                                    style: {
-                                        transition: 'lineWidth',
-                                        fill: CONTENT_COLOR,
-                                        stroke: '#555',
-                                        lineWidth: 0
-                                    }
-                                };
-                            }
-                        }
-                    };
-
-                    return {
-                        option: option,
-                        datasetId: datasetId
-                    };
-                }
-
-
-                function create_Pie_mSum_ATA() {
-                    var datasetId = 'mTagSum';
-                    var option = {
-                        tooltip: {},
-                        series: {
-                            type: 'custom',
-                            coordinateSystem: 'none',
-                            animationDurationUpdate: ANIMATION_DURATION_UPDATE,
-                            datasetId: datasetId,
-                            encode: {
-                                itemName: 'M_TAG',
-                                value: 'ATA',
-                                tooltip: 'ATA'
+                            yAxis: {
+                                name: 'ATA'
                             },
-                            renderItem: function (params, api) {
-                                var context = params.context;
-                                if (!context.layout) {
-                                    context.layout = true;
-                                    var totalValue = 0;
-                                    for (var i = 0; i < params.dataInsideLength; i++) {
-                                        totalValue += api.value('ATA', i);
-                                    }
-                                    var angles = [];
-                                    var currentAngle = -Math.PI / 2;
-                                    for (var i = 0; i < params.dataInsideLength; i++) {
-                                        var angle = api.value('ATA', i) / totalValue * Math.PI * 2;
-                                        angles.push([currentAngle, angle + currentAngle]);
-                                        currentAngle += angle;
-                                    }
-                                    context.angles = angles;
+                            dataZoom: [{
+                                type: 'slider',
+                            }, {
+                                type: 'inside'
+                            }],
+                            series: {
+                                type: 'custom',
+                                coordinateSystem: 'cartesian2d',
+                                animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                                datasetId: datasetId,
+                                encode: {
+                                    itemName: 'ID',
+                                    x: 'STE',
+                                    y: 'ATA',
+                                    tooltip: ['STE', 'ATA']
+                                },
+                                renderItem: function (params, api) {
+                                    var pos = api.coord([
+                                        api.value('STE'),
+                                        api.value('ATA')
+                                    ]);
+                                    // var clusterIndex = api.value('CLUSTER_IDX');
+                                    return {
+                                        type: 'circle',
+                                        morph: true,
+                                        shape: {
+                                            cx: pos[0],
+                                            cy: pos[1],
+                                            r: 10,
+                                            transition: ['cx', 'cy', 'r']
+                                        },
+                                        style: {
+                                            transition: 'lineWidth',
+                                            fill: CONTENT_COLOR,
+                                            // fill: COLORS[clusterIndex],
+                                            stroke: '#555',
+                                            lineWidth: 1
+                                        }
+                                    };
                                 }
-
-                                var width = chart.getWidth();
-                                var height = chart.getHeight();
-                                return {
-                                    type: 'sector',
-                                    morph: true,
-                                    shape: {
-                                        cx: width / 2,
-                                        cy: height / 2,
-                                        r: Math.min(width, height) / 3,
-                                        r0: Math.min(width, height) / 5,
-                                        startAngle: context.angles[params.dataIndex][0],
-                                        endAngle: context.angles[params.dataIndex][1],
-                                        clockwise: true
-                                    },
-                                    style: {
-                                        fill: CONTENT_COLOR,
-                                        stroke: '#555',
-                                        strokeNoScale: true
-                                    },
-                                };
                             }
-                        }
-                    };
-
-                    return {
-                        option: option,
-                        datasetId: datasetId
-                    };
-                }
+                        };
+                    },
 
-                function create_Scatter_ATA_STE_Cluster_Center() {
-                    var datasetId = 'rawClusterCenters';
-                    var option = {
-                        tooltip: {},
-                        grid: {
-                            containLabel: true
-                        },
-                        xAxis: {
-                            name: 'STE'
-                        },
-                        yAxis: {
-                            name: 'ATA'
-                        },
-                        dataZoom: [{
-                            type: 'slider',
-                        }, {
-                            type: 'inside'
-                        }],
-                        series: {
-                            type: 'custom',
-                            coordinateSystem: 'cartesian2d',
-                            animationDurationUpdate: ANIMATION_DURATION_UPDATE,
-                            datasetId: datasetId,
-                            encode: {
-                                x: 'CLUSTER_CENTER_STE',
-                                y: 'CLUSTER_CENTER_ATA',
-                                tooltip: ['CLUSTER_CENTER_STE', 'CLUSTER_CENTER_ATA']
+                    'Bar_mSum_ATA': function (datasetId) {
+                        return {
+                            grid: {
+                                containLabel: true
                             },
-                            renderItem: function (params, api) {
-                                var context = params.context;
-                                if (!context.layout) {
-                                    context.layout = true;
-                                    context.totalCount = 0;
-                                    for (var i = 0; i < params.dataInsideLength; i++) {
-                                        context.totalCount += api.value('COUNT', i);
-                                    }
+                            xAxis: {
+                                type: 'category'
+                            },
+                            yAxis: {
+                            },
+                            dataZoom: [{
+                                type: 'slider',
+                            }, {
+                                type: 'inside'
+                            }],
+                            series: {
+                                type: 'custom',
+                                coordinateSystem: 'cartesian2d',
+                                animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                                datasetId: datasetId,
+                                encode: {
+                                    x: 'M_TAG',
+                                    y: 'ATA',
+                                    tooltip: ['M_TAG', 'ATA']
+                                },
+                                renderItem: function (params, api) {
+                                    var mTagVal = api.value('M_TAG');
+                                    var ataVal = api.value('ATA');
+                                    var tarPos = api.coord([mTagVal, ataVal]);
+                                    var zeroPos = api.coord([mTagVal, 0]);
+                                    var size = api.size([mTagVal, ataVal]);
+                                    var width = size[0] * 0.4;
+                                    return {
+                                        type: 'rect',
+                                        morph: true,
+                                        shape: {
+                                            x: tarPos[0] - width / 2,
+                                            y: tarPos[1],
+                                            height: zeroPos[1] - tarPos[1],
+                                            width: width,
+                                            transition: ['x', 'y', 'width', 'height']
+                                        },
+                                        style: {
+                                            transition: 'lineWidth',
+                                            fill: CONTENT_COLOR,
+                                            stroke: '#555',
+                                            lineWidth: 0
+                                        }
+                                    };
                                 }
-
-                                var pos = api.coord([
-                                    api.value('CLUSTER_CENTER_STE'),
-                                    api.value('CLUSTER_CENTER_ATA')
-                                ]);
-                                var count = api.value('COUNT');
-                                var radius = count / context.totalCount * 100 + 10;
-                                return {
-                                    type: 'circle',
-                                    morph: true,
-                                    shape: {
-                                        cx: pos[0],
-                                        cy: pos[1],
-                                        r: radius,
-                                        transition: ['cx', 'cy', 'r']
-                                    },
-                                    style: {
-                                        transition: 'lineWidth',
-                                        fill: CONTENT_COLOR,
-                                        stroke: '#555',
-                                        lineWidth: 0
-                                    }
-                                };
                             }
-                        }
-                    };
+                        };
+                    },
 
-                    return {
-                        option: option,
-                        datasetId: datasetId
-                    };
-                }
+                    'Pie_mSum_ATA': function (datasetId) {
+                        return {
+                            series: {
+                                type: 'custom',
+                                coordinateSystem: 'none',
+                                animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                                datasetId: datasetId,
+                                encode: {
+                                    itemName: 'M_TAG',
+                                    value: 'ATA',
+                                    tooltip: 'ATA'
+                                },
+                                renderItem: function (params, api) {
+                                    var context = params.context;
+                                    if (!context.layout) {
+                                        context.layout = true;
+                                        var totalValue = 0;
+                                        for (var i = 0; i < params.dataInsideLength; i++) {
+                                            totalValue += api.value('ATA', i);
+                                        }
+                                        var angles = [];
+                                        var currentAngle = -Math.PI / 2;
+                                        for (var i = 0; i < params.dataInsideLength; i++) {
+                                            var angle = api.value('ATA', i) / totalValue * Math.PI * 2;
+                                            angles.push([currentAngle, angle + currentAngle - 0.01]);
+                                            currentAngle += angle;
+                                        }
+                                        context.angles = angles;
+                                    }
 
-                var optionInfoList = {
-                    'Scatter_ATA_STE': create_Scatter_ATA_STE(),
-                    'Bar_mTagSum_ATA': create_Bar_mSum_ATA(),
-                    'Pie_mTagSum_ATA': create_Pie_mSum_ATA(),
-                    'Scatter_ATA_STE_Cluster_Center': create_Scatter_ATA_STE_Cluster_Center()
-                };
+                                    var width = chart.getWidth();
+                                    var height = chart.getHeight();
+                                    return {
+                                        type: 'sector',
+                                        morph: true,
+                                        shape: {
+                                            cx: width / 2,
+                                            cy: height / 2,
+                                            r: Math.min(width, height) / 3,
+                                            r0: Math.min(width, height) / 5,
+                                            startAngle: context.angles[params.dataIndex][0],
+                                            endAngle: context.angles[params.dataIndex][1],
+                                            clockwise: true
+                                        },
+                                        style: {
+                                            transition: 'lineWidth',
+                                            fill: CONTENT_COLOR,
+                                            stroke: '#555',
+                                            lineWidth: 0,
+                                            strokeNoScale: true
+                                        },
+                                    };
+                                }
+                            }
+                        };
+                    },
 
-                var currOptionName;
-                function next(nextOptionName) {
-                    var lastOptionInfo = optionInfoList[currOptionName];
-                    var nextOptionInfo = optionInfoList[nextOptionName];
-                    var transitionOpt = {
-                        to: { seriesIndex: 0 }
-                    };
-
-                    if (lastOptionInfo) {
-                        var commonDimension = findCommonDimension(lastOptionInfo, nextOptionInfo)
-                            || findCommonDimension(nextOptionInfo, lastOptionInfo);
-                        if (commonDimension != null) {
-                            transitionOpt = {
-                                from: {
-                                    seriesIndex: 0,
-                                    dimension: commonDimension
+                    'Scatter_ATA_STE_Cluster_Center': function (datasetId) {
+                        return {
+                            grid: {
+                                containLabel: true
+                            },
+                            xAxis: {
+                                name: 'STE'
+                            },
+                            yAxis: {
+                                name: 'ATA'
+                            },
+                            dataZoom: [{
+                                type: 'slider',
+                            }, {
+                                type: 'inside'
+                            }],
+                            series: {
+                                type: 'custom',
+                                coordinateSystem: 'cartesian2d',
+                                animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                                datasetId: datasetId,
+                                encode: {
+                                    x: 'CLUSTER_CENTER_STE',
+                                    y: 'CLUSTER_CENTER_ATA',
+                                    tooltip: ['CLUSTER_CENTER_STE', 'CLUSTER_CENTER_ATA']
                                 },
-                                to: {
-                                    seriesIndex: 0,
-                                    dimension: commonDimension
+                                renderItem: function (params, api) {
+                                    var context = params.context;
+                                    if (!context.layout) {
+                                        context.layout = true;
+                                        context.totalCount = 0;
+                                        for (var i = 0; i < params.dataInsideLength; i++) {
+                                            context.totalCount += api.value('COUNT', i);
+                                        }
+                                    }
+
+                                    var pos = api.coord([
+                                        api.value('CLUSTER_CENTER_STE'),
+                                        api.value('CLUSTER_CENTER_ATA')
+                                    ]);
+                                    var count = api.value('COUNT');
+                                    var radius = count / context.totalCount * 100 + 10;
+                                    return {
+                                        type: 'circle',
+                                        morph: true,
+                                        shape: {
+                                            cx: pos[0],
+                                            cy: pos[1],
+                                            r: radius,
+                                            transition: ['cx', 'cy', 'r']
+                                        },
+                                        style: {
+                                            transition: 'lineWidth',
+                                            fill: CONTENT_COLOR,
+                                            stroke: '#555',
+                                            lineWidth: 0
+                                        }
+                                    };
                                 }
-                            };
-                        }
+                            }
+                        };
                     }
+                };
 
-                    currOptionName = nextOptionName;
-
-                    chart.setOption(nextOptionInfo.option, {
-                        replaceMerge: ['xAxis', 'yAxis'],
-                        transition: transitionOpt
-                    });
 
-                    function findCommonDimension(optionInfoA, optionInfoB) {
-                        var metaA = TRANSITION_INFO[optionInfoA.datasetId];
-                        var metaB = TRANSITION_INFO[optionInfoB.datasetId];
-                        if (metaA.dimensions.indexOf(metaB.uniqueKeyDimension) >= 0) {
-                            return metaB.uniqueKeyDimension;
+                var player = transitionPlayer.create({
+                    chart: function () {
+                        return chart;
+                    },
+                    seriesIndex: 0,
+                    replaceMerge: ['xAxis', 'yAxis'],
+                    dataMeta: {
+                        raw: {
+                            dimensions: RAW_DATA_DIMENSIONS
+                        },
+                        mTagSum: {
+                            dimensions: M_TAG_SUM_DIMENSIONS,
+                            uniqueDimension: 'M_TAG',
+                        },
+                        rawClusters: {
+                            dimensions: RAW_CLUSTER_DIMENSIONS
+                        },
+                        rawClusterCenters: {
+                            dimensions: RAW_CLUSTER_CENTERS_DIMENSIONS,
+                            uniqueDimension: 'CLUSTER_IDX'
                         }
-                    }
-                }
+                    },
+                    optionList: [{
+                        key: 'Scatter_ATA_STE',
+                        dataMetaKey: 'rawClusters',
+                        option: optionCreators['Scatter_ATA_STE']('rawClusters')
+                    }, {
+                        key: 'Bar_mTagSum_ATA',
+                        dataMetaKey: 'mTagSum',
+                        option: optionCreators['Bar_mSum_ATA']('mTagSum')
+                    }, {
+                        key: 'Pie_mTagSum_ATA',
+                        dataMetaKey: 'mTagSum',
+                        option: optionCreators['Pie_mSum_ATA']('mTagSum')
+                    }, {
+                        key: 'Scatter_ATA_STE_Cluster_Center',
+                        dataMetaKey: 'rawClusterCenters',
+                        option: optionCreators['Scatter_ATA_STE_Cluster_Center']('rawClusterCenters')
+                    }]
+                });
 
 
                 var chart = testHelper.create(echarts, 'main0', {
@@ -590,29 +450,29 @@ under the License.
                     buttons: [{
                         text: 'Bar_mTagSum_ATA',
                         onclick: function () {
-                            next('Bar_mTagSum_ATA');
+                            player.go('Bar_mTagSum_ATA');
                         }
                     }, {
                         text: 'Scatter_ATA_STE',
                         onclick: function () {
-                            next('Scatter_ATA_STE');
+                            player.go('Scatter_ATA_STE');
                         }
                     }, {
                         text: 'Pie_mTagSum_ATA',
                         onclick: function () {
-                            next('Pie_mTagSum_ATA');
+                            player.go('Pie_mTagSum_ATA');
                         }
                     }, {
                         text: 'Scatter_ATA_STE_Cluster_Center',
                         onclick: function () {
-                            next('Scatter_ATA_STE_Cluster_Center');
+                            player.go('Scatter_ATA_STE_Cluster_Center');
                         }
                     }]
                 });
 
-                next('Scatter_ATA_STE');
+                player.go('Scatter_ATA_STE');
 
-            })();
+            });
 
         </script>
 
diff --git a/test/data-transform-ecStat.html b/test/data-transform-ecStat.html
index 7663e3b..beb2c2f 100644
--- a/test/data-transform-ecStat.html
+++ b/test/data-transform-ecStat.html
@@ -23,13 +23,12 @@ under the License.
     <head>
         <meta charset="utf-8">
         <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <script src="lib/esl.js"></script>
+        <script src="lib/config.js"></script>
         <script src="lib/jquery.min.js"></script>
         <script src="lib/facePrint.js"></script>
         <script src="lib/testHelper.js"></script>
         <script src="../dist/echarts.js"></script>
-        <script src="../../echarts-stat/dist/ecStat.js"></script>
-        <!-- <script src="lib/ecStat-1.1.1.min.js"></script> -->
-        <!-- <script src="lib/ecStatTransform.js"></script> -->
         <!-- <script src="ut/lib/canteen.js"></script> -->
         <link rel="stylesheet" href="lib/reset.css" />
     </head>
@@ -46,6 +45,8 @@ under the License.
 
         <script>
 
+        require(['echarts', 'ecStat'], function (echarts, ecStat) {
+
             echarts.registerTransform(ecStat.transform.regression);
 
             var rawData = [
@@ -100,19 +101,25 @@ under the License.
                 }, {
                     name: 'regression',
                     type: 'line',
-                    symbol: 'none',
-                    datasetIndex: 1
+                    symbolSize: 0.1,
+                    symbol: 'circle',
+                    datasetIndex: 1,
+                    label: { show: true, fontSize: 16 },
+                    labelLayout: { dx: -20 },
+                    encode: { label: 2, tooltip: 1 }
                 }]
             };
 
             var chart = testHelper.create(echarts, 'main_regression', {
                 title: [
-                    'Regression',
+                    'Regression: the **expression string** should be displayed',
                 ],
                 option: option
             });
 
 
+        });
+
         </script>
 
 
diff --git a/test/lib/config.js b/test/lib/config.js
index c980f4e..99ee5ff 100644
--- a/test/lib/config.js
+++ b/test/lib/config.js
@@ -60,11 +60,13 @@
             paths: {
                 'echarts': ecDistPath,
                 'zrender': 'node_modules/zrender/dist/zrender',
+                // 'ecStat': 'test/lib/ecStat.min.js',
+                'ecStat': 'http://localhost:8001/echarts/echarts-stat/dist/ecStat',
                 'geoJson': '../geoData/geoJson',
                 'theme': 'theme',
                 'data': 'test/data',
                 'map': 'map',
-                'extension': 'dist/extension'
+                'extension': 'dist/extension',
             }
             // urlArgs will prevent break point on init in debug tool.
             // urlArgs: '_v_=' + (+new Date())


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


[incubator-echarts] 05/05: test: add experimental transform.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 82f62ca7d20ec28a2134110a8f5ba3c7b237c44c
Author: 100pah <su...@gmail.com>
AuthorDate: Mon Sep 28 00:04:45 2020 +0800

    test: add experimental transform.
---
 test/custom-shape-morphing3.html   | 242 +++++++++++++++++++++++++++++++++++
 test/lib/ecStatTransform.js        |  48 -------
 test/lib/myTransform/aggregate.js  | 174 +++++++++++++++++++++++++
 test/lib/myTransform/identifier.js |  86 +++++++++++++
 test/lib/transitionPlayer.js       | 253 +++++++++++++++++++++++++++++++++++++
 5 files changed, 755 insertions(+), 48 deletions(-)

diff --git a/test/custom-shape-morphing3.html b/test/custom-shape-morphing3.html
new file mode 100644
index 0000000..b416ae3
--- /dev/null
+++ b/test/custom-shape-morphing3.html
@@ -0,0 +1,242 @@
+<!DOCTYPE html>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<html>
+    <head>
+        <meta charset='utf-8'>
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <script src="lib/esl.js"></script>
+        <script src="lib/config.js"></script>
+        <script src='lib/jquery.min.js'></script>
+        <script src="../dist/echarts.js"></script>
+        <script src="lib/testHelper.js"></script>
+        <script src="lib/myTransform/aggregate.js"></script>
+        <script src="lib/myTransform/identifier.js"></script>
+        <script src="lib/transitionPlayer.js"></script>
+        <link rel="stylesheet" href="lib/reset.css" />
+    </head>
+    <body>
+        <style>
+        </style>
+
+        <div id="main0"></div>
+
+
+
+        <script>
+
+        require(['echarts', 'ecStat'], function (echarts, ecStat) {
+            $.get('data/life-expectancy-table.json', function (rawData) {
+
+                echarts.registerTransform(window.myTransform.aggregate);
+                echarts.registerTransform(window.myTransform.identifier);
+
+                const COLORS = [
+                    '#37A2DA', '#e06343', '#37a354', '#b55dba', '#b5bd48', '#8378EA', '#96BFFF'
+                ];
+                const CONTENT_COLOR = '#37a354';
+
+                // const COLORS = [
+                //     {name: 'Income', index: 0, text: '人均收入', unit: '美元'},
+                //     {name: 'LifeExpectancy', index: 1, text: '人均寿命', unit: '岁'},
+                //     {name: 'Population', index: 2, text: '总人口', unit: ''},
+                //     {name: 'Country', index: 3, text: '国家', unit: ''}
+                // ];
+
+                var RAW_DATA_DIMENSIONS = ['Income', 'Life Expectancy', 'Population', 'Country', 'Year'];
+                var ID_RAW_DATA_DIMENSIONS = ['Income', 'Life Expectancy', 'Population', 'Country', 'Year', 'Id'];
+
+                var baseOption = {
+                    dataset: [{
+                        id: 'RawData',
+                        source: rawData
+                    }, {
+                        id: 'IdRawData',
+                        transform: {
+                            type: 'myTransform:identifier',
+                            config: {
+                                dimensionIndex: ID_RAW_DATA_DIMENSIONS.indexOf('Id'),
+                                dimensionName: 'Id'
+                            }
+                        }
+                    }, {
+                        id: 'GermanyData',
+                        fromDatasetId: 'IdRawData',
+                        transform: {
+                            type: 'filter',
+                            config: {
+                                dimension: 'Country', '=': 'Germany'
+                            }
+                        }
+                    }],
+                    tooltip: {}
+                };
+
+                var optionCreators = {
+
+                    'Country_A_Year_Income_Bar': function (datasetId) {
+                        return {
+                            xAxis: {
+                                type: 'category'
+                            },
+                            yAxis: {
+                                name: 'Income',
+                                nameGap: 25,
+                                nameTextStyle: {
+                                    fontSize: 25
+                                }
+                            },
+                            series: {
+                                type: 'custom',
+                                coordinateSystem: 'cartesian2d',
+                                datasetId: datasetId,
+                                encode: {
+                                    x: 'Year',
+                                    y: 'Income'
+                                },
+                                renderItem: function (params, api) {
+                                    var valPos = api.coord([api.value('Year'), api.value('Income')]);
+                                    var basePos = api.coord([api.value('Year'), 0]);
+                                    var width = api.size([1, 0])[0] * 0.9;
+                                    return {
+                                        type: 'rect',
+                                        shape: {
+                                            x: basePos[0],
+                                            y: basePos[1],
+                                            width: width,
+                                            height: valPos[1] - basePos[1],
+                                            transition: ['x', 'y', 'width', 'height']
+                                        },
+                                        style: {
+                                            fill: CONTENT_COLOR
+                                        }
+                                    };
+                                }
+                            }
+                        };
+                    },
+
+                    'Country_A_Year_Population_Bar': function (datasetId) {
+                        return {
+                            xAxis: {
+                                type: 'category'
+                            },
+                            yAxis: {
+                                name: 'Income',
+                                nameGap: 25,
+                                nameTextStyle: {
+                                    fontSize: 25
+                                }
+                            },
+                            series: {
+                                type: 'custom',
+                                coordinateSystem: 'cartesian2d',
+                                datasetId: datasetId,
+                                encode: {
+                                    x: 'Year',
+                                    y: 'Population'
+                                },
+                                renderItem: function (params, api) {
+                                    var valPos = api.coord([api.value('Year'), api.value('Population')]);
+                                    var basePos = api.coord([api.value('Year'), 0]);
+                                    var width = api.size([1, 0])[0] * 0.9;
+                                    return {
+                                        type: 'rect',
+                                        shape: {
+                                            x: basePos[0],
+                                            y: basePos[1],
+                                            width: width,
+                                            height: valPos[1] - basePos[1],
+                                            transition: ['x', 'y', 'width', 'height']
+                                        },
+                                        style: {
+                                            fill: CONTENT_COLOR
+                                        }
+                                    };
+                                }
+                            }
+                        };
+                    }
+
+                };
+
+                var player = transitionPlayer.create({
+                    chart: function () {
+                        return chart;
+                    },
+                    seriesIndex: 0,
+                    replaceMerge: ['xAxis', 'yAxis'],
+                    dataMeta: {
+                        'IdRawData': {
+                            dimensions: ID_RAW_DATA_DIMENSIONS,
+                            uniqueDimension: 'Id'
+                        },
+                        'GermanyData': {
+                            dimensions: ID_RAW_DATA_DIMENSIONS,
+                            uniqueDimension: 'Id'
+                        }
+                    },
+                    optionList: [{
+                        key: 'Country_A_Year_Income_Bar',
+                        dataMetaKey: 'GermanyData',
+                        option: optionCreators['Country_A_Year_Income_Bar']('GermanyData')
+                    }, {
+                        key: 'Country_A_Year_Population_Bar',
+                        dataMetaKey: 'GermanyData',
+                        option: optionCreators['Country_A_Year_Population_Bar']('GermanyData')
+                    }]
+                });
+
+
+                var chart = testHelper.create(echarts, 'main0', {
+                    title: [
+                        'Test: buttons, should morph animation merge/split.',
+                        'Test: click buttons **before animation finished**, should no blink.',
+                        'Test: click buttons **twice**, should no blink.',
+                        'Test: use dataZoom, update animation should exist'
+                    ],
+                    option: baseOption,
+                    lazyUpdate: true,
+                    height: 600,
+                    buttons: [{
+                        text: 'next',
+                        onclick: function () {
+                            player.next();
+                        }
+                    }, {
+                        text: 'previous',
+                        onclick: function () {
+                            player.previous();
+                        }
+                    }]
+                });
+
+                player.next();
+
+            });
+
+        });
+
+        </script>
+
+
+
+    </body>
+</html>
\ No newline at end of file
diff --git a/test/lib/ecStatTransform.js b/test/lib/ecStatTransform.js
deleted file mode 100644
index e411eed..0000000
--- a/test/lib/ecStatTransform.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
-* 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.
-*/
-
-(function (root) {
-
-    root.ecStatTransform = function (ecStat) {
-
-        var regression = {
-
-            type: 'ecStat:regression',
-
-            transform: function transform(params) {
-                var source = params.source;
-                var config = params.config || {};
-                var method = config.method || 'linear';
-                var result = ecStat.regression(method, source.data);
-
-                return [{
-                    data: result.points
-                }, {
-                    data: [[result.expression]]
-                }];
-            }
-        };
-
-
-        return {
-            regression: regression
-        }
-    };
-
-})(window);
diff --git a/test/lib/myTransform/aggregate.js b/test/lib/myTransform/aggregate.js
new file mode 100644
index 0000000..a5fd4eb
--- /dev/null
+++ b/test/lib/myTransform/aggregate.js
@@ -0,0 +1,174 @@
+/*
+* 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.
+*/
+
+(function (exports) {
+
+    /**
+     * @usage
+     *
+     * ```js
+     * dataset: [{
+     *     source: [
+     *         ['aa', 'bb', 'cc', 'tag'],
+     *         [12, 0.33, 5200, 'AA'],
+     *         [21, 0.65, 7100, 'AA'],
+     *         [51, 0.15, 1100, 'BB'],
+     *         [71, 0.75, 9100, 'BB'],
+     *         ...
+     *     ]
+     * }, {
+     *     transform: {
+     *         type: 'my:aggregate',
+     *         config: {
+     *             resultDimensions: [
+     *                 // by default, use the same name with `from`.
+     *                 { from: 'aa', method: 'sum' },
+     *                 { from: 'bb', method: 'count' },
+     *                 { from: 'cc' }, // method by default: use the first value.
+     *                 { from: 'tag' }
+     *             ],
+     *             groupBy: 'tag'
+     *         }
+     *     }
+     *     // Then the result data will be:
+     *     // [
+     *     //     ['aa', 'bb', 'cc', 'tag'],
+     *     //     [12, 0.33, 5200, 'AA'],
+     *     //     [21, 0.65, 8100, 'BB'],
+     *     //     ...
+     *     // ]
+     * }]
+     * ```
+     */
+    var transform = {
+
+        type: 'myTransform:aggregate',
+
+        /**
+         * @param params
+         * @param params.config.resultDimensions Mandatory.
+         *        {
+         *            // Optional. The name of the result dimensions.
+         *            // If not provided, inherit the name from `from`.
+         *            name: DimensionName;
+         *            // Mandatory. `from` is used to reference dimension from `source`.
+         *            from: DimensionIndex | DimensionName;
+         *            // Optional. Aggregate method. Currently only these method supported.
+         *            // If not provided, use `'first'`.
+         *            method: 'sum' | 'count' | 'first';
+         *        }[]
+         * @param params.config.groupBy DimensionIndex | DimensionName Optional.
+         */
+        transform: function (params) {
+            var upstream = params.upstream;
+            var config = params.config;
+            var resultDimensionsConfig = config.resultDimensions;
+
+            var resultDimInfoList = [];
+            var resultDimensions = [];
+            for (var i = 0; i < resultDimensionsConfig.length; i++) {
+                var resultDimInfoConfig = resultDimensionsConfig[i];
+                var resultDimInfo = upstream.getDimensionInfo(resultDimInfoConfig.from);
+                assert(resultDimInfo, 'Can not find dimension by `from`: ' + resultDimInfoConfig.from);
+                resultDimInfo.method = resultDimInfoConfig.method;
+                resultDimInfoList.push(resultDimInfo);
+                if (resultDimInfoConfig.name != null) {
+                    resultDimInfo.name = resultDimInfoConfig.name;
+                }
+                resultDimensions.push(resultDimInfo.name);
+            }
+
+            var resultData = [];
+
+            var groupBy = config.groupBy;
+            var groupByDimInfo;
+            if (groupBy != null) {
+                var groupMap = {};
+                groupByDimInfo = upstream.getDimensionInfo(groupBy);
+                assert(groupByDimInfo, 'Can not find dimension by `groupBy`: ' + groupBy);
+
+                for (var dataIndex = 0, len = upstream.count(); dataIndex < len; dataIndex++) {
+                    var groupByVal = upstream.retrieveValue(dataIndex, groupByDimInfo.index);
+
+                    if (!groupMap.hasOwnProperty(groupByVal)) {
+                        var newLine = createLine(upstream, dataIndex, resultDimInfoList, groupByDimInfo, groupByVal);
+                        resultData.push(newLine);
+                        groupMap[groupByVal] = newLine;
+                    }
+                    else {
+                        var targetLine = groupMap[groupByVal];
+                        aggregateLine(upstream, dataIndex, targetLine, resultDimInfoList, groupByDimInfo);
+                    }
+                }
+            }
+            else {
+                var targetLine = createLine(upstream, 0, resultDimInfoList);
+                resultData.push(targetLine);
+                for (var dataIndex = 0, len = upstream.count(); dataIndex < len; dataIndex++) {
+                    aggregateLine(upstream, dataIndex, targetLine, resultDimInfoList);
+                }
+            }
+
+            return {
+                dimensions: resultDimensions,
+                data: resultData
+            };
+        }
+    };
+
+    function createLine(upstream, dataIndex, resultDimInfoList, groupByDimInfo, groupByVal) {
+        var newLine = [];
+        for (var j = 0; j < resultDimInfoList.length; j++) {
+            var resultDimInfo = resultDimInfoList[j];
+            var method = resultDimInfo.method;
+            newLine[j] = (groupByDimInfo && resultDimInfo.index === groupByDimInfo.index)
+                ? groupByVal
+                : (method === 'sum' || method === 'count')
+                ? 0
+                // By default, method: 'first'
+                : upstream.retrieveValue(dataIndex, resultDimInfo.index);
+        }
+        return newLine;
+    }
+
+    function aggregateLine(upstream, dataIndex, targetLine, resultDimInfoList, groupByDimInfo) {
+        for (var j = 0; j < resultDimInfoList.length; j++) {
+            var resultDimInfo = resultDimInfoList[j];
+            var method = resultDimInfo.method;
+            if (!groupByDimInfo || resultDimInfo.index !== groupByDimInfo.index) {
+                if (method === 'sum') {
+                    targetLine[j] += upstream.retrieveValue(dataIndex, resultDimInfo.index);
+                }
+                else if (method === 'count') {
+                    targetLine[j] += 1;
+                }
+            }
+        }
+    }
+
+    function assert(cond, msg) {
+        if (!cond) {
+            throw new Error(msg || 'transition player error');
+        }
+    }
+
+    var myTransform = exports.myTransform = exports.myTransform || {};
+    myTransform.aggregate = transform;
+
+})(this);
diff --git a/test/lib/myTransform/identifier.js b/test/lib/myTransform/identifier.js
new file mode 100644
index 0000000..8ff2590
--- /dev/null
+++ b/test/lib/myTransform/identifier.js
@@ -0,0 +1,86 @@
+/*
+* 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.
+*/
+
+(function (exports) {
+
+    /**
+     * @usage
+     *
+     * ```js
+     * dataset: [{
+     *     source: [
+     *         ['aa', 'bb', 'cc', 'tag'],
+     *         [12, 0.33, 5200, 'AA'],
+     *         [21, 0.65, 8100, 'AA'],
+     *         ...
+     *     ]
+     * }, {
+     *     transform: {
+     *         type: 'my:identifier',
+     *         config: {
+     *             dimensionIndex: 4,
+     *             dimensionName: 'ID'
+     *         }
+     *     }
+     *     // Then the result data will be:
+     *     // [
+     *     //     ['aa', 'bb', 'cc', 'tag', 'ID'],
+     *     //     [12, 0.33, 5200, 'AA', 0],
+     *     //     [21, 0.65, 8100, 'BB', 1],
+     *     //     ...
+     *     // ]
+     * }]
+     * ```
+     */
+    var transform = {
+
+        type: 'myTransform:identifier',
+
+        /**
+         * @param params.config.dimensionIndex DimensionIndex
+         *        Mandatory. Specify where to put the new id dimension.
+         * @param params.config.dimensionName DimensionName
+         *        Optional. If not provided, left the dimension name not defined.
+         */
+        transform: function (params) {
+            var upstream = params.upstream;
+            var config = params.config;
+            var dimensionIndex = config.dimensionIndex;
+            var dimensionName = config.dimensionName;
+
+            var dimsDef = upstream.cloneAllDimensionInfo();
+            dimsDef[dimensionIndex] = dimensionName;
+
+            for (var i = 0, len = upstream.count(); i < len; i++) {
+                var line = upstream.getRawDataItem(i);
+                line[dimensionIndex] = i;
+            }
+
+            return {
+                dimensions: dimsDef,
+                data: upstream.data
+            };
+        }
+    };
+
+    var myTransform = exports.myTransform = exports.myTransform || {};
+    myTransform.identifier = transform;
+
+})(this);
+
diff --git a/test/lib/transitionPlayer.js b/test/lib/transitionPlayer.js
new file mode 100644
index 0000000..fae17c7
--- /dev/null
+++ b/test/lib/transitionPlayer.js
@@ -0,0 +1,253 @@
+/*
+* 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.
+*/
+
+(function (exports) {
+
+    var transitionPlayer = {};
+
+    /**
+     * @usage
+     * ```js
+     * // Initialize with an array of echarts option info:
+     * var player = transitionPlayer.create({
+     *
+     *     // The echarts instance or chart instance getter.
+     *     chart: function () {
+     *         return myChart;
+     *     },
+     *     seriesIndex: 0,
+     *     replaceMerge: ['xAxis', 'yAxis']
+     *
+     *     // The data meta info used to determine how to
+     *     // make transition mapping.
+     *     // The strategy: If `uniqueDimension` provided and is a common
+     *     // dimension, use `uniqueDimension`.
+     *     dataMeta: {
+     *         aaa: {
+     *             dimensions: ['qqq', 'www', 'eee', 'rrr']
+     *         },
+     *         bbb: {
+     *             dimensions: ['ccc', 'www', 'eee'],
+     *             uniqueDimension: 'www'
+     *         },
+     *         ...
+     *     },
+     *
+     *     // echarts option collection:
+     *     optionList: [
+     *         // dataMetaKey is the key of 'dataMeta'.
+     *         { key: 'Time_Income_Bar', option: option0, dataMetaKey: 'aaa' },
+     *         { key: 'Population_Income_Scatter', option: option1, dataMetaKey: 'bbb' },
+     *         { key: 'Time_Income_Pie', option: option2, dataMetaKey: 'aaa' },
+     *         ...
+     *     ]
+     * });
+     *
+     * // Then start to play:
+     * player.next(); // Display next option (from the first option).
+     * player.previous(); // Display previous optoin.
+     * player.go('Time_Income_Pie'); // Display the specified option.
+     * player.getOptionKeys(); // return `['Time_Income_Bar', 'Population_Income_Scatter', 'Time_Income_Pie']`
+     * ```
+     *
+     * @parma opt See the constructor of `TransitionPlayer`.
+     */
+    transitionPlayer.create = function (opt) {
+        return new TransitionPlayer(opt);
+    };
+
+    /**
+     * @param opt
+     * @param opt.chart
+     *        (EChartsInstance | () => EChartsInstance)
+     *        echarts instance or echarts instance getter.
+     * @param opt.dataMeta
+     *        {
+     *            [dataMetaKey in string]: {
+     *                dimensions: string[];
+     *                uniqueDimension?: string;
+     *            }
+     *        }
+     * @param opt.optionList
+     *        {
+     *            key: string;
+     *            option: EChartsOption;
+     *            dataMetaKey: string;
+     *        }[]
+     * @param opt.seriesIndex number
+     *        Target series index to be transitioned.
+     * @param opt.replaceMerge? string[]
+     */
+    function TransitionPlayer(opt) {
+        assert(
+            opt.chart
+            && isObject(opt.dataMeta)
+            && isArray(opt.optionList)
+            && opt.seriesIndex != null
+            && opt.optionList.length
+        );
+
+        this._chart = opt.chart;
+        this._dataMeta = opt.dataMeta;
+        var optionList = this._optionList = opt.optionList;
+        var optionMap = this._optionMap = {};
+        this._replaceMerge = opt.replaceMerge;
+        this._seriesIndex = opt.seriesIndex;
+        this._currOptionIdx = null;
+
+        for (var i = 0; i < optionList.length; i++) {
+            var optionWrap = optionList[i];
+            var optionKey = optionWrap.key;
+            if (optionKey != null) {
+                assert(!hasOwn(optionMap, optionKey), 'option key duplicat: ' + optionKey);
+                optionMap[optionKey] = i;
+            }
+        }
+    }
+
+    var proto = TransitionPlayer.prototype;
+
+    proto.next = function () {
+        var optionList = this._optionList;
+        var newOptionIdx = this._currOptionIdx == null
+            ? 0
+            : Math.min(optionList.length - 1, this._currOptionIdx + 1);
+
+        this._doChangeOption(newOptionIdx);
+    };
+
+    proto.previous = function () {
+        var optionList = this._optionList;
+        var newOptionIdx = this._currOptionIdx == null
+            ? optionList.length - 1
+            : Math.max(0, this._currOptionIdx - 1);
+
+        this._doChangeOption(newOptionIdx);
+    };
+
+    /**
+     * @param optionKey string
+     */
+    proto.go = function (optionKey) {
+        var newOptionIdx = getMapValue(this._optionMap, optionKey);
+        assert(newOptionIdx != null, 'Can not find option by option key: ' + optionKey);
+
+        this._doChangeOption(newOptionIdx);
+    };
+
+    proto._doChangeOption = function (newOptionIdx) {
+        var optionList = this._optionList;
+        var oldOptionWrap = this._currOptionIdx != null ? optionList[this._currOptionIdx] : null;
+        var newOptionWrap = optionList[newOptionIdx];
+        var dataMeta = this._dataMeta;
+        var targetSeriesIndex = this._seriesIndex;
+
+        var transitionOpt = {
+            // If can not find mapped dimensions, do not make transition animation
+            // by default, becuase this transition probably bring about misleading.
+            to: { seriesIndex: targetSeriesIndex }
+        };
+
+        if (oldOptionWrap) {
+            var commonDimension =
+                findCommonDimension(oldOptionWrap, newOptionWrap)
+                || findCommonDimension(newOptionWrap, oldOptionWrap);
+            if (commonDimension != null) {
+                transitionOpt = {
+                    from: {
+                        seriesIndex: targetSeriesIndex,
+                        dimension: commonDimension
+                    },
+                    to: {
+                        seriesIndex: targetSeriesIndex,
+                        dimension: commonDimension
+                    }
+                };
+            }
+        }
+
+        this._currOptionIdx = newOptionIdx;
+
+        this._getChart().setOption(newOptionWrap.option, {
+            replaceMerge: this._replaceMerge,
+            transition: transitionOpt
+        });
+
+        function findCommonDimension(optionWrapA, optionWrapB) {
+            var metaA = getMapValue(dataMeta, optionWrapA.dataMetaKey);
+            var metaB = getMapValue(dataMeta, optionWrapB.dataMetaKey);
+            var uniqueDimensionB = metaB.uniqueDimension;
+            if (uniqueDimensionB != null && metaA.dimensions.indexOf(uniqueDimensionB) >= 0) {
+                return uniqueDimensionB;
+            }
+        }
+
+    };
+
+    proto._getChart = function () {
+        return isFunction(this._chart) ? this._chart() : this._chart;
+    };
+
+    /**
+     * @return string[]
+     */
+    proto.getOptionKeys = function () {
+        var optionKeys = [];
+        var optionList = this._optionList;
+        for (var i = 0; i < optionList.length; i++) {
+            optionKeys.push(optionList[i].key);
+        }
+        return optionKeys;
+    };
+
+
+    function assert(cond, msg) {
+        if (!cond) {
+            throw new Error(msg || 'transition player error');
+        }
+    }
+
+    function isObject(value) {
+        const type = typeof value;
+        return type === 'function' || (!!value && type === 'object');
+    }
+
+    function isArray(value) {
+        if (Array.isArray) {
+            return Array.isArray(value);
+        }
+        return Object.prototype.toString.call(value) === '[object Array]';
+    }
+
+    function isFunction(value) {
+        return typeof value === 'function';
+    }
+
+    function hasOwn(obj, key) {
+        return obj.hasOwnProperty(key);
+    }
+
+    function getMapValue(map, key) {
+        return (key != null && hasOwn(map, key)) ? map[key] : null;
+    }
+
+
+    exports.transitionPlayer = transitionPlayer;
+
+})(this);


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


[incubator-echarts] 01/05: fix: fix task clear when setOption in the second time and reuse series.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit b7b941a6ae255a6f37e63810e5914da832cdcfbb
Author: 100pah <su...@gmail.com>
AuthorDate: Tue Sep 22 16:27:02 2020 +0800

    fix: fix task clear when setOption in the second time and reuse series.
---
 src/stream/Scheduler.ts | 79 ++++++++++++++++++++++++-------------------------
 src/stream/task.ts      | 57 ++++++++++++++++++++++++++++++-----
 2 files changed, 88 insertions(+), 48 deletions(-)

diff --git a/src/stream/Scheduler.ts b/src/stream/Scheduler.ts
index 6bcaa15..2a0bdc0 100644
--- a/src/stream/Scheduler.ts
+++ b/src/stream/Scheduler.ts
@@ -17,7 +17,7 @@
 * under the License.
 */
 
-import {each, map, isFunction, createHashMap, noop, HashMap} from 'zrender/src/core/util';
+import {each, map, isFunction, createHashMap, noop, HashMap, assert} from 'zrender/src/core/util';
 import {
     createTask, Task, TaskContext,
     TaskProgressCallback, TaskProgressParams, TaskPlanCallbackReturn, PerformArgs
@@ -41,7 +41,7 @@ export type OverallTask = Task<OverallTaskContext> & {
     agentStubMap?: HashMap<StubTask>
 };
 export type StubTask = Task<StubTaskContext> & {
-    agent?: OverallTask
+    agent?: OverallTask;
 };
 
 export type Pipeline = {
@@ -263,6 +263,11 @@ class Scheduler {
         each(this._allHandlers, function (handler) {
             const record = stageTaskMap.get(handler.uid) || stageTaskMap.set(handler.uid, {});
 
+            if (__DEV__) {
+                // Currently do not need to support to sepecify them both.
+                assert(!(handler.reset && handler.overallReset));
+            }
+
             handler.reset && this._createSeriesStageTask(handler, record, ecModel, api);
             handler.overallReset && this._createOverallStageTask(handler, record, ecModel, api);
         }, this);
@@ -408,8 +413,9 @@ class Scheduler {
         api: ExtensionAPI
     ): void {
         const scheduler = this;
-        const seriesTaskMap = stageHandlerRecord.seriesTaskMap
-            || (stageHandlerRecord.seriesTaskMap = createHashMap());
+        const oldSeriesTaskMap = stageHandlerRecord.seriesTaskMap;
+        // Totally stages are about several dozen, so probalby do not need to reuse the map.
+        const newSeriesTaskMap = stageHandlerRecord.seriesTaskMap = createHashMap();
         const seriesType = stageHandler.seriesType;
         const getTargetSeries = stageHandler.getTargetSeries;
 
@@ -431,12 +437,15 @@ class Scheduler {
 
             // Init tasks for each seriesModel only once.
             // Reuse original task instance.
-            const task = seriesTaskMap.get(pipelineId)
-                || seriesTaskMap.set(pipelineId, createTask<SeriesTaskContext>({
+            const task = newSeriesTaskMap.set(
+                pipelineId,
+                oldSeriesTaskMap && oldSeriesTaskMap.get(pipelineId)
+                || createTask<SeriesTaskContext>({
                     plan: seriesTaskPlan,
                     reset: seriesTaskReset,
                     count: seriesTaskCount
-                }));
+                })
+            );
             task.context = {
                 model: seriesModel,
                 ecModel: ecModel,
@@ -449,15 +458,6 @@ class Scheduler {
             };
             scheduler._pipe(seriesModel, task);
         }
-
-        // Clear unused series tasks.
-        const pipelineMap = scheduler._pipelineMap;
-        seriesTaskMap.each(function (task, pipelineId) {
-            if (!pipelineMap.get(pipelineId)) {
-                task.dispose();
-                seriesTaskMap.removeKey(pipelineId);
-            }
-        });
     }
 
     private _createOverallStageTask(
@@ -478,13 +478,14 @@ class Scheduler {
             scheduler: scheduler
         };
 
-        // Reuse orignal stubs.
-        const agentStubMap = overallTask.agentStubMap = overallTask.agentStubMap
-            || createHashMap<StubTask>();
+        const oldAgentStubMap = overallTask.agentStubMap;
+        // Totally stages are about several dozen, so probalby do not need to reuse the map.
+        const newAgentStubMap = overallTask.agentStubMap = createHashMap<StubTask>();
 
         const seriesType = stageHandler.seriesType;
         const getTargetSeries = stageHandler.getTargetSeries;
         let overallProgress = true;
+        let shouldOverallTaskDirty = false;
         // FIXME:TS never used, so comment it
         // let modifyOutputEnd = stageHandler.modifyOutputEnd;
 
@@ -492,7 +493,10 @@ class Scheduler {
         // stub in each pipelines, it will set the overall task dirty when the pipeline
         // progress. Moreover, to avoid call the overall task each frame (too frequent),
         // we set the pipeline block.
-        if (seriesType) {
+        if (stageHandler.createOnAllSeries) {
+            ecModel.eachRawSeries(createStub);
+        }
+        else if (seriesType) {
             ecModel.eachRawSeriesByType(seriesType, createStub);
         }
         else if (getTargetSeries) {
@@ -509,15 +513,18 @@ class Scheduler {
 
         function createStub(seriesModel: SeriesModel): void {
             const pipelineId = seriesModel.uid;
-            let stub = agentStubMap.get(pipelineId);
-            if (!stub) {
-                stub = agentStubMap.set(pipelineId, createTask<StubTaskContext>(
-                    {reset: stubReset, onDirty: stubOnDirty}
-                ));
-                // When the result of `getTargetSeries` changed, the overallTask
-                // should be set as dirty and re-performed.
-                overallTask.dirty();
-            }
+            const stub = newAgentStubMap.set(
+                pipelineId,
+                oldAgentStubMap && oldAgentStubMap.get(pipelineId)
+                || (
+                    // When the result of `getTargetSeries` changed, the overallTask
+                    // should be set as dirty and re-performed.
+                    shouldOverallTaskDirty = true,
+                    createTask<StubTaskContext>(
+                        {reset: stubReset, onDirty: stubOnDirty}
+                    )
+                )
+            );
             stub.context = {
                 model: seriesModel,
                 overallProgress: overallProgress
@@ -530,17 +537,9 @@ class Scheduler {
             scheduler._pipe(seriesModel, stub);
         }
 
-        // Clear unused stubs.
-        const pipelineMap = scheduler._pipelineMap;
-        agentStubMap.each(function (stub, pipelineId) {
-            if (!pipelineMap.get(pipelineId)) {
-                stub.dispose();
-                // When the result of `getTargetSeries` changed, the overallTask
-                // should be set as dirty and re-performed.
-                overallTask.dirty();
-                agentStubMap.removeKey(pipelineId);
-            }
-        });
+        if (shouldOverallTaskDirty) {
+            overallTask.dirty();
+        }
     }
 
     private _pipe(seriesModel: SeriesModel, task: GeneralTask) {
diff --git a/src/stream/task.ts b/src/stream/task.ts
index 103f701..97bcaeb 100644
--- a/src/stream/task.ts
+++ b/src/stream/task.ts
@@ -23,6 +23,7 @@ import { Pipeline } from './Scheduler';
 import { Payload } from '../util/types';
 import List from '../data/List';
 
+
 export interface TaskContext {
     outputData?: List;
     data?: List;
@@ -183,7 +184,6 @@ export class Task<Ctx extends TaskContext> {
         const step = performArgs && performArgs.step;
 
         if (upTask) {
-
             if (__DEV__) {
                 assert(upTask._outputDueEnd != null);
             }
@@ -386,16 +386,18 @@ const iterator: TaskDataIterator = (function () {
 
 ///////////////////////////////////////////////////////////
 // For stream debug (Should be commented out after used!)
-// Usage: printTask(this, 'begin');
-// Usage: printTask(this, null, {someExtraProp});
-// function printTask(task, prefix, extra) {
+// @usage: printTask(this, 'begin');
+// @usage: printTask(this, null, {someExtraProp});
+// @usage: Use `__idxInPipeline` as conditional breakpiont.
+//
+// window.printTask = function (task: any, prefix: string, extra: { [key: string]: unknown }): void {
 //     window.ecTaskUID == null && (window.ecTaskUID = 0);
 //     task.uidDebug == null && (task.uidDebug = `task_${window.ecTaskUID++}`);
 //     task.agent && task.agent.uidDebug == null && (task.agent.uidDebug = `task_${window.ecTaskUID++}`);
 //     let props = [];
 //     if (task.__pipeline) {
 //         let val = `${task.__idxInPipeline}/${task.__pipeline.tail.__idxInPipeline} ${task.agent ? '(stub)' : ''}`;
-//         props.push({text: 'idx', value: val});
+//         props.push({text: '__idxInPipeline/total', value: val});
 //     } else {
 //         let stubCount = 0;
 //         task.agentStubMap.each(() => stubCount++);
@@ -403,7 +405,7 @@ const iterator: TaskDataIterator = (function () {
 //     }
 //     props.push({text: 'uid', value: task.uidDebug});
 //     if (task.__pipeline) {
-//         props.push({text: 'pid', value: task.__pipeline.id});
+//         props.push({text: 'pipelineId', value: task.__pipeline.id});
 //         task.agent && props.push(
 //             {text: 'stubFor', value: task.agent.uidDebug}
 //         );
@@ -421,9 +423,48 @@ const iterator: TaskDataIterator = (function () {
 //     }
 //     let args = ['color: blue'];
 //     let msg = `%c[${prefix || 'T'}] %c` + props.map(item => (
-//         args.push('color: black', 'color: red'),
+//         args.push('color: green', 'color: red'),
 //         `${item.text}: %c${item.value}`
 //     )).join('%c, ');
 //     console.log.apply(console, [msg].concat(args));
 //     // console.log(this);
-// }
+// };
+// window.printPipeline = function (task: any, prefix: string) {
+//     const pipeline = task.__pipeline;
+//     let currTask = pipeline.head;
+//     while (currTask) {
+//         window.printTask(currTask, prefix);
+//         currTask = currTask._downstream;
+//     }
+// };
+// window.showChain = function (chainHeadTask) {
+//     var chain = [];
+//     var task = chainHeadTask;
+//     while (task) {
+//         chain.push({
+//             task: task,
+//             up: task._upstream,
+//             down: task._downstream,
+//             idxInPipeline: task.__idxInPipeline
+//         });
+//         task = task._downstream;
+//     }
+//     return chain;
+// };
+// window.findTaskInChain = function (task, chainHeadTask) {
+//     let chain = window.showChain(chainHeadTask);
+//     let result = [];
+//     for (let i = 0; i < chain.length; i++) {
+//         let chainItem = chain[i];
+//         if (chainItem.task === task) {
+//             result.push(i);
+//         }
+//     }
+//     return result;
+// };
+// window.printChainAEachInChainB = function (chainHeadTaskA, chainHeadTaskB) {
+//     let chainA = window.showChain(chainHeadTaskA);
+//     for (let i = 0; i < chainA.length; i++) {
+//         console.log('chainAIdx:', i, 'inChainB:', window.findTaskInChain(chainA[i].task, chainHeadTaskB));
+//     }
+// };


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


[incubator-echarts] 02/05: feature: (1) Support custom series morphing with split/merge effect. (2) Add API: setOption(option, { transition: { ... } }) to indicate the transition relationship. (3) Fix underlying schedule framework buggy disopose when setOption at the second time and change data.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 82331873c60d100b842a9d70b95c61db963527ae
Author: 100pah <su...@gmail.com>
AuthorDate: Wed Sep 23 17:12:59 2020 +0800

    feature:
    (1) Support custom series morphing with split/merge effect.
    (2) Add API: setOption(option, { transition: { ... } }) to indicate the transition relationship.
    (3) Fix underlying schedule framework buggy disopose when setOption at the second time and change data.
---
 src/chart/custom.ts              | 214 +++++++++++++++------
 src/coord/scaleRawExtentInfo.ts  |  10 +-
 src/data/DataDiffer.ts           | 238 ++++++++++++++++++------
 src/echarts.ts                   |  96 +++++++++-
 src/model/Global.ts              |   8 +-
 src/model/Series.ts              |  11 +-
 src/util/model.ts                |  26 ++-
 src/util/types.ts                |  34 +++-
 test/custom-shape-morphing2.html | 388 +++++++++++++++++++++++++++++++++++++++
 9 files changed, 892 insertions(+), 133 deletions(-)

diff --git a/src/chart/custom.ts b/src/chart/custom.ts
index 0c645f8..ca87fb4 100644
--- a/src/chart/custom.ts
+++ b/src/chart/custom.ts
@@ -19,7 +19,7 @@
 
 import {
     hasOwn, assert, isString, retrieve2, retrieve3, defaults, each,
-    keys, isArrayLike, bind, isFunction, eqNaN
+    keys, isArrayLike, bind, isFunction, eqNaN, isArray
 } from 'zrender/src/core/util';
 import * as graphicUtil from '../util/graphic';
 import { setDefaultStateProxy, enableHoverEmphasis } from '../util/states';
@@ -81,8 +81,8 @@ import {
 import Transformable from 'zrender/src/core/Transformable';
 import { ItemStyleProps } from '../model/mixin/itemStyle';
 import { cloneValue } from 'zrender/src/animation/Animator';
-import { warn, error } from '../util/log';
-import { morphPath } from 'zrender/src/tool/morphPath';
+import { warn, error, throwError } from '../util/log';
+import { morphPath, splitShapeForMorphingFrom, isPathMorphing } from 'zrender/src/tool/morphPath';
 
 
 const inner = makeInner<{
@@ -460,22 +460,58 @@ class CustomSeriesView extends ChartView {
         // is complicated, where merge mode is probably necessary for optimization.
         // For example, reuse graphic elements and only update the transform when
         // roam or data zoom according to `actionType`.
-        data.diff(oldData)
-            .add(function (newIdx) {
-                createOrUpdateItem(
-                    null, newIdx, renderItem(newIdx, payload), customSeries, group, data
-                );
-            })
-            .update(function (newIdx, oldIdx) {
+
+        const transOpt = customSeries.__transientTransitionOpt;
+
+        (new DataDiffer(
+            oldData ? oldData.getIndices() : [],
+            data.getIndices(),
+            createGetKey(oldData, transOpt && transOpt.from),
+            createGetKey(data, transOpt && transOpt.to),
+            null,
+            transOpt ? 'multiple' : 'single'
+        ))
+        .add(function (newIdx) {
+            createOrUpdateItem(
+                null, newIdx, renderItem(newIdx, payload), customSeries, group, data, null
+            );
+        })
+        .update(function (newIdx, oldIdx) {
+            createOrUpdateItem(
+                oldData.getItemGraphicEl(oldIdx),
+                newIdx, renderItem(newIdx, payload), customSeries, group, data, null
+            );
+        })
+        .remove(function (oldIdx) {
+            doRemoveEl(oldData.getItemGraphicEl(oldIdx), customSeries, group);
+        })
+        .updateManyToOne(function (newIdx, oldIndices) {
+            const oldElsToMerge: graphicUtil.Path[] = [];
+            for (let i = 0; i < oldIndices.length; i++) {
+                const oldEl = oldData.getItemGraphicEl(oldIndices[i]);
+                if (elCanMorph(oldEl)) {
+                    oldElsToMerge.push(oldEl);
+                }
+                removeElementDirectly(oldEl, group);
+            }
+            createOrUpdateItem(
+                null, newIdx, renderItem(newIdx, payload), customSeries,
+                group, data, oldElsToMerge
+            );
+        })
+        .updateOneToMany(function (newIndices, oldIdx) {
+            const newLen = newIndices.length;
+            const oldEl = oldData.getItemGraphicEl(oldIdx);
+            const oldElSplitted = elCanMorph(oldEl) ? splitShapeForMorphingFrom(oldEl, newLen) : [];
+            removeElementDirectly(oldEl, group);
+            for (let i = 0; i < newLen; i++) {
                 createOrUpdateItem(
-                    oldData.getItemGraphicEl(oldIdx),
-                    newIdx, renderItem(newIdx, payload), customSeries, group, data
+                    null, newIndices[i], renderItem(newIndices[i], payload), customSeries,
+                    group, data, oldElSplitted[i]
                 );
-            })
-            .remove(function (oldIdx) {
-                doRemoveEl(oldData.getItemGraphicEl(oldIdx), customSeries, group);
-            })
-            .execute();
+            }
+        })
+        .execute();
 
         // Do clipping
         const clipPath = customSeries.get('clip', true)
@@ -516,7 +552,9 @@ class CustomSeriesView extends ChartView {
             }
         }
         for (let idx = params.start; idx < params.end; idx++) {
-            const el = createOrUpdateItem(null, idx, renderItem(idx, payload), customSeries, this.group, data);
+            const el = createOrUpdateItem(
+                null, idx, renderItem(idx, payload), customSeries, this.group, data, null
+            );
             el.traverse(setIncrementalAndHoverLayer);
         }
     }
@@ -544,6 +582,40 @@ class CustomSeriesView extends ChartView {
 ChartView.registerClass(CustomSeriesView);
 
 
+function createGetKey(data: List, dimension: DimensionLoose) {
+    if (!data) {
+        return;
+    }
+
+    const diffBy = data.getDimension(dimension);
+
+    if (diffBy == null) {
+        return function (rawIdx: number, dataIndex: number) {
+            return data.getId(dataIndex);
+        };
+    }
+
+    const dimInfo = data.getDimensionInfo(diffBy);
+    if (!dimInfo) {
+        let errMsg = '';
+        if (__DEV__) {
+            errMsg = `${dimension} is not a valid dimension.`;
+        }
+        throwError(errMsg);
+    }
+    const ordinalMeta = dimInfo.ordinalMeta;
+    return function (rawIdx: number, dataIndex: number) {
+        let key = data.get(diffBy, dataIndex);
+        if (ordinalMeta) {
+            key = ordinalMeta.categories[key as number];
+        }
+        return (key == null || eqNaN(key))
+            ? rawIdx + ''
+            : '_ec_' + key;
+    };
+}
+
+
 function createEl(elOption: CustomElementOption): Element {
     const graphicType = elOption.type;
     let el;
@@ -582,11 +654,13 @@ function createEl(elOption: CustomElementOption): Element {
     }
     else {
         const Clz = graphicUtil.getShapeClass(graphicType);
-
-        if (__DEV__) {
-            assert(Clz, 'graphic type "' + graphicType + '" can not be found.');
+        if (!Clz) {
+            let errMsg = '';
+            if (__DEV__) {
+                errMsg = 'graphic type "' + graphicType + '" can not be found.';
+            }
+            throwError(errMsg);
         }
-
         el = new Clz();
     }
 
@@ -654,7 +728,7 @@ function createEl(elOption: CustomElementOption): Element {
  */
 function updateElNormal(
     el: Element,
-    morphingFromEl: Element,
+    morphingFromEl: graphicUtil.Path | graphicUtil.Path[],
     dataIndex: number,
     elOption: CustomElementOption,
     styleOpt: CustomElementOption['style'],
@@ -757,7 +831,7 @@ function updateElNormal(
     styleOpt ? el.dirty() : el.markRedraw();
 
     if (morphingFromEl) {
-        applyShapeMorphingAnimation(morphingFromEl, el, seriesModel, dataIndex);
+        applyShapeMorphingAnimation(morphingFromEl, el as graphicUtil.Path, seriesModel, dataIndex);
     }
 }
 
@@ -766,7 +840,7 @@ function updateElNormal(
 function prepareShapeOrExtraUpdate(
     mainAttr: 'shape' | 'extra',
     el: Element,
-    morphingFromEl: Element,
+    morphingFromEl: graphicUtil.Path | graphicUtil.Path[],
     elOption: CustomElementOption,
     allProps: LooseElementProps,
     transFromProps: LooseElementProps,
@@ -834,14 +908,14 @@ function prepareShapeOrExtraUpdate(
 // See [STRATEGY_TRANSITION].
 function prepareTransformUpdate(
     el: Element,
-    morphingFromEl: Element,
+    morphingFromEl: graphicUtil.Path | graphicUtil.Path[],
     elOption: CustomElementOption,
     allProps: ElementProps,
     transFromProps: ElementProps,
     isInit: boolean
 ): void {
     const enterFrom = elOption.enterFrom;
-    const fromEl = morphingFromEl || el;
+    const fromEl = (morphingFromEl instanceof graphicUtil.Path) && morphingFromEl || el;
     if (isInit && enterFrom) {
         const enterFromKeys = keys(enterFrom);
         for (let i = 0; i < enterFromKeys.length; i++) {
@@ -904,7 +978,7 @@ function prepareTransformUpdate(
 // See [STRATEGY_TRANSITION].
 function prepareStyleUpdate(
     el: Element,
-    morphingFromEl: Element,
+    morphingFromEl: graphicUtil.Path | graphicUtil.Path[],
     styleOpt: CustomElementOption['style'],
     transFromProps: LooseElementProps,
     isInit: boolean
@@ -913,7 +987,7 @@ function prepareStyleUpdate(
         return;
     }
 
-    const fromEl = morphingFromEl || el;
+    const fromEl = (morphingFromEl instanceof graphicUtil.Path) && morphingFromEl || el;
 
     const fromElStyle = (fromEl as LooseElementProps).style as LooseElementProps['style'];
     let transFromStyleProps: LooseElementProps['style'];
@@ -1546,7 +1620,8 @@ function createOrUpdateItem(
     elOption: CustomElementOption,
     seriesModel: CustomSeriesModel,
     group: ViewRootGroup,
-    data: List<CustomSeriesModel>
+    data: List<CustomSeriesModel>,
+    morphingFroms: graphicUtil.Path | graphicUtil.Path[]
 ): Element {
     // [Rule]
     // If `renderItem` returns `null`/`undefined`/`false`, remove the previous el if existing.
@@ -1557,10 +1632,10 @@ function createOrUpdateItem(
 
     // If `elOption` is `null`/`undefined`/`false` (when `renderItem` returns nothing).
     if (!elOption) {
-        el && group.remove(el);
+        removeElementDirectly(el, group);
         return;
     }
-    el = doCreateOrUpdateEl(el, dataIndex, elOption, seriesModel, group, true);
+    el = doCreateOrUpdateEl(el, dataIndex, elOption, seriesModel, group, true, morphingFroms);
     el && data.setItemGraphicEl(dataIndex, el);
 
     enableHoverEmphasis(el, elOption.focus, elOption.blurScope);
@@ -1568,13 +1643,12 @@ function createOrUpdateItem(
     return el;
 }
 
-function applyShapeMorphingAnimation(oldEl: Element, el: Element, seriesModel: SeriesModel, dataIndex: number) {
-    if (!((oldEl instanceof graphicUtil.Path) && (el instanceof graphicUtil.Path))) {
-        if (__DEV__) {
-            error('`morph` can only be applied on two paths.');
-        }
-        return;
-    }
+function applyShapeMorphingAnimation(
+    oldEl: graphicUtil.Path | graphicUtil.Path[],
+    el: graphicUtil.Path,
+    seriesModel: SeriesModel,
+    dataIndex: number
+) {
     if (seriesModel.isAnimationEnabled()) {
         const duration = seriesModel.get('animationDurationUpdate');
         const delay = seriesModel.get('animationDelayUpdate');
@@ -1590,13 +1664,20 @@ function applyShapeMorphingAnimation(oldEl: Element, el: Element, seriesModel: S
     }
 }
 
+function hintInvalidMorph(): void {
+    if (__DEV__) {
+        error('`morph` can only be applied on two paths.');
+    }
+}
+
 function doCreateOrUpdateEl(
     el: Element,
     dataIndex: number,
     elOption: CustomElementOption,
     seriesModel: CustomSeriesModel,
     group: ViewRootGroup,
-    isRoot: boolean
+    isRoot: boolean,
+    morphingFroms: graphicUtil.Path | graphicUtil.Path[]
 ): Element {
 
     if (__DEV__) {
@@ -1604,17 +1685,29 @@ function doCreateOrUpdateEl(
     }
 
     let toBeReplacedIdx = -1;
-    let oldEl: Element;
+    let morphingFromEl: graphicUtil.Path | graphicUtil.Path[];
+    const optionMorph = (elOption as CustomZRPathOption).morph;
 
-    if (el && doesElNeedRecreate(el, elOption)) {
-        // Should keep at the original index, otherwise "merge by index" will be incorrect.
-        toBeReplacedIdx = group.childrenRef().indexOf(el);
-        oldEl = el;
-        el = null;
+    if (el) {
+        const elNeedRecreate = doesElNeedRecreate(el, elOption);
+        if (optionMorph) {
+            if (!elCanMorph(el)) {
+                hintInvalidMorph();
+            }
+            // When an el has been morphing, we also need to make it go through
+            // morphing logic, other it will blink.
+            else if (elNeedRecreate || isPathMorphing(el)) {
+                morphingFromEl = el;
+            }
+        }
+        if (elNeedRecreate) {
+            // Should keep at the original index, otherwise "merge by index" will be incorrect.
+            toBeReplacedIdx = group.childrenRef().indexOf(el);
+            el = null;
+        }
     }
 
-    let isInit = !el;
-    let needsMorphing = false;
+    const elIsNewCreated = !el;
 
     if (!el) {
         el = createEl(elOption);
@@ -1626,13 +1719,13 @@ function doCreateOrUpdateEl(
         el.clearStates();
     }
 
-    // Do shape morphing
-    if ((elOption as CustomZRPathOption).morph && el && oldEl && (el !== oldEl)) {
-        needsMorphing = true;
-        // Use update animation when morph is enabled.
-        isInit = false;
+    if (morphingFroms && optionMorph) {
+        morphingFromEl = morphingFroms;
     }
 
+    // Use update animation when morph is enabled.
+    const isInit = elIsNewCreated && !morphingFromEl;
+
     attachedTxInfoTmp.normal.cfg = attachedTxInfoTmp.normal.conOpt =
         attachedTxInfoTmp.emphasis.cfg = attachedTxInfoTmp.emphasis.conOpt =
         attachedTxInfoTmp.blur.cfg = attachedTxInfoTmp.blur.conOpt =
@@ -1650,7 +1743,7 @@ function doCreateOrUpdateEl(
 
     updateElNormal(
         el,
-        needsMorphing ? oldEl : null,
+        morphingFromEl,
         dataIndex,
         elOption,
         elOption.style,
@@ -1945,7 +2038,8 @@ function mergeChildren(
             newChildren[index],
             seriesModel,
             el,
-            false
+            false,
+            null
         );
     }
     for (let i = el.childCount() - 1; i >= index; i--) {
@@ -1997,7 +2091,8 @@ function processAddUpdate(
         childOption,
         context.seriesModel,
         context.group,
-        false
+        false,
+        null
     );
 }
 
@@ -2036,3 +2131,10 @@ function hasOwnPathData(shape: CustomSVGPathOption['shape']): boolean {
     return shape && (hasOwn(shape, 'pathData') || hasOwn(shape, 'd'));
 }
 
+function elCanMorph(el: Element): el is graphicUtil.Path {
+    return el && el instanceof graphicUtil.Path;
+}
+
+function removeElementDirectly(el: Element, group: ViewRootGroup): void {
+    el && group.remove(el);
+}
diff --git a/src/coord/scaleRawExtentInfo.ts b/src/coord/scaleRawExtentInfo.ts
index 9f90b1a..7b3c92d 100644
--- a/src/coord/scaleRawExtentInfo.ts
+++ b/src/coord/scaleRawExtentInfo.ts
@@ -286,12 +286,12 @@ const DATA_MIN_MAX_ATTR = { min: '_dataMin', max: '_dataMax' } as const;
  * Scale extent info probably be required multiple times during a workflow.
  * For example:
  * (1) `dataZoom` depends it to get the axis extent in "100%" state.
- * (2) `processor/extentCalculator` depends it to make sure whethe axis extent is specified.
+ * (2) `processor/extentCalculator` depends it to make sure whether axis extent is specified.
  * (3) `coordSys.update` use it to finally decide the scale extent.
- * But the callback of `min`/`max` should not be called multiple time.
- * The code should not be duplicated either.
- * So we cache the result in the scale instance, which will be recreated in the begining
- * of the workflow.
+ * But the callback of `min`/`max` should not be called multiple times.
+ * The code below should not be implemented repeatedly either.
+ * So we cache the result in the scale instance, which will be recreated at the begining
+ * of the workflow (because `scale` instance will be recreated each round of the workflow).
  */
 export function ensureScaleRawExtentInfo(
     scale: Scale,
diff --git a/src/data/DataDiffer.ts b/src/data/DataDiffer.ts
index 993ead1..43baad1 100644
--- a/src/data/DataDiffer.ts
+++ b/src/data/DataDiffer.ts
@@ -20,28 +20,51 @@
 import {ArrayLike} from 'zrender/src/core/types';
 
 // return key.
-export type DiffKeyGetter = (this: DataDiffer, value: unknown, index: number) => string;
-export type DiffCallbackAdd = (newIndex: number) => void;
-export type DiffCallbackUpdate = (newIndex: number, oldIndex: number) => void;
-export type DiffCallbackRemove = (oldIndex: number) => void;
+type DiffKeyGetter<CTX = unknown> =
+    (this: DataDiffer<CTX>, value: unknown, index: number) => string;
 
+type DiffCallbackAdd = (newIndex: number) => void;
+type DiffCallbackUpdate = (newIndex: number, oldIndex: number) => void;
+type DiffCallbackRemove = (oldIndex: number) => void;
+type DiffCallbackUpdateManyToOne = (newIndex: number, oldIndex: number[]) => void;
+type DiffCallbackUpdateOneToMany = (newIndex: number[], oldIndex: number) => void;
+
+/**
+ * The value of `DataIndexMap` can only be:
+ * + a number
+ * + a number[] that length >= 2.
+ * + null/undefined
+ */
 type DataIndexMap = {[key: string]: number | number[]};
 
+function dataIndexMapValueLength(
+    valNumOrArrLengthMoreThan2: number | number[]
+): number {
+    return valNumOrArrLengthMoreThan2 == null
+        ? 0
+        : ((valNumOrArrLengthMoreThan2 as number[]).length || 1);
+}
+
 function defaultKeyGetter(item: string): string {
     return item;
 }
 
-class DataDiffer<Ctx = unknown> {
+export type DataDiffCallbackMode = 'single' | 'multiple';
+
+class DataDiffer<CTX = unknown> {
 
     private _old: ArrayLike<unknown>;
     private _new: ArrayLike<unknown>;
-    private _oldKeyGetter: DiffKeyGetter;
-    private _newKeyGetter: DiffKeyGetter;
+    private _oldKeyGetter: DiffKeyGetter<CTX>;
+    private _newKeyGetter: DiffKeyGetter<CTX>;
     private _add: DiffCallbackAdd;
     private _update: DiffCallbackUpdate;
+    private _updateManyToOne: DiffCallbackUpdateManyToOne;
+    private _updateOneToMany: DiffCallbackUpdateOneToMany;
     private _remove: DiffCallbackRemove;
+    private _cbModeMultiple: boolean;
 
-    readonly context: Ctx;
+    readonly context: CTX;
 
     /**
      * @param context Can be visited by this.context in callback.
@@ -49,9 +72,10 @@ class DataDiffer<Ctx = unknown> {
     constructor(
         oldArr: ArrayLike<unknown>,
         newArr: ArrayLike<unknown>,
-        oldKeyGetter?: DiffKeyGetter,
-        newKeyGetter?: DiffKeyGetter,
-        context?: Ctx
+        oldKeyGetter?: DiffKeyGetter<CTX>,
+        newKeyGetter?: DiffKeyGetter<CTX>,
+        context?: CTX,
+        cbMode?: DataDiffCallbackMode
     ) {
         this._old = oldArr;
         this._new = newArr;
@@ -61,12 +85,14 @@ class DataDiffer<Ctx = unknown> {
 
         // Visible in callback via `this.context`;
         this.context = context;
+
+        this._cbModeMultiple = cbMode === 'multiple';
     }
 
     /**
      * Callback function when add a data
      */
-    add(func: DiffCallbackAdd): DataDiffer {
+    add(func: DiffCallbackAdd): this {
         this._add = func;
         return this;
     }
@@ -74,94 +100,200 @@ class DataDiffer<Ctx = unknown> {
     /**
      * Callback function when update a data
      */
-    update(func: DiffCallbackUpdate): DataDiffer {
+    update(func: DiffCallbackUpdate): this {
         this._update = func;
         return this;
     }
 
     /**
+     * Callback function when update a data and only work in `cbMode: 'byKey'`.
+     */
+    updateManyToOne(func: DiffCallbackUpdateManyToOne): this {
+        this._updateManyToOne = func;
+        return this;
+    }
+
+    /**
+     * Callback function when update a data and only work in `cbMode: 'byKey'`.
+     */
+    updateOneToMany(func: DiffCallbackUpdateOneToMany): this {
+        this._updateOneToMany = func;
+        return this;
+    }
+
+    /**
      * Callback function when remove a data
      */
-    remove(func: DiffCallbackRemove): DataDiffer {
+    remove(func: DiffCallbackRemove): this {
         this._remove = func;
         return this;
     }
 
     execute(): void {
+        this[this._cbModeMultiple ? '_executeByKey' : '_executeByIndex']();
+    }
+
+    private _executeByIndex(): void {
         const oldArr = this._old;
         const newArr = this._new;
-
-        const oldDataIndexMap: DataIndexMap = {};
         const newDataIndexMap: DataIndexMap = {};
-        const oldDataKeyArr: string[] = [];
-        const newDataKeyArr: string[] = [];
-        let i;
+        const oldDataKeyArr: string[] = new Array(oldArr.length);
+        const newDataKeyArr: string[] = new Array(newArr.length);
 
-        this._initIndexMap(oldArr, oldDataIndexMap, oldDataKeyArr, '_oldKeyGetter');
+        this._initIndexMap(oldArr, null, oldDataKeyArr, '_oldKeyGetter');
         this._initIndexMap(newArr, newDataIndexMap, newDataKeyArr, '_newKeyGetter');
 
-        for (i = 0; i < oldArr.length; i++) {
-            const key = oldDataKeyArr[i];
-            let idx = newDataIndexMap[key];
+        for (let i = 0; i < oldArr.length; i++) {
+            const oldKey = oldDataKeyArr[i];
+            const newIdxMapVal = newDataIndexMap[oldKey];
+            const newIdxMapValLen = dataIndexMapValueLength(newIdxMapVal);
 
             // idx can never be empty array here. see 'set null' logic below.
-            if (idx != null) {
+            if (newIdxMapValLen > 1) {
                 // Consider there is duplicate key (for example, use dataItem.name as key).
                 // We should make sure every item in newArr and oldArr can be visited.
-                const len = (idx as number[]).length;
-                if (len) {
-                    len === 1 && (newDataIndexMap[key] = null);
-                    idx = (idx as number[]).shift();
-                }
-                else {
-                    newDataIndexMap[key] = null;
+                const newIdx = (newIdxMapVal as number[]).shift();
+                if ((newIdxMapVal as number[]).length === 1) {
+                    newDataIndexMap[oldKey] = (newIdxMapVal as number[])[0];
                 }
-                this._update && this._update(idx as number, i);
+                this._update && this._update(newIdx as number, i);
+            }
+            else if (newIdxMapValLen === 1) {
+                newDataIndexMap[oldKey] = null;
+                this._update && this._update(newIdxMapVal as number, i);
             }
             else {
                 this._remove && this._remove(i);
             }
         }
 
-        for (i = 0; i < newDataKeyArr.length; i++) {
-            const key = newDataKeyArr[i];
-            if (newDataIndexMap.hasOwnProperty(key)) {
-                const idx = newDataIndexMap[key];
-                if (idx == null) {
-                    continue;
-                }
-                // idx can never be empty array here. see 'set null' logic above.
-                if (!(idx as number[]).length) {
-                    this._add && this._add(idx as number);
+        this._performRestAdd(newDataKeyArr, newDataIndexMap);
+    }
+
+    /**
+     * For example, consider the case:
+     * oldData: [o0, o1, o2, o3, o4, o5, o6, o7],
+     * newData: [n0, n1, n2, n3, n4, n5, n6, n7, n8],
+     * Where:
+     *     o0, o1, n0 has key 'a' (many to one)
+     *     o5, n4, n5, n6 has key 'b' (one to many)
+     *     o2, n1 has key 'c' (one to one)
+     *     n2, n3 has key 'd' (add)
+     *     o3, o4 has key 'e' (remove)
+     *     o6, o7, n7, n8 has key 'f' (many to many, treated as add and remove)
+     * Then:
+     *     (The order of the following directives are not ensured.)
+     *     this._updateManyToOne(n0, [o0, o1]);
+     *     this._updateOneToMany([n4, n5, n6], o5);
+     *     this._update(n1, o2);
+     *     this._remove(o3);
+     *     this._remove(o4);
+     *     this._remove(o6);
+     *     this._remove(o7);
+     *     this._add(n2);
+     *     this._add(n3);
+     *     this._add(n7);
+     *     this._add(n8);
+     */
+    private _executeByKey(): void {
+        const oldArr = this._old;
+        const newArr = this._new;
+        const oldDataIndexMap: DataIndexMap = {};
+        const newDataIndexMap: DataIndexMap = {};
+        const oldDataKeyArr: string[] = [];
+        const newDataKeyArr: string[] = [];
+
+        this._initIndexMap(oldArr, oldDataIndexMap, oldDataKeyArr, '_oldKeyGetter');
+        this._initIndexMap(newArr, newDataIndexMap, newDataKeyArr, '_newKeyGetter');
+
+        for (let i = 0; i < oldDataKeyArr.length; i++) {
+            const oldKey = oldDataKeyArr[i];
+            const oldIdxMapVal = oldDataIndexMap[oldKey];
+            const newIdxMapVal = newDataIndexMap[oldKey];
+            const oldIdxMapValLen = dataIndexMapValueLength(oldIdxMapVal);
+            const newIdxMapValLen = dataIndexMapValueLength(newIdxMapVal);
+
+            if (oldIdxMapValLen > 1 && newIdxMapValLen === 1) {
+                this._updateManyToOne && this._updateManyToOne(newIdxMapVal as number, oldIdxMapVal as number[]);
+                newDataIndexMap[oldKey] = null;
+            }
+            else if (oldIdxMapValLen === 1 && newIdxMapValLen > 1) {
+                this._updateOneToMany && this._updateOneToMany(newIdxMapVal as number[], oldIdxMapVal as number);
+                newDataIndexMap[oldKey] = null;
+            }
+            else if (oldIdxMapValLen === 1 && newIdxMapValLen === 1) {
+                this._update && this._update(newIdxMapVal as number, oldIdxMapVal as number);
+                newDataIndexMap[oldKey] = null;
+            }
+            else if (oldIdxMapValLen > 1) {
+                for (let i = 0; i < oldIdxMapValLen; i++) {
+                    this._remove && this._remove((oldIdxMapVal as number[])[i]);
                 }
-                else {
-                    for (let j = 0, len = (idx as number[]).length; j < len; j++) {
-                        this._add && this._add((idx as number[])[j]);
-                    }
+            }
+            else {
+                this._remove && this._remove(oldIdxMapVal as number);
+            }
+        }
+
+        this._performRestAdd(newDataKeyArr, newDataIndexMap);
+    }
+
+    private _performRestAdd(newDataKeyArr: string[], newDataIndexMap: DataIndexMap) {
+        for (let i = 0; i < newDataKeyArr.length; i++) {
+            const newKey = newDataKeyArr[i];
+            const newIdxMapVal = newDataIndexMap[newKey];
+            const idxMapValLen = dataIndexMapValueLength(newIdxMapVal);
+            if (idxMapValLen > 1) {
+                for (let j = 0; j < idxMapValLen; j++) {
+                    this._add && this._add((newIdxMapVal as number[])[j]);
                 }
             }
+            else if (idxMapValLen === 1) {
+                this._add && this._add(newIdxMapVal as number);
+            }
+            // Support both `newDataKeyArr` are duplication removed or not removed.
+            newDataIndexMap[newKey] = null;
         }
     }
 
     private _initIndexMap(
         arr: ArrayLike<unknown>,
+        // Can be null.
         map: DataIndexMap,
+        // In 'byKey', the output `keyArr` is duplication removed.
+        // In 'byIndex', the output `keyArr` is not duplication removed and
+        //     its indices are accurately corresponding to `arr`.
         keyArr: string[],
         keyGetterName: '_oldKeyGetter' | '_newKeyGetter'
     ): void {
+        const cbModeByKey = this._cbModeMultiple;
+
         for (let i = 0; i < arr.length; i++) {
             // Add prefix to avoid conflict with Object.prototype.
             const key = '_ec_' + this[keyGetterName](arr[i], i);
-            let existence = map[key];
-            if (existence == null) {
-                keyArr.push(key);
+            if (!cbModeByKey) {
+                keyArr[i] = key;
+            }
+            if (!map) {
+                continue;
+            }
+
+            const idxMapVal = map[key];
+            const idxMapValLen = dataIndexMapValueLength(idxMapVal);
+
+            if (idxMapValLen === 0) {
+                // Simple optimize: in most cases, one index has one key,
+                // do not need array.
                 map[key] = i;
+                if (cbModeByKey) {
+                    keyArr.push(key);
+                }
+            }
+            else if (idxMapValLen === 1) {
+                map[key] = [idxMapVal as number, i];
             }
             else {
-                if (!(existence as number[]).length) {
-                    map[key] = existence = [existence as number];
-                }
-                (existence as number[]).push(i);
+                (idxMapVal as number[]).push(i);
             }
         }
     }
diff --git a/src/echarts.ts b/src/echarts.ts
index dbe4378..c27a65b 100644
--- a/src/echarts.ts
+++ b/src/echarts.ts
@@ -88,14 +88,15 @@ import {
     ComponentMainType,
     ComponentSubType,
     ColorString,
-    SelectChangedPayload
+    SelectChangedPayload,
+    DimensionLoose
 } from './util/types';
 import Displayable from 'zrender/src/graphic/Displayable';
 import IncrementalDisplayable from 'zrender/src/graphic/IncrementalDisplayable';
 import { seriesSymbolTask, dataSymbolTask } from './visual/symbol';
 import { getVisualFromData, getItemVisualFromData } from './visual/helper';
 import LabelManager from './label/LabelManager';
-import { deprecateLog } from './util/log';
+import { deprecateLog, throwError } from './util/log';
 import { handleLegacySelectEvents } from './legacy/dataSelectAction';
 
 // At least canvas renderer.
@@ -185,9 +186,19 @@ interface SetOptionOpts {
     silent?: boolean;
     // Rule: only `id` mapped will be merged,
     // other components of the certain `mainType` will be removed.
-    replaceMerge?: GlobalModelSetOptionOpts['replaceMerge']
+    replaceMerge?: GlobalModelSetOptionOpts['replaceMerge'];
+    transition?: SetOptionTransitionOptItem | SetOptionTransitionOptItem[];
 };
 
+interface SetOptionTransitionOptItem {
+    from: SetOptionTransitionOptFinder | DimensionLoose;
+    to: SetOptionTransitionOptFinder | DimensionLoose;
+}
+interface SetOptionTransitionOptFinder extends modelUtil.ModelFinderObject {
+    dimension: DimensionLoose;
+}
+
+
 type EventMethodName = 'on' | 'off';
 function createRegisterEventWithLowercaseECharts(method: EventMethodName) {
     return function (this: ECharts, ...args: any): ECharts {
@@ -254,6 +265,10 @@ let renderSeries: (
 let performPostUpdateFuncs: (ecModel: GlobalModel, api: ExtensionAPI) => void;
 let createExtensionAPI: (ecIns: ECharts) => ExtensionAPI;
 let enableConnect: (ecIns: ECharts) => void;
+let setTransitionOpt: (
+    chart: ECharts,
+    transitionOpt: SetOptionTransitionOptItem | SetOptionTransitionOptItem[]
+) => void;
 
 let markStatusToUpdate: (ecIns: ECharts) => void;
 let applyChangedStates: (ecIns: ECharts) => void;
@@ -309,6 +324,7 @@ class ECharts extends Eventful {
 
     private _labelManager: LabelManager;
 
+
     private [OPTION_UPDATED_KEY]: boolean | {silent: boolean};
     private [IN_MAIN_PROCESS_KEY]: boolean;
     private [CONNECT_STATUS_KEY]: ConnectStatus;
@@ -502,10 +518,12 @@ class ECharts extends Eventful {
 
         let silent;
         let replaceMerge;
+        let transitionOpt;
         if (isObject(notMerge)) {
             lazyUpdate = notMerge.lazyUpdate;
             silent = notMerge.silent;
             replaceMerge = notMerge.replaceMerge;
+            transitionOpt = notMerge.transition;
             notMerge = notMerge.notMerge;
         }
 
@@ -519,7 +537,9 @@ class ECharts extends Eventful {
             ecModel.init(null, null, null, theme, this._locale, optionManager);
         }
 
-        this._model.setOption(option as ECOption, {replaceMerge: replaceMerge}, optionPreprocessorFuncs);
+        this._model.setOption(option as ECOption, { replaceMerge }, optionPreprocessorFuncs);
+
+        setTransitionOpt(this, transitionOpt);
 
         if (lazyUpdate) {
             this[OPTION_UPDATED_KEY] = {silent: silent};
@@ -852,7 +872,7 @@ class ECharts extends Eventful {
         const ecModel = this._model;
 
         const parsedFinder = modelUtil.parseFinder(ecModel, finder, {
-            defaultMainType: 'series'
+            useDefaultMainType: ['series']
         });
 
         const seriesModel = parsedFinder.seriesModel;
@@ -1926,6 +1946,8 @@ class ECharts extends Eventful {
                     unfinished = true;
                 }
 
+                seriesModel.__transientTransitionOpt = null;
+
                 chartView.group.silent = !!seriesModel.get('silent');
                 // Should not call markRedraw on group, because it will disable zrender
                 // increamental render (alway render from the __startIndex each frame)
@@ -2260,6 +2282,70 @@ class ECharts extends Eventful {
             });
         };
 
+        setTransitionOpt = function (
+            chart: ECharts,
+            transitionOpt: SetOptionTransitionOptItem | SetOptionTransitionOptItem[]
+        ): void {
+            const ecModel = chart._model;
+            zrUtil.each(modelUtil.normalizeToArray(transitionOpt), transOpt => {
+
+                function normalizeFromTo(fromTo: DimensionLoose | SetOptionTransitionOptFinder) {
+                    return (zrUtil.isString(fromTo) || zrUtil.isNumber(fromTo))
+                        ? { dimension: fromTo }
+                        : fromTo;
+                }
+
+                let errMsg;
+                const fromOpt = normalizeFromTo(transOpt.from);
+                const toOpt = normalizeFromTo(transOpt.to);
+
+                if (fromOpt == null || toOpt == null) {
+                    if (__DEV__) {
+                        errMsg = '`transition.from` and `transition.to` must be specified.';
+                    }
+                    throwError(errMsg);
+                }
+
+                const finderOpt = {
+                    useDefaultMainType: ['series'],
+                    includeMainTypes: ['series'],
+                    enableAll: false,
+                    enableNone: false
+                };
+                const fromResult = modelUtil.parseFinder(ecModel, fromOpt, finderOpt);
+                const toResult = modelUtil.parseFinder(ecModel, toOpt, finderOpt);
+                const toSeries = toResult.seriesModel;
+
+                if (toSeries == null) {
+                    errMsg = '';
+                    if (__DEV__) {
+                        errMsg = '`transition` is only supported on series.';
+                    }
+                }
+                if (fromResult.seriesModel !== toSeries) {
+                    errMsg = '';
+                    if (__DEV__) {
+                        errMsg = '`transition.from` and `transition.to` must be specified to the same series.';
+                    }
+                }
+                if (fromOpt.dimension == null || toOpt.dimension == null) {
+                    errMsg = '';
+                    if (__DEV__) {
+                        errMsg = '`dimension` must be specified in `transition`.';
+                    }
+                }
+                if (errMsg != null) {
+                    throwError(errMsg);
+                }
+
+                // Just a temp solution: mount them on series.
+                toSeries.__transientTransitionOpt = {
+                    from: fromOpt.dimension,
+                    to: toOpt.dimension
+                };
+            });
+        };
+
     })();
 }
 
diff --git a/src/model/Global.ts b/src/model/Global.ts
index b1f5dfb..ec7927c 100644
--- a/src/model/Global.ts
+++ b/src/model/Global.ts
@@ -141,7 +141,7 @@ class GlobalModel extends Model<ECUnitOption> {
             'please use chart.getOption()'
         );
 
-        const innerOpt = normalizeReplaceMergeInput(opts);
+        const innerOpt = normalizeSetOptionInput(opts);
 
         this._optionManager.setOption(option, optionPreprocessorFuncs, innerOpt);
 
@@ -157,9 +157,9 @@ class GlobalModel extends Model<ECUnitOption> {
      */
     resetOption(
         type: 'recreate' | 'timeline' | 'media',
-        opt?: GlobalModelSetOptionOpts
+        opt?: Pick<GlobalModelSetOptionOpts, 'replaceMerge'>
     ): boolean {
-        return this._resetOption(type, normalizeReplaceMergeInput(opt));
+        return this._resetOption(type, normalizeSetOptionInput(opt));
     }
 
     private _resetOption(
@@ -933,7 +933,7 @@ function filterBySubType(
         : components;
 }
 
-function normalizeReplaceMergeInput(opts: GlobalModelSetOptionOpts): InnerSetOptionOpts {
+function normalizeSetOptionInput(opts: GlobalModelSetOptionOpts): InnerSetOptionOpts {
     const replaceMergeMainTypeMap = createHashMap<boolean, string>();
     opts && each(modelUtil.normalizeToArray(opts.replaceMerge), function (mainType) {
         if (__DEV__) {
diff --git a/src/model/Series.ts b/src/model/Series.ts
index a082994..3a0f774 100644
--- a/src/model/Series.ts
+++ b/src/model/Series.ts
@@ -23,7 +23,7 @@ import * as modelUtil from '../util/model';
 import {
     DataHost, DimensionName, StageHandlerProgressParams,
     SeriesOption, ZRColor, BoxLayoutOptionMixin,
-    ScaleDataValue, Dictionary, OptionDataItemObject, SeriesDataType
+    ScaleDataValue, Dictionary, OptionDataItemObject, SeriesDataType, DimensionLoose
 } from '../util/types';
 import ComponentModel, { ComponentModelConstructor } from './Component';
 import {ColorPaletteMixin} from './mixin/colorPalette';
@@ -130,6 +130,15 @@ class SeriesModel<Opt extends SeriesOption = SeriesOption> extends ComponentMode
     // Injected outside
     pipelineContext: PipelineContext;
 
+    // only avalible in `render()` caused by `setOption`.
+    __transientTransitionOpt: {
+        // [MEMO] Currently only support single "from". If intending to
+        // support multiple "from", if not hard to implement "merge morph",
+        // but correspondingly not easy to implement "split morph".
+        from: DimensionLoose;
+        to: DimensionLoose;
+    };
+
     // ---------------------------------------
     // Props to tell visual/style.ts about how to do visual encoding.
     // ---------------------------------------
diff --git a/src/util/model.ts b/src/util/model.ts
index c09a154..d7e46e8 100644
--- a/src/util/model.ts
+++ b/src/util/model.ts
@@ -27,7 +27,9 @@ import {
     assert,
     isString,
     indexOf,
-    isStringSafe
+    isStringSafe,
+    hasOwn,
+    defaults
 } from 'zrender/src/core/util';
 import env from 'zrender/src/core/env';
 import GlobalModel from '../model/Global';
@@ -750,6 +752,7 @@ let innerUniqueIndex = getRandomIdBase();
 export type ModelFinderIndexQuery = number | number[] | 'all' | 'none' | false;
 export type ModelFinderIdQuery = OptionId | OptionId[];
 export type ModelFinderNameQuery = OptionId | OptionId[];
+// If string, like 'series', means { seriesIndex: 0 }.
 export type ModelFinder = string | ModelFinderObject;
 export type ModelFinderObject = {
     seriesIndex?: ModelFinderIndexQuery, seriesId?: ModelFinderIdQuery, seriesName?: ModelFinderNameQuery
@@ -793,7 +796,13 @@ export type ParsedModelFinder = ParsedModelFinderKnown & {
 export function parseFinder(
     ecModel: GlobalModel,
     finderInput: ModelFinder,
-    opt?: {defaultMainType?: ComponentMainType, includeMainTypes?: ComponentMainType[]}
+    opt?: {
+        includeMainTypes?: ComponentMainType[];
+        // The `mainType` listed will set `useDefault: true`.
+        useDefaultMainType?: ComponentMainType[];
+        enableAll?: boolean;
+        enableNone?: boolean;
+    }
 ): ParsedModelFinder {
     let finder: ModelFinderObject;
     if (isString(finderInput)) {
@@ -805,7 +814,6 @@ export function parseFinder(
         finder = finderInput;
     }
 
-    const defaultMainType = opt ? opt.defaultMainType : null;
     const queryOptionMap = createHashMap<QueryReferringUserOption, ComponentMainType>();
     const result = {} as ParsedModelFinder;
 
@@ -823,7 +831,6 @@ export function parseFinder(
         if (
             !mainType
             || !queryType
-            || (mainType !== defaultMainType && value == null)
             || (opt && opt.includeMainTypes && indexOf(opt.includeMainTypes, mainType) < 0)
         ) {
             return;
@@ -833,15 +840,20 @@ export function parseFinder(
         queryOption[queryType] = value as any;
     });
 
+    const useDefaultMainType = opt && opt.useDefaultMainType || [];
+    each(useDefaultMainType, function (mainType) {
+        !queryOptionMap.get(mainType) && queryOptionMap.set(mainType, {});
+    });
+
     queryOptionMap.each(function (queryOption, mainType) {
         const queryResult = queryReferringComponents(
             ecModel,
             mainType,
             queryOption,
             {
-                useDefault: mainType === defaultMainType,
-                enableAll: true,
-                enableNone: true
+                useDefault: indexOf(useDefaultMainType, mainType) >= 0,
+                enableAll: (opt && opt.enableAll != null) ? opt.enableAll : true,
+                enableNone: (opt && opt.enableNone != null) ? opt.enableNone : true
             }
         );
         result[mainType + 'Models'] = queryResult.models;
diff --git a/src/util/types.ts b/src/util/types.ts
index d57e6b3..dbe4d21 100644
--- a/src/util/types.ts
+++ b/src/util/types.ts
@@ -46,6 +46,7 @@ import { PathStyleProps } from 'zrender/src/graphic/Path';
 import { ImageStyleProps } from 'zrender/src/graphic/Image';
 import ZRText, { TextStyleProps } from 'zrender/src/graphic/Text';
 import { Source } from '../data/Source';
+import { ModelFinderObject } from './model';
 
 
 
@@ -237,13 +238,42 @@ export interface StageHandlerOverallReset {
     (ecModel: GlobalModel, api: ExtensionAPI, payload?: Payload): void
 }
 export interface StageHandler {
-    seriesType?: string;
+    /**
+     * Indicate that the task will be only piped all series
+     * (`performRawSeries` indicate whether includes filtered series).
+     */
     createOnAllSeries?: boolean;
+    /**
+     * Indicate that the task will be only piped in the pipeline of this type of series.
+     * (`performRawSeries` indicate whether includes filtered series).
+     */
+    seriesType?: string;
+    /**
+     * Indicate that the task will be only piped in the pipeline of the returned series.
+     */
+    getTargetSeries?: (ecModel: GlobalModel, api: ExtensionAPI) => HashMap<SeriesModel>;
+
+    /**
+     * If `true`, filtered series will also be "performed".
+     */
     performRawSeries?: boolean;
+
+    /**
+     * Called only when this task in a pipeline.
+     */
     plan?: StageHandlerPlan;
+    /**
+     * If `overallReset` specified, an "overall task" will be created.
+     * "overall task" does not belong to a certain pipeline.
+     * They always be "performed" in certain phase (depends on when they declared).
+     * They has "stub"s to connect with pipelines (one stub for one pipeline),
+     * delivering info like "dirty" and "output end".
+     */
     overallReset?: StageHandlerOverallReset;
+    /**
+     * Called only when this task in a pipeline, and "dirty".
+     */
     reset?: StageHandlerReset;
-    getTargetSeries?: (ecModel: GlobalModel, api: ExtensionAPI) => HashMap<SeriesModel>;
 }
 
 export interface StageHandlerInternal extends StageHandler {
diff --git a/test/custom-shape-morphing2.html b/test/custom-shape-morphing2.html
new file mode 100644
index 0000000..01e8bc2
--- /dev/null
+++ b/test/custom-shape-morphing2.html
@@ -0,0 +1,388 @@
+<!DOCTYPE html>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<html>
+    <head>
+        <meta charset='utf-8'>
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <script src='lib/esl.js'></script>
+        <script src='lib/config.js'></script>
+        <script src='lib/jquery.min.js'></script>
+        <script src="lib/testHelper.js"></script>
+        <link rel="stylesheet" href="lib/reset.css" />
+    </head>
+    <body>
+        <style>
+        </style>
+
+        <div id="main0"></div>
+
+
+
+        <script>
+
+            require([
+                'echarts'
+            ], function (echarts) {
+
+                const COLORS = [
+                    '#37A2DA', '#e06343', '#37a354', '#b55dba', '#b5bd48', '#8378EA', '#96BFFF'
+                ];
+                var COUNT = 50;
+                var CONTENT_COLOR = '#37A2DA';
+
+                function makeRandomValue(range, precision) {
+                    return +(
+                        Math.random() * (range[1] - range[0]) + range[0]
+                    ).toFixed(precision);
+                }
+
+                var M_TAG_LIST = ['MA', 'MB', 'MC', 'MD'];
+                var Z_TAG_LIST = ['ZA', 'ZB', 'ZC', 'ZD', 'ZE'];
+                var ANIMATION_DURATION_UPDATE = 1500;
+
+                function initRawData() {
+                    var DIMENSION = {
+                        DATE: 0,
+                        ATA: 1,
+                        STE: 2,
+                        CTZ: 3,
+                        M_TAG: 4,
+                        Z_TAG: 5,
+                        ID: 6
+                    };
+                    var data = [];
+                    var currDate = +new Date(2015, 2, 1);
+                    var ONE_DAY = 3600 * 24 * 1000;
+                    for (var i = 0; i < COUNT; i++, currDate += ONE_DAY) {
+                        var line = [];
+                        data.push(line);
+                        line[DIMENSION.DATE] = currDate;
+                        line[DIMENSION.ATA] = makeRandomValue([10, 40], 0);
+                        line[DIMENSION.STE] = makeRandomValue([0.01, 0.99], 2);
+                        line[DIMENSION.CTZ] = makeRandomValue([1, 10], 1);
+                        line[DIMENSION.M_TAG] = M_TAG_LIST[makeRandomValue([0, M_TAG_LIST.length - 1], 0)];
+                        line[DIMENSION.Z_TAG] = Z_TAG_LIST[makeRandomValue([0, Z_TAG_LIST.length - 1], 0)];
+                        line[DIMENSION.ID] = 'P' + i;
+                    }
+                    return {
+                        DIMENSION: DIMENSION,
+                        data: data
+                    };
+                }
+                function aggregateSum(rawDataWrap, byDimProp, RESULT_DIMENSION) {
+                    var map = {};
+                    var result = [];
+                    var data = rawDataWrap.data;
+
+                    for (var i = 0; i < data.length; i++) {
+                        var line = data[i];
+                        var byVal = line[rawDataWrap.DIMENSION[byDimProp]];
+                        if (!map.hasOwnProperty(byVal)) {
+                            var newLine = [];
+                            map[byVal] = newLine;
+                            result.push(newLine);
+                            newLine[RESULT_DIMENSION.ATA] = 0;
+                            newLine[RESULT_DIMENSION.STE] = 0;
+                            newLine[RESULT_DIMENSION.CTZ] = 0;
+                            newLine[RESULT_DIMENSION[byDimProp]] = byVal;
+                        }
+                        else {
+                            var newLine = map[byVal];
+                            newLine[RESULT_DIMENSION.ATA] += line[rawDataWrap.DIMENSION.ATA];
+                            newLine[RESULT_DIMENSION.STE] += line[rawDataWrap.DIMENSION.STE];
+                            newLine[RESULT_DIMENSION.CTZ] += line[rawDataWrap.DIMENSION.CTZ];
+                        }
+                    }
+
+                    return {
+                        DIMENSION: RESULT_DIMENSION,
+                        data: result,
+                        uniqueKey: 'M_TAG'
+                    };
+                }
+
+                var rawDataWrap = initRawData();
+                // console.log(JSON.stringify(rawDataWrap.data));
+                rawDataWrap.data = [[1425139200000,34,0.13,2,"MD","ZD","P0"],[1425225600000,28,0.71,1.5,"MB","ZD","P1"],[1425312000000,23,0.9,2.8,"MA","ZC","P2"],[1425398400000,21,0.58,6,"MB","ZC","P3"],[1425484800000,14,0.1,1.6,"MC","ZA","P4"],[1425571200000,21,0.6,7.7,"MC","ZA","P5"],[1425657600000,23,0.31,2.6,"MC","ZC","P6"],[1425744000000,34,0.74,2.4,"MD","ZE","P7"],[1425830400000,14,0.59,2.3,"MB","ZD","P8"],[1425916800000,18,0.85,5.1,"MB","ZB","P9"],[1426003200000,36,0.96,1.2,"MC"," [...]
+                var mTagSumDataWrap = aggregateSum(rawDataWrap, 'M_TAG', {
+                    ATA: 0,
+                    STE: 1,
+                    CTZ: 2,
+                    M_TAG: 3
+                });
+                var zTagSumDataWrap = aggregateSum(rawDataWrap, 'Z_TAG', {
+                    ATA: 0,
+                    STE: 1,
+                    CTZ: 2,
+                    Z_TAG: 3
+                });
+
+
+                function create_Scatter_ATA_STE() {
+                    var option = {
+                        tooltip: {},
+                        grid: {
+                            containLabel: true
+                        },
+                        xAxis: {
+                            name: 'STE'
+                        },
+                        yAxis: {
+                            name: 'ATA'
+                        },
+                        dataZoom: [{
+                            type: 'slider',
+                        }, {
+                            type: 'inside'
+                        }],
+                        series: {
+                            type: 'custom',
+                            coordinateSystem: 'cartesian2d',
+                            animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                            data: rawDataWrap.data,
+                            encode: {
+                                itemName: rawDataWrap.DIMENSION.ID,
+                                x: rawDataWrap.DIMENSION.STE,
+                                y: rawDataWrap.DIMENSION.ATA,
+                                tooltip: [rawDataWrap.DIMENSION.STE, rawDataWrap.DIMENSION.ATA]
+                            },
+                            renderItem: function (params, api) {
+                                var pos = api.coord([
+                                    api.value(rawDataWrap.DIMENSION.STE),
+                                    api.value(rawDataWrap.DIMENSION.ATA)
+                                ]);
+                                return {
+                                    type: 'circle',
+                                    // x: pos[0],
+                                    // y: pos[1],
+                                    morph: true,
+                                    shape: {
+                                        // cx: 0,
+                                        // cy: 0,
+                                        cx: pos[0],
+                                        cy: pos[1],
+                                        r: 10,
+                                        transition: ['cx', 'cy', 'r']
+                                    },
+                                    style: {
+                                        transition: 'lineWidth',
+                                        fill: CONTENT_COLOR,
+                                        stroke: '#555',
+                                        lineWidth: 1
+                                    }
+                                };
+                            }
+                        }
+                    };
+
+                    return {
+                        option: option,
+                        dataWrap: rawDataWrap
+                    };
+                }
+
+                function create_Bar_mSum_ATA(mTagSumDataWrap) {
+                    var option = {
+                        tooltip: {},
+                        grid: {
+                            containLabel: true
+                        },
+                        xAxis: {
+                            type: 'category'
+                        },
+                        yAxis: {
+                        },
+                        dataZoom: [{
+                            type: 'slider',
+                        }, {
+                            type: 'inside'
+                        }],
+                        series: {
+                            type: 'custom',
+                            coordinateSystem: 'cartesian2d',
+                            animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                            data: mTagSumDataWrap.data,
+                            encode: {
+                                x: mTagSumDataWrap.DIMENSION.M_TAG,
+                                y: mTagSumDataWrap.DIMENSION.ATA,
+                                tooltip: [mTagSumDataWrap.DIMENSION.M_TAG, mTagSumDataWrap.DIMENSION.ATA]
+                            },
+                            renderItem: function (params, api) {
+                                var mTagVal = api.value(mTagSumDataWrap.DIMENSION.M_TAG);
+                                var ataVal = api.value(mTagSumDataWrap.DIMENSION.ATA);
+                                var tarPos = api.coord([mTagVal, ataVal]);
+                                var zeroPos = api.coord([mTagVal, 0]);
+                                var size = api.size([mTagVal, ataVal]);
+                                var width = size[0] * 0.4;
+                                return {
+                                    type: 'rect',
+                                    morph: true,
+                                    shape: {
+                                        x: tarPos[0] - width / 2,
+                                        y: tarPos[1],
+                                        height: zeroPos[1] - tarPos[1],
+                                        width: width,
+                                        transition: ['x', 'y', 'width', 'height']
+                                    },
+                                    style: {
+                                        transition: 'lineWidth',
+                                        fill: CONTENT_COLOR,
+                                        stroke: '#555',
+                                        lineWidth: 0
+                                    }
+                                };
+                            }
+                        }
+                    };
+
+                    return {
+                        option: option,
+                        dataWrap: mTagSumDataWrap
+                    };
+                }
+
+                function create_Pie_mSum_ATA(mTagSumDataWrap) {
+                    var totalValue = mTagSumDataWrap.data.reduce(function (val, item) {
+                        return val + item[mTagSumDataWrap.DIMENSION.ATA];
+                    }, 0);
+                    let angles = [];
+                    let currentAngle = -Math.PI / 2;
+                    for (let i = 0; i < mTagSumDataWrap.data.length; i++) {
+                        const angle = mTagSumDataWrap.data[i][mTagSumDataWrap.DIMENSION.ATA] / totalValue * Math.PI * 2;
+                        angles.push([currentAngle, angle + currentAngle]);
+                        currentAngle += angle;
+                    }
+
+                    var option = {
+                        tooltip: {},
+                        series: {
+                            type: 'custom',
+                            coordinateSystem: 'none',
+                            animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                            data: mTagSumDataWrap.data,
+                            encode: {
+                                itemName: mTagSumDataWrap.DIMENSION.M_TAG,
+                                value: mTagSumDataWrap.DIMENSION.ATA,
+                                tooltip: [mTagSumDataWrap.DIMENSION.ATA]
+                            },
+                            renderItem: function (params, api) {
+                                const width = chart.getWidth();
+                                const height = chart.getHeight();
+                                return {
+                                    type: 'sector',
+                                    morph: true,
+                                    shape: {
+                                        cx: width / 2,
+                                        cy: height / 2,
+                                        r: Math.min(width, height) / 3,
+                                        r0: Math.min(width, height) / 5,
+                                        startAngle: angles[params.dataIndex][0],
+                                        endAngle: angles[params.dataIndex][1],
+                                        clockwise: true
+                                    },
+                                    style: {
+                                        fill: CONTENT_COLOR,
+                                        stroke: '#555',
+                                        strokeNoScale: true
+                                    },
+                                };
+                            }
+                        }
+                    };
+
+                    return {
+                        option: option,
+                        dataWrap: mTagSumDataWrap
+                    };
+                }
+
+                function createScatter_zSum_ATA(zTagSumDataWrap) {
+                }
+
+
+                var currOptionName = 'Scatter_ATA_STE';
+                var optionInfoList = {
+                    'Scatter_ATA_STE': create_Scatter_ATA_STE(rawDataWrap),
+                    'Bar_mTagSum_ATA': create_Bar_mSum_ATA(mTagSumDataWrap),
+                    'Pie_mTagSum_ATA': create_Pie_mSum_ATA(mTagSumDataWrap),
+                };
+
+                function next(nextOptionName) {
+                    const lastOptionInfo = optionInfoList[currOptionName];
+                    const nextOptionInfo = optionInfoList[nextOptionName];
+
+                    const commonDimension = findCommonDimension(lastOptionInfo, nextOptionInfo)
+                        || findCommonDimension(nextOptionInfo, lastOptionInfo);
+                    const fromDimension = lastOptionInfo.dataWrap.DIMENSION[commonDimension];
+                    const toDimension = nextOptionInfo.dataWrap.DIMENSION[commonDimension];
+                    const transitionOpt = (fromDimension != null && toDimension != null)
+                        ? { from: fromDimension, to: toDimension } : null;
+
+                    currOptionName = nextOptionName;
+
+                    chart.setOption(nextOptionInfo.option, {
+                        replaceMerge: ['xAxis', 'yAxis'],
+                        transition: transitionOpt
+                    });
+                }
+
+                function findCommonDimension(optionInfoA, optionInfoB) {
+                    var uniqueKey = optionInfoB.dataWrap.uniqueKey;
+                    if (uniqueKey != null && optionInfoA.dataWrap.DIMENSION[uniqueKey] != null) {
+                        return uniqueKey;
+                    }
+                }
+
+                var chart = testHelper.create(echarts, 'main0', {
+                    title: [
+                        'Test: buttons, should morph animation merge/split.',
+                        'Test: click buttons **before animation finished**, should no blink.',
+                        'Test: click buttons **twice**, should no blink.',
+                        'Test: use dataZoom, update animation should exist'
+                    ],
+                    option: optionInfoList[currOptionName].option,
+                    height: 600,
+                    buttons: [{
+                        text: 'ToBar',
+                        onclick: function () {
+                            next('Bar_mTagSum_ATA');
+                        }
+                    }, {
+                        text: 'ToScatter',
+                        onclick: function () {
+                            next('Scatter_ATA_STE');
+                        }
+                    }, {
+                        text: 'ToPie',
+                        onclick: function () {
+                            next('Pie_mTagSum_ATA');
+                        }
+                    }]
+                });
+
+            });
+
+        </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