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/11/04 13:13:38 UTC

[incubator-echarts-examples] branch next updated (9c95942 -> 301953c)

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

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


    from 9c95942  update gauge-car example
     new 3199bc1  add examples about morphing and data-transform.
     new 301953c  enhance dat.GUI in examples.

The 2 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.


Summary of changes:
 README.md                                         |  50 ++-
 public/data/asset/data/south-america-polygon.json |   1 +
 public/data/asset/img/custom-gauge-panel.png      | Bin 0 -> 13745 bytes
 public/data/asset/js/myTransform.js               | 247 +++++++++++
 public/data/asset/js/transitionPlayer.js          | 256 +++++++++++
 public/data/custom-combine-separate-morph.js      | 505 ++++++++++++++++++++++
 public/data/custom-gauge.js                       | 185 ++++++++
 public/data/custom-one-to-one-morph.js            | 222 ++++++++++
 public/data/custom-spiral-race.js                 | 257 +++++++++++
 public/data/custom-story-transition.js            | 400 +++++++++++++++++
 public/data/data-transform-filter.js              |  81 ++++
 public/data/scatter-linear-regression.js          |   4 +-
 src/editor/sandbox.js                             |   2 +-
 13 files changed, 2207 insertions(+), 3 deletions(-)
 create mode 100644 public/data/asset/data/south-america-polygon.json
 create mode 100644 public/data/asset/img/custom-gauge-panel.png
 create mode 100644 public/data/asset/js/myTransform.js
 create mode 100644 public/data/asset/js/transitionPlayer.js
 create mode 100644 public/data/custom-combine-separate-morph.js
 create mode 100644 public/data/custom-gauge.js
 create mode 100644 public/data/custom-one-to-one-morph.js
 create mode 100644 public/data/custom-spiral-race.js
 create mode 100644 public/data/custom-story-transition.js
 create mode 100644 public/data/data-transform-filter.js


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


[incubator-echarts-examples] 02/02: enhance dat.GUI in examples.

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

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

commit 301953c01ed3bd7eefa0f87531873edaa0a60023
Author: 100pah <su...@gmail.com>
AuthorDate: Wed Nov 4 21:13:18 2020 +0800

    enhance dat.GUI in examples.
---
 README.md             | 50 +++++++++++++++++++++++++++++++++++++++++++++++++-
 src/editor/sandbox.js |  2 +-
 2 files changed, 50 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index bd143c2..e24213b 100644
--- a/README.md
+++ b/README.md
@@ -50,4 +50,52 @@ category: 'line, visualMap'
 */
 ```
 
-describes the meta info of this example.
\ No newline at end of file
+describes the meta info of this example.
+
+
+## Some built-in features available in examples
+
+### Controller panel
+
+Use this code to enable controller panel for a example:
+```js
+app.config = {
+    aNameForTheSelectWidget: 'This is the initial value'
+    aNameForTheRangeWidget: 45,
+    aNameForTheButtonWidget: function () {
+        // Do something.
+    },
+    onChange: function () {
+        // Do something.
+    }
+};
+app.configParameters = {
+    aNameForTheSelectWidget: {
+        options: [
+            'This is the initial value',
+            'This is another value',
+            'This is the third value'
+        ]
+    },
+    aNameForTheRangeWidget: {
+        min: -90,
+        max: 90
+    }
+};
+```
+
+### Resize
+
+```js
+app.onresize = function () {
+    // Do something.
+}
+```
+
+### Get width and height of the chart area
+
+```js
+var width = myChart.getWidth();
+var height = myChart.getHeight();
+```
+
diff --git a/src/editor/sandbox.js b/src/editor/sandbox.js
index 211c4a3..cfeafe8 100644
--- a/src/editor/sandbox.js
+++ b/src/editor/sandbox.js
@@ -152,7 +152,7 @@ export function createSandbox(optionUpdated) {
                     if (name !== 'onChange' && name !== 'onFinishChange') {
                         var isColor = false;
                         // var value = obj;
-                        var controller;
+                        var controller = null;
                         if (configParameters[name]) {
                             if (configParameters[name].options) {
                                 controller = gui.add(appEnv.config, name, configParameters[name].options);


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


[incubator-echarts-examples] 01/02: add examples about morphing and 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 next
in repository https://gitbox.apache.org/repos/asf/incubator-echarts-examples.git

commit 3199bc1c41b3af83a2e62fd1186fe880f9ac4167
Author: 100pah <su...@gmail.com>
AuthorDate: Wed Nov 4 21:12:56 2020 +0800

    add examples about morphing and data-transform.
---
 public/data/asset/data/south-america-polygon.json |   1 +
 public/data/asset/img/custom-gauge-panel.png      | Bin 0 -> 13745 bytes
 public/data/asset/js/myTransform.js               | 247 +++++++++++
 public/data/asset/js/transitionPlayer.js          | 256 +++++++++++
 public/data/custom-combine-separate-morph.js      | 505 ++++++++++++++++++++++
 public/data/custom-gauge.js                       | 185 ++++++++
 public/data/custom-one-to-one-morph.js            | 222 ++++++++++
 public/data/custom-spiral-race.js                 | 257 +++++++++++
 public/data/custom-story-transition.js            | 400 +++++++++++++++++
 public/data/data-transform-filter.js              |  81 ++++
 public/data/scatter-linear-regression.js          |   4 +-
 11 files changed, 2157 insertions(+), 1 deletion(-)

diff --git a/public/data/asset/data/south-america-polygon.json b/public/data/asset/data/south-america-polygon.json
new file mode 100644
index 0000000..3209c15
--- /dev/null
+++ b/public/data/asset/data/south-america-polygon.json
@@ -0,0 +1 @@
+{"Argentina":[[0.43921076893406374,0.5057440624578535],[0.4443941216063111,0.5142347489261295],[0.4478344261379398,0.5047774996236355],[0.457889246381859,0.505263514876542],[0.45931243573415903,0.5077798588484654],[0.47552271509602206,0.5269622733616183],[0.48273161227464145,0.5287502019732476],[0.4935047478743797,0.5374340795045541],[0.502585639115404,0.5420293537207854],[0.5038514050896924,0.5472175665455625],[0.4951707929733665,0.5650877398758679],[0.5040621382657573,0.568287542797191 [...]
\ No newline at end of file
diff --git a/public/data/asset/img/custom-gauge-panel.png b/public/data/asset/img/custom-gauge-panel.png
new file mode 100644
index 0000000..d1485e1
Binary files /dev/null and b/public/data/asset/img/custom-gauge-panel.png differ
diff --git a/public/data/asset/js/myTransform.js b/public/data/asset/js/myTransform.js
new file mode 100644
index 0000000..1d72b52
--- /dev/null
+++ b/public/data/asset/js/myTransform.js
@@ -0,0 +1,247 @@
+/*
+* 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);
+
+
+
+
+
+(function (exports) {
+
+    /**
+     * @usage
+     *
+     * ```js
+     * dataset: [{
+     *     source: [
+     *         ['aa', 'bb', 'cc', 'tag'],
+     *         [12, 0.33, 5200, 'AA'],
+     *         [21, 0.65, 8100, 'AA'],
+     *         ...
+     *     ]
+     * }, {
+     *     transform: {
+     *         type: 'my:id',
+     *         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:id',
+
+        /**
+         * @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;
+
+            var data = upstream.cloneRawData();
+
+            for (var i = 0, len = data.length; i < len; i++) {
+                var line = data[i];
+                line[dimensionIndex] = i;
+            }
+
+            return {
+                dimensions: dimsDef,
+                data: upstream.data
+            };
+        }
+    };
+
+    var myTransform = exports.myTransform = exports.myTransform || {};
+    myTransform.id = transform;
+
+})(this);
diff --git a/public/data/asset/js/transitionPlayer.js b/public/data/asset/js/transitionPlayer.js
new file mode 100644
index 0000000..6355e99
--- /dev/null
+++ b/public/data/asset/js/transitionPlayer.js
@@ -0,0 +1,256 @@
+/*
+* 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',
+     *             dividingMethod: 'duplicate'
+     *         },
+     *         ...
+     *     },
+     *
+     *     // 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;
+     *                dividingMethod?: 'split' | 'duplicate'
+     *            }
+     *        }
+     * @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,
+                    },
+                    dividingMethod: dataMeta.dividingMethod
+                };
+            }
+        }
+
+        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);
diff --git a/public/data/custom-combine-separate-morph.js b/public/data/custom-combine-separate-morph.js
new file mode 100644
index 0000000..e839ce2
--- /dev/null
+++ b/public/data/custom-combine-separate-morph.js
@@ -0,0 +1,505 @@
+/*
+title: Combine-Separate Morphing
+category: custom
+titleCN: 聚合分割形变
+difficulty: 11
+*/
+
+
+
+var PIE_COLORS = [
+    '#e06343', '#37a354', '#b55dba', '#b5bd48', '#8378EA', '#96BFFF'
+];
+var CLUSTER_COLORS = [
+    '#cc5664', '#9bd6ec', '#ea946e', '#8acaaa', '#f1ec64', '#ee8686', '#a48dc1', '#5da6bc', '#b9dcae'
+];
+var Z_TAG_COLORS = [
+    '#c23531', '#2f4554', '#61a0a8', '#d48265', '#91c7ae', '#749f83',
+    '#ca8622', '#bda29a', '#6e7074', '#546570', '#c4ccd3'
+];
+var Z_TAG_COLORS_2 = [
+    '#51689b', '#ce5c5c', '#fbc357', '#8fbf8f', '#659d84', '#fb8e6a', '#c77288', '#786090',
+    '#91c4c5', '#6890ba'
+];
+var SYMBOL_PATHS = [
+    'path://m67.25,28.9c27.42,-69.1 134.84,0 0,88.85c-134.84,-88.85 -27.42,-157.96 0,-88.85z',
+    'path://M16 0c-8.837 0-16 7.163-16 16s7.163 16 16 16 16-7.163 16-16-7.163-16-16-16zM22 8c1.105 0 2 1.343 2 3s-0.895 3-2 3-2-1.343-2-3 0.895-3 2-3zM10 8c1.105 0 2 1.343 2 3s-0.895 3-2 3-2-1.343-2-3 0.895-3 2-3zM16 28c-5.215 0-9.544-4.371-10-9.947 2.93 1.691 6.377 2.658 10 2.658s7.070-0.963 10-2.654c-0.455 5.576-4.785 9.942-10 9.942z',
+    'path://M16 0c-8.837 0-16 7.163-16 16s7.163 16 16 16c8.837 0 16-7.163 16-16s-7.163-16-16-16zM22 7.375c1.999 0 3.625 1.626 3.625 3.625 0 0.199-0.017 0.402-0.051 0.604-0.051 0.301-0.311 0.521-0.616 0.521s-0.566-0.22-0.616-0.522c-0.192-1.146-1.177-1.666-2.341-1.666s-2.149 0.52-2.341 1.666c-0.050 0.301-0.311 0.522-0.616 0.522-0 0 0 0-0 0-0.305 0-0.566-0.22-0.616-0.521-0.034-0.201-0.051-0.404-0.051-0.604 0-1.999 1.626-3.625 3.625-3.625zM10 7.375c1.999 0 3.625 1.626 3.625 3.625 0 0.199-0.0 [...]
+    'path://M23.6 2c4.637 0 8.4 3.764 8.4 8.401 0 9.132-9.87 11.964-15.999 21.232-6.485-9.326-16.001-11.799-16.001-21.232 0-4.637 3.763-8.401 8.4-8.401 1.886 0 3.625 0.86 5.025 2.12l-2.425 3.88 7 4-4 10 11-12-7-4 1.934-2.901c1.107-0.68 2.35-1.099 3.665-1.099z',
+    'path://M237.062,81.761L237.062,81.761c-12.144-14.24-25.701-20.1-40.68-19.072 c-10.843,0.747-20.938,5.154-30.257,13.127c-9.51-5.843-19.8-9.227-30.859-10.366c0.521-3.197,1.46-6.306,2.85-9.363 c3.458-7.038,8.907-12.741,16.331-17.296c-5.609-3.384-11.227-6.799-16.854-10.279c-16.257,8.104-25.06,20.601-26.463,38.417 c-7.599,1.705-14.685,4.486-21.247,8.437c-9.164-7.677-18.996-11.917-29.496-12.632c-14.819-0.998-28.467,4.787-40.938,18.827 C6.445,96.182,0,114.867,0,136.242c-0.007,6.371,0.674,1 [...]
+    'path://M237.062,81.761L237.062,81.761c-12.144-14.24-25.701-20.1-40.68-19.072 c-10.843,0.747-20.938,5.154-30.257,13.127c-9.51-5.843-19.8-9.227-30.859-10.366c0.521-3.197,1.46-6.306,2.85-9.363 c3.458-7.038,8.907-12.741,16.331-17.296c-5.609-3.384-11.227-6.799-16.854-10.279c-16.257,8.104-25.06,20.601-26.463,38.417 c-7.599,1.705-14.685,4.486-21.247,8.437c-9.164-7.677-18.996-11.917-29.496-12.632c-14.819-0.998-28.467,4.787-40.938,18.827 C6.445,96.182,0,114.867,0,136.242c-0.007,6.371,0.674,1 [...]
+];
+var CONTENT_COLOR = '#37A2DA';
+var ANIMATION_DURATION_UPDATE = 1500;
+
+
+var RAW_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","ZC","P10"],[14260896 [...]
+
+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'];
+
+function getFromPalette(value, palette) {
+    if (!palette.__colorMap) {
+        palette.__colorMap = {};
+    }
+    if (palette.__colorIdx == null) {
+        palette.__colorIdx = 0;
+    }
+    if (!palette.__colorMap[value]) {
+        palette.__colorMap[value] = palette[palette.__colorIdx];
+        palette.__colorIdx++;
+        if (palette.__colorIdx >= palette.length) {
+            palette.__colorIdx = 0;
+        }
+    }
+    return palette.__colorMap[value];
+}
+
+
+
+
+
+var baseOption = {
+    dataset: [{
+        id: 'raw',
+        dimensions: RAW_DATA_DIMENSIONS,
+        source: RAW_DATA
+    }, {
+        id: 'mTagSum',
+        fromDatasetId: 'raw',
+        transform: {
+            type: 'myTransform:aggregate',
+            config: {
+                resultDimensions: [
+                    { from: 'ATA', method: 'sum' },
+                    { from: 'STE', method: 'sum' },
+                    { from: 'CTZ', method: 'sum' },
+                    { from: 'M_TAG' }
+                ],
+                groupBy: 'M_TAG'
+            }
+        }
+    }, {
+        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'
+                }]
+            }
+        }
+    }, {
+        id: 'rawClusterCenters',
+        fromDatasetId: 'rawClusters',
+        transform: {
+            type: 'myTransform: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'
+            }
+        }
+    }],
+    tooltip: {}
+};
+
+
+function makeScatterOptionCreator(renderItem) {
+    return function () {
+        var datasetId = 'rawClusters';
+        return {
+            datasetId: datasetId,
+            option: {
+                grid: {
+                    containLabel: true
+                },
+                xAxis: {
+                    name: 'STE'
+                },
+                yAxis: {
+                    name: 'ATA'
+                },
+                series: {
+                    type: 'custom',
+                    coordinateSystem: 'cartesian2d',
+                    animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                    datasetId: datasetId,
+                    encode: {
+                        itemName: 'ID',
+                        x: 'STE',
+                        y: 'ATA',
+                        tooltip: ['STE', 'ATA']
+                    },
+                    renderItem: renderItem
+                }
+            }
+        }
+    };
+}
+
+
+
+
+var optionCreators = {
+
+    'Scatter': makeScatterOptionCreator(
+        function (params, api) {
+            var pos = api.coord([
+                api.value('STE'),
+                api.value('ATA')
+            ]);
+            var zTagVal = api.value('Z_TAG');
+            var color = getFromPalette(zTagVal, Z_TAG_COLORS);
+            return {
+                type: 'circle',
+                morph: true,
+                shape: {
+                    cx: pos[0],
+                    cy: pos[1],
+                    r: 10
+                },
+                style: {
+                    fill: color
+                },
+                transition: ['shape', 'style']
+            };
+        }
+    ),
+
+    'Glyph': makeScatterOptionCreator(
+        function (params, api) {
+            var pos = api.coord([
+                api.value('STE'),
+                api.value('ATA')
+            ]);
+            var zTagVal = api.value('Z_TAG');
+            var color = getFromPalette(zTagVal, Z_TAG_COLORS);
+            var symbolPath = getFromPalette(zTagVal, SYMBOL_PATHS);
+            return {
+                type: 'path',
+                morph: true,
+                x: pos[0],
+                y: pos[1],
+                shape: {
+                    pathData: symbolPath,
+                    width: 40,
+                    height: 40
+                },
+                style: {
+                    fill: color
+                },
+                transition: ['style']
+            };
+        }
+    ),
+
+    'House': makeScatterOptionCreator(
+        function (params, api) {
+            var pos = api.coord([
+                api.value('STE'),
+                api.value('ATA')
+            ]);
+            var zTagVal = api.value('Z_TAG');
+            var color1 = getFromPalette(zTagVal, Z_TAG_COLORS);
+            var color2 = getFromPalette(zTagVal, Z_TAG_COLORS_2);
+            return {
+                type: 'group',
+                x: pos[0],
+                y: pos[1],
+                children: [{
+                    type: 'polygon',
+                    morph: true,
+                    shape: {
+                        points: [
+                            [-40, -2],
+                            [40, -2],
+                            [0, -35]
+                        ]
+                    },
+                    style: {
+                        fill: color1
+                    },
+                    transition: ['shape', 'style']
+                }, {
+                    type: 'rect',
+                    morph: true,
+                    shape: {
+                        x: -20,
+                        y: 0,
+                        width: 40,
+                        height: 30
+                    },
+                    style: {
+                        fill: color2
+                    },
+                    transition: ['shape', 'style']
+                }]
+            };
+        }
+    ),
+
+    'Bar_Sum': function () {
+        var datasetId = 'mTagSum'
+        return {
+            datasetId: datasetId,
+            option: {
+                grid: {
+                    containLabel: true
+                },
+                xAxis: {
+                    type: 'category'
+                },
+                yAxis: {
+                },
+                series: {
+                    type: 'custom',
+                    coordinateSystem: 'cartesian2d',
+                    animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                    datasetId: datasetId,
+                    encode: {
+                        x: 'M_TAG',
+                        y: 'ATA',
+                        tooltip: ['M_TAG', 'ATA']
+                    },
+                    renderItem: function (params, api) {
+                        var mTagVal = api.value('M_TAG');
+                        var ataVal = api.value('ATA');
+                        var tarPos = api.coord([mTagVal, ataVal]);
+                        var zeroPos = api.coord([mTagVal, 0]);
+                        var size = api.size([mTagVal, ataVal]);
+                        var width = size[0] * 0.4;
+                        return {
+                            type: 'rect',
+                            morph: true,
+                            shape: {
+                                x: tarPos[0] - width / 2,
+                                y: tarPos[1],
+                                height: zeroPos[1] - tarPos[1],
+                                width: width,
+                            },
+                            style: {
+                                fill: CONTENT_COLOR
+                            },
+                            transition: ['shape', 'style']
+                        };
+                    }
+                }
+            }
+        };
+    },
+
+    'Pie_Sum': function () {
+        var datasetId = 'mTagSum';
+        return {
+            datasetId: datasetId,
+            option: {
+                series: {
+                    type: 'custom',
+                    coordinateSystem: 'none',
+                    animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                    datasetId: datasetId,
+                    encode: {
+                        itemName: 'M_TAG',
+                        value: 'ATA',
+                        tooltip: 'ATA'
+                    },
+                    renderItem: function (params, api) {
+                        var context = params.context;
+                        if (!context.layout) {
+                            context.layout = true;
+                            var totalValue = 0;
+                            for (var i = 0; i < params.dataInsideLength; i++) {
+                                totalValue += api.value('ATA', i);
+                            }
+                            var angles = [];
+                            var colors = [];
+                            var currentAngle = -Math.PI / 2;
+                            for (var i = 0; i < params.dataInsideLength; i++) {
+                                colors.push(PIE_COLORS[i]);
+                                var angle = api.value('ATA', i) / totalValue * Math.PI * 2;
+                                angles.push([currentAngle, angle + currentAngle - 0.01]);
+                                currentAngle += angle;
+                            }
+                            context.angles = angles;
+                            context.colors = colors;
+                        }
+
+                        var width = myChart.getWidth();
+                        var height = myChart.getHeight();
+                        return {
+                            type: 'sector',
+                            morph: true,
+                            shape: {
+                                cx: width / 2,
+                                cy: height / 2,
+                                r: Math.min(width, height) / 3,
+                                r0: Math.min(width, height) / 5,
+                                startAngle: context.angles[params.dataIndex][0],
+                                endAngle: context.angles[params.dataIndex][1],
+                                clockwise: true
+                            },
+                            style: {
+                                // fill: CONTENT_COLOR,
+                                fill: context.colors[params.dataIndex]
+                            },
+                            transition: ['shape', 'style']
+                        };
+                    }
+                }
+            }
+        };
+    },
+
+    'Clustered': function () {
+        var datasetId = 'rawClusterCenters';
+        return {
+            datasetId: datasetId,
+            option: {
+                grid: {
+                    containLabel: true
+                },
+                xAxis: {
+                    name: 'STE'
+                },
+                yAxis: {
+                    name: 'ATA'
+                },
+                series: {
+                    type: 'custom',
+                    coordinateSystem: 'cartesian2d',
+                    animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+                    datasetId: datasetId,
+                    encode: {
+                        x: 'CLUSTER_CENTER_STE',
+                        y: 'CLUSTER_CENTER_ATA',
+                        tooltip: ['CLUSTER_CENTER_STE', 'CLUSTER_CENTER_ATA']
+                    },
+                    renderItem: function (params, api) {
+                        var context = params.context;
+                        if (!context.layout) {
+                            context.layout = true;
+                            context.totalCount = 0;
+                            for (var i = 0; i < params.dataInsideLength; i++) {
+                                context.totalCount += api.value('COUNT', i);
+                            }
+                        }
+
+                        var pos = api.coord([
+                            api.value('CLUSTER_CENTER_STE'),
+                            api.value('CLUSTER_CENTER_ATA')
+                        ]);
+                        var count = api.value('COUNT');
+                        var radius = count / context.totalCount * 100 + 10;
+                        return {
+                            type: 'circle',
+                            morph: true,
+                            shape: {
+                                cx: pos[0],
+                                cy: pos[1],
+                                r: radius,
+                            },
+                            style: {
+                                // fill: CONTENT_COLOR,
+                                fill: CLUSTER_COLORS[params.dataIndex]
+                            },
+                            transition: ['shape', 'style']
+                        };
+                    }
+                }
+            }
+        };
+    }
+};
+
+
+
+var _player;
+var _optionList = [];
+app.config = {};
+echarts.util.each(optionCreators, function (creator, key) {
+    var optionWrap = creator();
+    _optionList.push({
+        key: key,
+        dataMetaKey: optionWrap.datasetId,
+        option: optionWrap.option
+    });
+    app.config[key] = function () {
+        _player.go(key);
+    }
+});
+console.log(app.config);
+
+
+
+var _global = {};
+$.get(ROOT_PATH + '/data/asset/js/myTransform.js', function (aggregateJS) {
+    (new Function(aggregateJS)).call(_global);
+
+    $.get(ROOT_PATH + '/data/asset/js/transitionPlayer.js', function (transitionPlayerJS) {
+        (new Function(transitionPlayerJS)).call(_global);
+
+        run();
+    });
+});
+
+function run() {
+
+    echarts.registerTransform(_global.myTransform.aggregate);
+    echarts.registerTransform(ecStat.transform.clustering);
+
+    _player = _global.transitionPlayer.create({
+        chart: function () {
+            return myChart;
+        },
+        seriesIndex: 0,
+        replaceMerge: ['xAxis', 'yAxis'],
+        dataMeta: {
+            raw: {
+                dimensions: RAW_DATA_DIMENSIONS,
+                uniqueDimension: 'ID'
+            },
+            mTagSum: {
+                dimensions: M_TAG_SUM_DIMENSIONS,
+                uniqueDimension: 'M_TAG'
+            },
+            rawClusters: {
+                dimensions: RAW_CLUSTER_DIMENSIONS,
+                uniqueDimension: 'ID',
+                dividingMethod: 'duplicate'
+            },
+            rawClusterCenters: {
+                dimensions: RAW_CLUSTER_CENTERS_DIMENSIONS,
+                uniqueDimension: 'CLUSTER_IDX'
+            }
+        },
+        optionList: _optionList
+    });
+
+    myChart.setOption(baseOption, { lazyUpdate: true });
+
+    _player.go('Glyph');
+
+}
+
+
+
diff --git a/public/data/custom-gauge.js b/public/data/custom-gauge.js
new file mode 100644
index 0000000..0241981
--- /dev/null
+++ b/public/data/custom-gauge.js
@@ -0,0 +1,185 @@
+/*
+title: Custom Gauge
+category: custom
+titleCN: 自定义仪表
+difficulty: 9
+*/
+
+
+var _datasourceList = [
+    [[1, 156]],
+    [[1, 54]],
+    [[1, 131]],
+    [[1, 32]],
+    [[1, 103]],
+    [[1, 66]],
+];
+var _panelImageURL = ROOT_PATH + '/data/asset/img/custom-gauge-panel.png';
+var _animationDuration = 1000;
+var _animationDurationUpdate = 1000;
+var _animationEasingUpdate = 'elasticOut';
+var _valOnRadianMax = 200;
+var _outerRadius = 100;
+var _innerRadius = 85;
+var _pointerInnerRadius = 40;
+var _insidePanelRadius = 65;
+var _currentDataIndex = 0;
+
+function renderItem(params, api) {
+    var valOnRadian = api.value(1);
+    var coords = api.coord([api.value(0), valOnRadian]);
+    var polarEndRadian = coords[3];
+    var imageStyle = {
+        image: _panelImageURL,
+        x: params.coordSys.cx - _outerRadius,
+        y: params.coordSys.cy - _outerRadius,
+        width: _outerRadius * 2,
+        height: _outerRadius * 2
+    };
+
+    return {
+        type: 'group',
+        children: [{
+            type: 'image',
+            style: imageStyle,
+            clipPath: {
+                type: 'sector',
+                shape: {
+                    cx: params.coordSys.cx,
+                    cy: params.coordSys.cy,
+                    r: _outerRadius,
+                    r0: _innerRadius,
+                    startAngle: 0,
+                    endAngle: -polarEndRadian,
+                    transition: 'endAngle',
+                    enterFrom: { endAngle: 0 }
+                }
+            }
+        }, {
+            type: 'image',
+            style: imageStyle,
+            clipPath: {
+                type: 'polygon',
+                shape: {
+                    points: makePionterPoints(params, polarEndRadian),
+                },
+                extra: {
+                    polarEndRadian: polarEndRadian,
+                    transition: 'polarEndRadian',
+                    enterFrom: { polarEndRadian: 0 }
+                },
+                during: function (apiDuring) {
+                    apiDuring.setShape(
+                        'points',
+                        makePionterPoints(params, apiDuring.getExtra('polarEndRadian'))
+                    );
+                }
+            },
+        }, {
+            type: 'circle',
+            shape: {
+                cx: params.coordSys.cx,
+                cy: params.coordSys.cy,
+                r: _insidePanelRadius
+            },
+            style: {
+                fill: '#fff',
+                shadowBlur: 25,
+                shadowOffsetX: 0,
+                shadowOffsetY: 0,
+                shadowColor: 'rgb(0,0,50)'
+            }
+        }, {
+            type: 'text',
+            extra: {
+                valOnRadian: valOnRadian,
+                transition: 'valOnRadian',
+                enterFrom: { valOnRadian: 0 }
+            },
+            style: {
+                text: makeText(valOnRadian),
+                fontSize: 40,
+                x: params.coordSys.cx,
+                y: params.coordSys.cy,
+                fill: 'rgb(0,50,190)',
+                align: 'center',
+                verticalAlign: 'middle',
+                enterFrom: { opacity: 0 }
+            },
+            during: function (apiDuring) {
+                apiDuring.setStyle('text', makeText(apiDuring.getExtra('valOnRadian')));
+            }
+        }]
+    };
+}
+
+function convertToPolarPoint(renderItemParams, radius, radian) {
+    return [
+        Math.cos(radian) * radius + renderItemParams.coordSys.cx,
+        -Math.sin(radian) * radius + renderItemParams.coordSys.cy
+    ];
+}
+
+function makePionterPoints(renderItemParams, polarEndRadian) {
+    return [
+        convertToPolarPoint(renderItemParams, _outerRadius, polarEndRadian),
+        convertToPolarPoint(renderItemParams, _outerRadius, polarEndRadian + Math.PI * 0.03),
+        convertToPolarPoint(renderItemParams, _pointerInnerRadius, polarEndRadian)
+    ];
+}
+
+function makeText(valOnRadian) {
+    // Validate additive animation calc.
+    if (valOnRadian < -10) {
+        alert('illegal during val: ' + valOnRadian);
+    }
+    return (valOnRadian / _valOnRadianMax * 100).toFixed(0) + '%'
+}
+
+option = {
+    animationEasing: _animationEasingUpdate,
+    animationDuration: _animationDuration,
+    animationDurationUpdate: _animationDurationUpdate,
+    animationEasingUpdate: _animationEasingUpdate,
+    dataset: {
+        source: _datasourceList[_currentDataIndex]
+    },
+    tooltip: {},
+    angleAxis: {
+        type: 'value',
+        startAngle: 0,
+        axisLine: { show: false },
+        axisTick: { show: false },
+        axisLabel: { show: false },
+        splitLine: { show: false },
+        min: 0,
+        max: _valOnRadianMax
+    },
+    radiusAxis: {
+        type: 'value',
+        axisLine: { show: false },
+        axisTick: { show: false },
+        axisLabel: { show: false },
+        splitLine: { show: false }
+    },
+    polar: {},
+    series: [{
+        type: 'custom',
+        coordinateSystem: 'polar',
+        renderItem: renderItem
+    }]
+};
+
+// --------------
+// Control Panel
+app.config = {
+    'Click Me ...': function () {
+        _currentDataIndex++;
+        _currentDataIndex >= _datasourceList.length && (_currentDataIndex = 0);
+        myChart.setOption({
+            dataset: {
+                source: _datasourceList[_currentDataIndex]
+            }
+        });
+    }
+};
diff --git a/public/data/custom-one-to-one-morph.js b/public/data/custom-one-to-one-morph.js
new file mode 100644
index 0000000..bece8c0
--- /dev/null
+++ b/public/data/custom-one-to-one-morph.js
@@ -0,0 +1,222 @@
+/*
+title: One-to-one Morphing
+category: custom
+titleCN: 一对一映射形变
+difficulty: 11
+*/
+
+$.get(ROOT_PATH + '/data/asset/data/south-america-polygon.json', function (rawGeoPolygonMap) {
+
+    var _geoWidth = myChart.getWidth();
+    var _geoHeight = myChart.getHeight() * 0.8;
+    var _animationDurationUpdate = 2000;
+    var _data = [
+        { name: "Argentina", value: 2652124 },
+        { name: "Falkland Is.", value: 7697384 },
+        { name: "Colombia", value: 3156196 },
+        { name: "Paraguay", value: 8457736 },
+        { name: "Peru", value: 1401646 },
+        { name: "Bolivia", value: 5505577 },
+        { name: "Brazil", value: 2217237 },
+        { name: "Ecuador", value: 3398189 },
+        { name: "Chile", value: 8008877 },
+        { name: "Guyana", value: 4070041 },
+        { name: "Uruguay", value: 478873 },
+        { name: "Suriname", value: 2793265 },
+        { name: "Venezuela", value: 2720478 }
+    ];
+    var _nameList = [];
+    echarts.util.each(_data, function (item) {
+        _nameList.push(item.name);
+    });
+
+
+    var geoPolygonMap = scalePolygons(rawGeoPolygonMap, _geoWidth, _geoHeight);
+
+    var mapOption = {
+        series: [{
+            coordinateSystem: 'none',
+            type: 'custom',
+            data: _data,
+            animationDurationUpdate: _animationDurationUpdate,
+            renderItem: function (params, api) {
+                var dataItem = _data[params.dataIndex];
+                var points = geoPolygonMap[dataItem.name];
+                return {
+                    type: 'polygon',
+                    morph: true,
+                    shape: {
+                        points: points
+                    },
+                    style: {
+                        fill: '#aaa',
+                        stroke: '#555',
+                        strokeNoScale: true
+                    }
+                }
+            }
+        }]
+    };
+
+    var barOption = {
+        xAxis: {
+            data: _nameList,
+            axisLabel: {
+                interval: 0,
+                rotate: 30
+            }
+        },
+        yAxis: {
+        },
+        series: [{
+            type: 'custom',
+            data: _data,
+            animationDurationUpdate: _animationDurationUpdate,
+            renderItem: function (params, api) {
+                var start = api.coord([params.dataIndex, 0]);
+                var size = api.size([0, api.value(1)]);
+                var width = 20;
+                return {
+                    type: 'rect',
+                    morph: true,
+                    shape: {
+                        x: start[0] - width / 2,
+                        y: start[1],
+                        width: width,
+                        height: -size[1]
+                    },
+                    style: {
+                        fill: '#aaa',
+                        stroke: '#555',
+                        strokeNoScale: true
+                    },
+                };
+            }
+        }]
+    };
+
+    var bubbleOption = {
+        xAxis: {
+            data: _nameList,
+            axisLabel: {
+                interval: 0,
+                rotate: 30
+            }
+        },
+        yAxis: {
+        },
+        series: [{
+            type: 'custom',
+            data: _data,
+            animationDurationUpdate: _animationDurationUpdate,
+            renderItem: function (params, api) {
+                var center = api.coord([params.dataIndex, api.value(1)]);
+                return {
+                    type: 'circle',
+                    morph: true,
+                    shape: {
+                        cx: center[0],
+                        cy: center[1],
+                        r: api.value(1) / 5e5
+                    },
+                    style: {
+                        fill: '#aaa',
+                        stroke: '#555',
+                        strokeNoScale: true
+                    },
+                };
+            }
+
+        }]
+    };
+
+    var angles = createPieAngles(_data);
+    var pieOption = {
+        series: [{
+            type: 'custom',
+            coordinateSystem: 'none',
+            data: _data,
+            animationDurationUpdate: 2000,
+            renderItem(params, api) {
+                var width = myChart.getWidth();
+                var height = myChart.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: '#aaa',
+                        stroke: '#555',
+                        strokeNoScale: true
+                    },
+                };
+            }
+        }]
+    };
+
+    var options = [
+        mapOption,
+        barOption,
+        bubbleOption,
+        pieOption
+    ];
+
+    var currentIndex = 0;
+    myChart.setOption(options[currentIndex]);
+
+    function transitionToNext() {
+        var nextIndex = (currentIndex + 1) % options.length;
+        myChart.setOption(options[nextIndex], true);
+        currentIndex = nextIndex;
+    }
+
+    setInterval(transitionToNext, 3000);
+
+});
+
+
+
+// --------
+// Utils
+// --------
+function scalePolygons(rawGeoPolygonMap, width, height) {
+    // Scale polygons for specified window size:
+    var matrix = echarts.matrix.create();
+    var geoPolygonMap = {};
+    echarts.matrix.scale(matrix, matrix, [width, height]);
+    echarts.util.each(rawGeoPolygonMap, function (polygon, name) {
+        var scaledPolygon = [];
+        for (var i = 0; i < polygon.length; i++) {
+            var point = polygon[i].slice();
+            echarts.vector.applyTransform(point, point, matrix);
+            scaledPolygon.push(point);
+        }
+        geoPolygonMap[name] = scaledPolygon;
+    });
+    return geoPolygonMap;
+}
+
+function createPieAngles(data) {
+    var pieData = data.slice();
+    var totalValue = 0;
+    echarts.util.each(pieData, function (item) {
+        totalValue += item.value;
+    });
+    var angles = [];
+    var currentAngle = -Math.PI / 2;
+    for (var i = 0; i < pieData.length; i++) {
+        var angle = pieData[i].value / totalValue * Math.PI * 2;
+        angles.push([currentAngle, angle + currentAngle]);
+        currentAngle += angle;
+    }
+    return angles;
+}
+
diff --git a/public/data/custom-spiral-race.js b/public/data/custom-spiral-race.js
new file mode 100644
index 0000000..48caa25
--- /dev/null
+++ b/public/data/custom-spiral-race.js
@@ -0,0 +1,257 @@
+/*
+title: Custom Spiral Race
+category: custom
+titleCN: 自定义螺旋线竞速
+difficulty: 11
+*/
+
+
+var _animationDuration = 5000;
+var _animationDurationUpdate = 7000;
+var _animationEasingUpdate = 'linear';
+var _radianLabels = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpius', 'Sagittarius', 'Capricornus', 'Aquarius', 'Pisces'];
+var _valOnRoundRadian = _radianLabels.length;
+var _radianStep = Math.PI / 45;
+var _barWidthValue = 0.4;
+var _valOnRadiusStep = 4;
+// angleAxis.startAngle is 90 by default.
+var _startRadian = Math.PI / 2;
+
+var _colors = [
+    { border: 'green', inner: 'rgba(0,152,0,0.6)' },
+    { border: 'red', inner: 'rgba(152,0,0,0.6)' },
+    { border: 'blue', inner: 'rgba(0,0, 152,0.6)' },
+];
+var _currentDataIndex = 0;
+var _datasourceList = [
+    [ [1, 3], [2, 6], [3, 9] ], // datasource 0
+    [ [1, 12], [2, 16], [3, 14] ], // datasource 1
+    [ [1, 17], [2, 22], [3, 19] ],  // datasource 2
+    [ [1, 19], [2, 33], [3, 24] ],
+    [ [1, 24], [2, 42], [3, 29] ],
+    [ [1, 27], [2, 47], [3, 41] ],
+    [ [1, 36], [2, 52], [3, 52] ],
+    [ [1, 46], [2, 59], [3, 63] ],
+    [ [1, 60], [2, 63], [3, 69] ],
+];
+var _barNamesByOrdinal = {1: 'A', 2: 'B', 3: 'C'};
+
+function getMaxRadius() {
+    var radius = 0;
+    var datasource = _datasourceList[_currentDataIndex];
+    for (var j = 0; j < datasource.length; j++) {
+        var dataItem = datasource[j];
+        radius = Math.max(radius, getSpiralValueOnRadius(dataItem[0], dataItem[1]));
+    }
+    return Math.ceil(radius * 1.2);
+}
+
+function getSpiralValueOnRadius(valOnStartRadius, valOnEndAngle) {
+    return valOnStartRadius + _valOnRadiusStep * (valOnEndAngle / _valOnRoundRadian);
+}
+function getSpiralRadius(startRadius, endRadian, radiusStep) {
+    return startRadius + radiusStep * ((_startRadian - endRadian) / (Math.PI * 2));
+}
+
+function renderItem(params, api) {
+    var children = [];
+    var dataIdx = params.dataIndex;
+    addShapes(params, api, children, api.value(0), api.value(1), _colors[dataIdx]);
+
+    return {
+        type: 'group',
+        children: children
+    };
+}
+
+function addShapes(params, api, children, valOnStartRadius, valOnEndRadian, color) {
+    var coords = api.coord([valOnStartRadius, valOnEndRadian]);
+    var startRadius = coords[2];
+    var endRadian = coords[3];
+    var widthRadius = api.coord([_barWidthValue, 0])[2];
+    addPolygon(params, children, widthRadius, startRadius, endRadian, color);
+    addLabel(params, children, widthRadius, startRadius, endRadian, color);
+}
+
+function addPolygon(params, children, widthRadius, startRadius, endRadian, color) {
+    children.push({
+        type: 'polygon',
+        shape: {
+            points: makeShapePoints(params, widthRadius, startRadius, endRadian)
+        },
+        extra: {
+            widthRadius: widthRadius,
+            startRadius: startRadius,
+            endRadian: endRadian,
+            transition: ['widthRadius', 'startRadius', 'endRadian']
+        },
+        style: {
+            lineWidth: 1,
+            fill: color.inner,
+            stroke: color.border
+        },
+        during: function (apiDuring) {
+            apiDuring.setShape('points', makeShapePoints(
+                params,
+                apiDuring.getExtra('widthRadius'),
+                apiDuring.getExtra('startRadius'),
+                apiDuring.getExtra('endRadian')
+            ));
+        }
+    });
+}
+
+function makeShapePoints(params, widthRadius, startRadius, endRadian) {
+    var points = [];
+    var radiusStep = getRadiusStepByWidth(widthRadius);
+    // angleAxis.clockwise is true by default. So when rotate clickwisely, radian decreases.
+    for (
+        var iRadian = _startRadian, end = endRadian - _radianStep;
+        iRadian > end;
+        iRadian -= _radianStep
+    ) {
+        iRadian < endRadian && (iRadian = endRadian);
+        var iRadius = getSpiralRadius(startRadius - widthRadius, iRadian, radiusStep);
+        points.push(convertToPolarPoint(params, iRadius, iRadian));
+    }
+    for (
+        var iRadian = endRadian;
+        iRadian < _startRadian + _radianStep;
+        iRadian += _radianStep
+    ) {
+        iRadian > _startRadian && (iRadian = _startRadian);
+        var iRadius = getSpiralRadius(startRadius + widthRadius, iRadian, radiusStep);
+        points.push(convertToPolarPoint(params, iRadius, iRadian));
+    }
+    return points;
+}
+
+function getRadiusStepByWidth(widthRadius) {
+    return widthRadius / _barWidthValue * _valOnRadiusStep;
+}
+
+function addLabel(params, children, widthRadius, startRadius, endRadian, color) {
+    var point = makeLabelPosition(params, widthRadius, startRadius, endRadian);
+    children.push({
+        type: 'text',
+        x: point[0],
+        y: point[1],
+        transition: [],
+        extra: {
+            startRadius: startRadius,
+            endRadian: endRadian,
+            widthRadius: widthRadius,
+            transition: ['startRadius', 'endRadian', 'widthRadius']
+        },
+        style: {
+            text: makeText(endRadian),
+            fill: color.inner,
+            stroke: '#fff',
+            lineWidth: 3,
+            fontSize: 16,
+            align: 'center',
+            verticalAlign: 'middle',
+            rich: {
+                round: { fontSize: 24 },
+                percent: { fontSize: 18 }
+            }
+        },
+        z2: 50,
+        during: function (apiDuring) {
+            var endRadian = apiDuring.getExtra('endRadian');
+            var point = makeLabelPosition(
+                params,
+                apiDuring.getExtra('widthRadius'),
+                apiDuring.getExtra('startRadius'),
+                endRadian
+            );
+            apiDuring.setTransform('x', point[0]).setTransform('y', point[1]);
+            apiDuring.setStyle('text', makeText(endRadian));
+        }
+    });
+
+    function makeText(endRadian) {
+        var radian = _startRadian - endRadian;
+        var PI2 = Math.PI * 2;
+        var round = Math.floor(radian / PI2);
+        var percent = (((radian / PI2) % 1) * 100).toFixed(1) + '%';
+        return 'Round {round|' + round + '}\n{percent|' + percent + '}';
+    }
+}
+
+function makeLabelPosition(params, widthRadius, startRadius, endRadian) {
+    var radiusStep = getRadiusStepByWidth(widthRadius);
+    var iRadius = getSpiralRadius(startRadius, endRadian, radiusStep);
+    return convertToPolarPoint(params, iRadius, endRadian - 10 / iRadius);
+}
+
+function convertToPolarPoint(renderItemParams, radius, radian) {
+    return [
+        Math.cos(radian) * radius + renderItemParams.coordSys.cx,
+        -Math.sin(radian) * radius + renderItemParams.coordSys.cy
+    ];
+}
+
+option = {
+    animationDuration: _animationDuration,
+    animationDurationUpdate: _animationDurationUpdate,
+    animationEasingUpdate: _animationEasingUpdate,
+    dataset: {
+        source: _datasourceList[_currentDataIndex]
+    },
+    tooltip: {},
+    angleAxis: {
+        type: 'value',
+        splitArea: { show: true },
+        axisLabel: {
+            formatter: function(val) {
+                return _radianLabels[val];
+            },
+            color: 'rgba(0,0,0,0.2)'
+        },
+        axisLine: { lineStyle: { color: 'rgba(0,0,0,0.2)' } },
+        min: 0,
+        max: _valOnRoundRadian
+    },
+    radiusAxis: {
+        type: 'value',
+        interval: 1,
+        splitLine: { show: false },
+        axisLabel: {
+            color: 'rgba(0,0,0,0.6)',
+            formatter: function (value) {
+                return _barNamesByOrdinal[value] || '';
+            }
+        },
+        axisTick: { show: false },
+        axisLine: { lineStyle: { color: 'rgba(0,0,0,0.2)' } },
+        min: 0,
+        max: getMaxRadius()
+    },
+    polar: {},
+    series: [{
+        type: 'custom',
+        coordinateSystem: 'polar',
+        renderItem: renderItem
+    }]
+};
+
+
+function next() {
+    ++_currentDataIndex;
+    myChart.setOption({
+        dataset: {
+            source: _datasourceList[_currentDataIndex]
+        },
+        radiusAxis: {
+            max: getMaxRadius()
+        }
+    });
+
+    if (_currentDataIndex < _datasourceList.length - 1) {
+        setTimeout(next, _animationDurationUpdate);
+    }
+}
+
+setTimeout(next, 1000);
+
diff --git a/public/data/custom-story-transition.js b/public/data/custom-story-transition.js
new file mode 100644
index 0000000..0095014
--- /dev/null
+++ b/public/data/custom-story-transition.js
@@ -0,0 +1,400 @@
+/*
+title: Simple Story Transition
+category: custom
+titleCN: 极简场景变换示例
+difficulty: 11
+*/
+
+const _global = {};
+$.get(ROOT_PATH + '/data/asset/js/myTransform.js', function (aggregateJS) {
+    (new Function(aggregateJS)).call(_global);
+
+    $.get(ROOT_PATH + '/data/asset/data/life-expectancy-table.json', function (_rawData) {
+        run(_rawData);
+    });
+});
+
+let _optionList;
+
+function run(_rawData) {
+    echarts.registerTransform(_global.myTransform.aggregate);
+    echarts.registerTransform(_global.myTransform.id);
+
+
+    const COLORS = [
+        '#37A2DA', '#e06343', '#37a354', '#b55dba', '#b5bd48', '#8378EA', '#96BFFF'
+    ];
+    const COUNTRY_A = 'Germany';
+    const COUNTRY_B = 'France';
+    const CONTENT_COLORS = {
+        [COUNTRY_A]: '#37a354',
+        [COUNTRY_B]: '#e06343'
+    };
+    const Z2 = {
+        [COUNTRY_A]: 1,
+        [COUNTRY_B]: 2
+    }
+
+
+    const ANIMATION_DURATION_UPDATE = 1000;
+    const AXIS_NAME_STYLE = {
+        nameGap: 25,
+        nameTextStyle: {
+            fontSize: 20
+        },
+    };
+
+
+    const baseOption = {
+        animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+        dataset: [{
+            id: 'DatasetRaw',
+            source: _rawData
+        }, {
+            id: 'DatasetRawWithId',
+            fromDatasetId: 'DatasetRaw',
+            transform: [{
+                type: 'filter',
+                config: {
+                    dimension: 'Year', gte: 1950
+                }
+            }, {
+                type: 'myTransform:id',
+                config: {
+                    dimensionIndex: 5,
+                    dimensionName: 'Id'
+                }
+            }]
+        }, {
+            id: 'DatasetCountryAB',
+            fromDatasetId: 'DatasetRawWithId',
+            transform: {
+                type: 'filter',
+                config: {
+                    or: [{
+                        dimension: 'Country', '=': COUNTRY_A
+                    }, {
+                        dimension: 'Country', '=': COUNTRY_B
+                    }]
+                }
+            }
+        }, {
+            id: 'DatasetCountryABSumIncome',
+            fromDatasetId: 'DatasetCountryAB',
+            transform: {
+                type: 'myTransform:aggregate',
+                config: {
+                    resultDimensions: [
+                        { from: 'Income', method: 'sum' },
+                        { from: 'Country' }
+                    ],
+                    groupBy: 'Country'
+                }
+            }
+        }],
+        tooltip: {}
+    };
+
+
+
+    const optionCreators = {
+
+        'Option_CountryAB_Year_Income_Bar': function (datasetId, specifiedCountry) {
+            return {
+                xAxis: {
+                    type: 'category',
+                    nameLocation: 'middle'
+                },
+                yAxis: {
+                    name: 'Income',
+                    ...AXIS_NAME_STYLE
+                },
+                series: {
+                    type: 'custom',
+                    coordinateSystem: 'cartesian2d',
+                    datasetId: datasetId,
+                    encode: {
+                        x: 'Year',
+                        y: 'Income',
+                        itemName: 'Year',
+                        tooltip: ['Income'],
+                    },
+                    renderItem: function (params, api) {
+                        const valPos = api.coord([api.value('Year'), api.value('Income')]);
+                        const basePos = api.coord([api.value('Year'), 0]);
+                        const width = api.size([1, 0])[0] * 0.9;
+
+                        const country = api.value('Country');
+                        if (specifiedCountry != null && specifiedCountry !== country) {
+                            return;
+                        }
+
+                        return {
+                            type: 'group',
+                            children: [{
+                                type: 'rect',
+                                transition: ['shape', 'style'],
+                                morph: true,
+                                shape: {
+                                    x: basePos[0],
+                                    y: basePos[1],
+                                    width: width,
+                                    height: valPos[1] - basePos[1]
+                                },
+                                z2: Z2[country],
+                                style: {
+                                    fill: CONTENT_COLORS[country],
+                                    opacity: 0.8
+                                }
+                            }]
+                        };
+                    }
+                }
+            };
+        },
+
+        'Option_CountryAB_Year_Population_Bar': function (datasetId, specifiedCountry) {
+            return {
+                xAxis: {
+                    type: 'category',
+                    nameLocation: 'middle'
+                },
+                yAxis: {
+                    name: 'Population',
+                    ...AXIS_NAME_STYLE
+                },
+                series: {
+                    type: 'custom',
+                    coordinateSystem: 'cartesian2d',
+                    datasetId: datasetId,
+                    encode: {
+                        x: 'Year',
+                        y: 'Population',
+                        itemName: 'Year',
+                        tooltip: ['Population'],
+                    },
+                    renderItem: function (params, api) {
+                        const valPos = api.coord([api.value('Year'), api.value('Population')]);
+                        const basePos = api.coord([api.value('Year'), 0]);
+                        const width = api.size([1, 0])[0] * 0.9;
+
+                        const country = api.value('Country');
+                        if (specifiedCountry != null && specifiedCountry !== country) {
+                            return;
+                        }
+
+                        return {
+                            type: 'group',
+                            children: [{
+                                type: 'rect',
+                                transition: ['shape', 'style'],
+                                morph: true,
+                                shape: {
+                                    x: basePos[0],
+                                    y: basePos[1],
+                                    width: width,
+                                    height: valPos[1] - basePos[1]
+                                },
+                                style: {
+                                    fill: CONTENT_COLORS[country]
+                                }
+                            }]
+                        };
+                    }
+                }
+            };
+        },
+
+        'Option_CountryAB_Income_Population_Scatter': function (datasetId, specifiedCountry) {
+            return {
+                xAxis: {
+                    name: 'Income',
+                    ...AXIS_NAME_STYLE,
+                    scale: true,
+                    nameLocation: 'middle'
+                },
+                yAxis: {
+                    name: 'Population',
+                    ...AXIS_NAME_STYLE,
+                    scale: true
+                },
+                series: {
+                    type: 'custom',
+                    coordinateSystem: 'cartesian2d',
+                    datasetId: datasetId,
+                    encode: {
+                        x: 'Income',
+                        y: 'Population',
+                        itemName: ['Year'],
+                        tooltip: ['Income', 'Population', 'Country']
+                    },
+                    renderItem: function (params, api) {
+                        const pos = api.coord([api.value('Income'), api.value('Population')]);
+
+                        const country = api.value('Country');
+                        if (specifiedCountry != null && specifiedCountry !== country) {
+                            return;
+                        }
+
+                        return {
+                            type: 'group',
+                            children: [{
+                                type: 'circle',
+                                transition: ['shape', 'style'],
+                                morph: true,
+                                shape: {
+                                    cx: pos[0],
+                                    cy: pos[1],
+                                    r: 10
+                                },
+                                style: {
+                                    fill: CONTENT_COLORS[country],
+                                    lineWidth: 1,
+                                    stroke: '#333',
+                                    opacity: 1,
+                                    enterFrom: {
+                                        opacity: 0
+                                    }
+                                }
+                            }]
+                        };
+                    }
+                }
+            };
+        },
+
+        'Option_CountryAB_Income_Sum_Bar': function (datasetId) {
+            return {
+                xAxis: {
+                    name: 'Income',
+                    ...AXIS_NAME_STYLE,
+                    nameLocation: 'middle'
+                },
+                yAxis: {
+                    type: 'category'
+                },
+                series: {
+                    type: 'custom',
+                    coordinateSystem: 'cartesian2d',
+                    datasetId: datasetId,
+                    encode: {
+                        x: 'Income',
+                        y: 'Country',
+                        itemName: ['Country'],
+                        tooltip: ['Income']
+                    },
+                    renderItem: function (params, api) {
+                        const country = api.ordinalRawValue('Country');
+                        const valPos = api.coord([api.value('Income'), country]);
+                        const basePos = api.coord([0, country]);
+                        const height = api.size([0, 1])[1] * 0.4;
+
+                        return {
+                            type: 'group',
+                            children: [{
+                                type: 'rect',
+                                transition: ['shape', 'style'],
+                                morph: true,
+                                shape: {
+                                    x: basePos[0],
+                                    y: valPos[1] - height / 2,
+                                    width: valPos[0] - basePos[0],
+                                    height: height
+                                },
+                                style: {
+                                    fill: CONTENT_COLORS[country]
+                                }
+                            }]
+                        };
+                    }
+                }
+            };
+        }
+
+    };
+
+    _optionList = [{
+        backwardTransitionOpt: {
+            from: { dimension: 'Id', seriesIndex: 0 },
+            to: { dimension: 'Id', seriesIndex: 0 }
+        },
+        option: optionCreators['Option_CountryAB_Year_Income_Bar']('DatasetCountryAB', COUNTRY_A)
+    }, {
+        backwardTransitionOpt: {
+            from: { dimension: 'Id', seriesIndex: 0 },
+            to: { dimension: 'Id', seriesIndex: 0 }
+        },
+        forwardTransitionOpt: {
+            from: { dimension: 'Id', seriesIndex: 0 },
+            to: { dimension: 'Id', seriesIndex: 0 }
+        },
+        option: optionCreators['Option_CountryAB_Year_Population_Bar']('DatasetCountryAB', COUNTRY_A)
+    }, {
+        backwardTransitionOpt: {
+            from: { dimension: 'Id', seriesIndex: 0 },
+            to: { dimension: 'Id', seriesIndex: 0 }
+        },
+        forwardTransitionOpt: {
+            from: { dimension: 'Id', seriesIndex: 0 },
+            to: { dimension: 'Id', seriesIndex: 0 }
+        },
+        option: optionCreators['Option_CountryAB_Income_Population_Scatter']('DatasetCountryAB', COUNTRY_A)
+    }, {
+        backwardTransitionOpt: {
+            from: { dimension: 'Country', seriesIndex: 0 },
+            to: { dimension: 'Country', seriesIndex: 0 }
+        },
+        forwardTransitionOpt: {
+            from: { dimension: 'Id', seriesIndex: 0 },
+            to: { dimension: 'Id', seriesIndex: 0 }
+        },
+        option: optionCreators['Option_CountryAB_Income_Population_Scatter']('DatasetCountryAB')
+    }, {
+        backwardTransitionOpt: {
+            from: { dimension: 'Country', seriesIndex: 0 },
+            to: { dimension: 'Country', seriesIndex: 0 }
+        },
+        forwardTransitionOpt: {
+            from: { dimension: 'Country', seriesIndex: 0 },
+            to: { dimension: 'Country', seriesIndex: 0 }
+        },
+        option: optionCreators['Option_CountryAB_Income_Sum_Bar']('DatasetCountryABSumIncome')
+    }, {
+        forwardTransitionOpt: {
+            from: { dimension: 'Country', seriesIndex: 0 },
+            to: { dimension: 'Country', seriesIndex: 0 }
+        },
+        option: optionCreators['Option_CountryAB_Year_Income_Bar']('DatasetCountryAB')
+    }];
+
+
+    myChart.setOption(baseOption, { lazyMode: true });
+
+    // Initialize state
+    myChart.setOption(_optionList[currentOptionIndex].option);
+}
+
+let currentOptionIndex = 0;
+app.config = {
+    'Next': function () {
+        if (!_optionList || currentOptionIndex >= _optionList.length - 1) {
+            return;
+        }
+        const optionWrap = _optionList[++currentOptionIndex];
+        myChart.setOption(optionWrap.option, {
+            replaceMerge: ['xAxis', 'yAxis'],
+            transition: optionWrap.forwardTransitionOpt
+        });
+    },
+
+    'Previous': function () {
+        if (!_optionList || currentOptionIndex <= 0) {
+            return;
+        }
+        const optionWrap = _optionList[--currentOptionIndex];
+        myChart.setOption(optionWrap.option, {
+            replaceMerge: ['xAxis', 'yAxis'],
+            transition: optionWrap.backwardTransitionOpt
+        });
+    }
+}
diff --git a/public/data/data-transform-filter.js b/public/data/data-transform-filter.js
new file mode 100644
index 0000000..219a99f
--- /dev/null
+++ b/public/data/data-transform-filter.js
@@ -0,0 +1,81 @@
+/*
+title: Data Transform Fitler
+category: line
+titleCN: 数据过滤
+difficulty: 3
+*/
+
+$.get(ROOT_PATH + '/data/asset/data/life-expectancy-table.json', function (_rawData) {
+    run(_rawData);
+});
+
+function run(_rawData) {
+
+    option = {
+        dataset: [{
+            id: 'dataset_raw',
+            source: _rawData
+        }, {
+            id: 'dataset_since_1950_of_germany',
+            fromDatasetId: 'dataset_raw',
+            transform: {
+                type: 'filter',
+                config: {
+                    and: [
+                        { dimension: 'Year', gte: 1950 },
+                        { dimension: 'Country', '=': 'Germany' }
+                    ]
+                }
+            }
+        }, {
+            id: 'dataset_since_1950_of_france',
+            fromDatasetId: 'dataset_raw',
+            transform: {
+                type: 'filter',
+                config: {
+                    and: [
+                        { dimension: 'Year', gte: 1950 },
+                        { dimension: 'Country', '=': 'France' }
+                    ]
+                }
+            }
+        }],
+        title: {
+            text: 'Income of Germany and France since 1950'
+        },
+        tooltip: {
+            trigger: 'axis'
+        },
+        xAxis: {
+            type: 'category',
+            nameLocation: 'middle'
+        },
+        yAxis: {
+            name: 'Income'
+        },
+        series: [{
+            type: 'line',
+            datasetId: 'dataset_since_1950_of_germany',
+            showSymbol: false,
+            encode: {
+                x: 'Year',
+                y: 'Income',
+                itemName: 'Year',
+                tooltip: ['Income'],
+            }
+        }, {
+            type: 'line',
+            datasetId: 'dataset_since_1950_of_france',
+            showSymbol: false,
+            encode: {
+                x: 'Year',
+                y: 'Income',
+                itemName: 'Year',
+                tooltip: ['Income'],
+            }
+        }]
+    };
+
+    myChart.setOption(option);
+
+}
diff --git a/public/data/scatter-linear-regression.js b/public/data/scatter-linear-regression.js
index 6fff303..6133ba6 100644
--- a/public/data/scatter-linear-regression.js
+++ b/public/data/scatter-linear-regression.js
@@ -228,7 +228,9 @@ option = {
         sublink: 'https://github.com/ecomfe/echarts-stat',
         left: 'center'
     },
-    legend: {},
+    legend: {
+        bottom: 5
+    },
     tooltip: {
         trigger: 'axis',
         axisPointer: {


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