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

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

commit e9a2b0f5aee017be1bf41726e6531c38530ce28e
Author: 100pah <sushuang0...@gmail.com>
AuthorDate: Fri Jul 31 13:02:59 2020 +0800

    feature: data transform
---
 src/chart/helper/createListFromArray.ts        |   9 +-
 src/chart/parallel/ParallelSeries.ts           |  30 +-
 src/component/dataset.ts                       |  46 +-
 src/{util/ecData.ts => component/transform.ts} |  22 +-
 src/component/transform/filterTransform.ts     | 101 ++++
 src/component/transform/sortTransform.ts       | 223 +++++++++
 src/component/visualMap/PiecewiseModel.ts      |   1 +
 src/data/List.ts                               |  45 +-
 src/data/Source.ts                             |  32 +-
 src/data/helper/dataProvider.ts                | 267 ++++++----
 src/data/helper/parseDataValue.ts              |  73 +++
 src/data/helper/sourceHelper.ts                | 219 ++++----
 src/data/helper/sourceManager.ts               | 368 ++++++++++++++
 src/data/helper/transform.ts                   | 317 ++++++++++++
 src/echarts.ts                                 |   3 +
 src/model/Component.ts                         |   4 +-
 src/model/OptionManager.ts                     |   4 +
 src/model/Series.ts                            |  16 +-
 src/util/conditionalExpression.ts              | 536 ++++++++++++++++++++
 src/util/ecData.ts                             |   4 +-
 src/util/log.ts                                |  67 ++-
 src/util/model.ts                              |   8 +-
 src/util/number.ts                             |   9 +-
 src/util/types.ts                              |   7 +-
 test/data-transform.html                       | 661 +++++++++++++++++++++++++
 25 files changed, 2760 insertions(+), 312 deletions(-)

diff --git a/src/chart/helper/createListFromArray.ts 
b/src/chart/helper/createListFromArray.ts
index e451cd3..c12ac24 100644
--- a/src/chart/helper/createListFromArray.ts
+++ b/src/chart/helper/createListFromArray.ts
@@ -28,13 +28,13 @@ import Source from '../../data/Source';
 import {enableDataStack} from '../../data/helper/dataStackHelper';
 import {makeSeriesEncodeForAxisCoordSys} from '../../data/helper/sourceHelper';
 import {
-    SOURCE_FORMAT_ORIGINAL, DimensionDefinitionLoose, DimensionDefinition, 
OptionSourceData
+    SOURCE_FORMAT_ORIGINAL, DimensionDefinitionLoose, DimensionDefinition, 
OptionSourceData, EncodeDefaulter
 } from '../../util/types';
 import SeriesModel from '../../model/Series';
 
 function createListFromArray(source: Source | OptionSourceData, seriesModel: 
SeriesModel, opt?: {
     generateCoord?: string
-    useEncodeDefaulter?: boolean
+    useEncodeDefaulter?: boolean | EncodeDefaulter
 }): List {
     opt = opt || {};
 
@@ -73,10 +73,13 @@ function createListFromArray(source: Source | 
OptionSourceData, seriesModel: Ser
         )) || ['x', 'y'];
     }
 
+    const useEncodeDefaulter = opt.useEncodeDefaulter;
     const dimInfoList = createDimensions(source, {
         coordDimensions: coordSysDimDefs,
         generateCoord: opt.generateCoord,
-        encodeDefaulter: opt.useEncodeDefaulter
+        encodeDefaulter: zrUtil.isFunction(useEncodeDefaulter)
+            ? useEncodeDefaulter
+            : useEncodeDefaulter
             ? zrUtil.curry(makeSeriesEncodeForAxisCoordSys, coordSysDimDefs, 
seriesModel)
             : null
     });
diff --git a/src/chart/parallel/ParallelSeries.ts 
b/src/chart/parallel/ParallelSeries.ts
index d0b5a38..287bc2f 100644
--- a/src/chart/parallel/ParallelSeries.ts
+++ b/src/chart/parallel/ParallelSeries.ts
@@ -18,7 +18,7 @@
 */
 
 
-import {each, createHashMap} from 'zrender/src/core/util';
+import {each, bind} from 'zrender/src/core/util';
 import SeriesModel from '../../model/Series';
 import createListFromArray from '../helper/createListFromArray';
 import {
@@ -29,13 +29,15 @@ import {
     SeriesTooltipOption,
     DimensionName,
     OptionDataValue,
-    StatesOptionMixin
+    StatesOptionMixin,
+    OptionEncodeValue,
+    Dictionary,
+    OptionEncode
  } from '../../util/types';
 import GlobalModel from '../../model/Global';
 import List from '../../data/List';
 import { ParallelActiveState, ParallelAxisOption } from 
'../../coord/parallel/AxisModel';
 import Parallel from '../../coord/parallel/Parallel';
-import Source from '../../data/Source';
 import ParallelModel from '../../coord/parallel/ParallelModel';
 
 type ParallelSeriesDataValue = OptionDataValue[];
@@ -89,12 +91,10 @@ class ParallelSeriesModel extends 
SeriesModel<ParallelSeriesOption> {
     coordinateSystem: Parallel;
 
 
-    getInitialData(option: ParallelSeriesOption, ecModel: GlobalModel): List {
-        const source = this.getSource();
-
-        setEncodeAndDimensions(source, this);
-
-        return createListFromArray(source, this);
+    getInitialData(this: ParallelSeriesModel, option: ParallelSeriesOption, 
ecModel: GlobalModel): List {
+        return createListFromArray(this.getSource(), this, {
+            useEncodeDefaulter: bind(makeDefaultEncode, null, this)
+        });
     }
 
     /**
@@ -151,7 +151,7 @@ class ParallelSeriesModel extends 
SeriesModel<ParallelSeriesOption> {
 
 SeriesModel.registerClass(ParallelSeriesModel);
 
-function setEncodeAndDimensions(source: Source, seriesModel: 
ParallelSeriesModel): void {
+function makeDefaultEncode(seriesModel: ParallelSeriesModel): OptionEncode {
     // The mapping of parallelAxis dimension to data dimension can
     // be specified in parallelAxis.option.dim. For example, if
     // parallelAxis.option.dim is 'dim3', it mapping to the third
@@ -159,10 +159,6 @@ function setEncodeAndDimensions(source: Source, 
seriesModel: ParallelSeriesModel
     // Moreover, parallelModel.dimension should not be regarded as data
     // dimensions. Consider dimensions = ['dim4', 'dim2', 'dim6'];
 
-    if (source.encodeDefine) {
-        return;
-    }
-
     const parallelModel = seriesModel.ecModel.getComponent(
         'parallel', seriesModel.get('parallelIndex')
     ) as ParallelModel;
@@ -170,11 +166,13 @@ function setEncodeAndDimensions(source: Source, 
seriesModel: ParallelSeriesModel
         return;
     }
 
-    const encodeDefine = source.encodeDefine = createHashMap();
+    const encodeDefine: Dictionary<OptionEncodeValue> = {};
     each(parallelModel.dimensions, function (axisDim) {
         const dataDimIndex = convertDimNameToNumber(axisDim);
-        encodeDefine.set(axisDim, dataDimIndex);
+        encodeDefine[axisDim] = dataDimIndex;
     });
+
+    return encodeDefine;
 }
 
 function convertDimNameToNumber(dimName: DimensionName): number {
diff --git a/src/component/dataset.ts b/src/component/dataset.ts
index 8e16f47..26b77ec 100644
--- a/src/component/dataset.ts
+++ b/src/component/dataset.ts
@@ -28,22 +28,33 @@
 
 import ComponentModel from '../model/Component';
 import ComponentView from '../view/Component';
-import {detectSourceFormat} from '../data/helper/sourceHelper';
 import {
-    SERIES_LAYOUT_BY_COLUMN, ComponentOption, SeriesEncodeOptionMixin, 
OptionSourceData, SeriesLayoutBy
+    SERIES_LAYOUT_BY_COLUMN, ComponentOption, SeriesEncodeOptionMixin,
+    OptionSourceData, SeriesLayoutBy, OptionSourceHeader
 } from '../util/types';
+import { DataTransformOption, PipedDataTransformOption } from 
'../data/helper/transform';
+import GlobalModel from '../model/Global';
+import Model from '../model/Model';
+import { disableTransformOptionMerge, SourceManager } from 
'../data/helper/sourceManager';
 
 
-interface DatasetOption extends
+export interface DatasetOption extends
         Pick<ComponentOption, 'type' | 'id' | 'name'>,
         Pick<SeriesEncodeOptionMixin, 'dimensions'> {
     seriesLayoutBy?: SeriesLayoutBy;
-    // null/undefined/'auto': auto detect header, see 
"src/data/helper/sourceHelper".
-    sourceHeader?: boolean | 'auto';
-    data?: OptionSourceData;
+    sourceHeader?: OptionSourceHeader;
+    source?: OptionSourceData;
+
+    fromDatasetIndex?: number;
+    fromDatasetId?: string;
+    transform?: DataTransformOption | PipedDataTransformOption;
+    // When a transform result more than on results, the results can be 
referenced only by:
+    // Using `fromDatasetIndex`/`fromDatasetId` and `transfromResultIndex` to 
retrieve
+    // the results from other dataset.
+    fromTransformResult?: number;
 }
 
-class DatasetModel extends ComponentModel {
+export class DatasetModel<Opts extends DatasetOption = DatasetOption> extends 
ComponentModel<Opts> {
 
     type = 'dataset';
     static type = 'dataset';
@@ -52,18 +63,33 @@ class DatasetModel extends ComponentModel {
         seriesLayoutBy: SERIES_LAYOUT_BY_COLUMN
     };
 
+    private _sourceManager: SourceManager;
+
+    init(option: Opts, parentModel: Model, ecModel: GlobalModel): void {
+        super.init(option, parentModel, ecModel);
+        this._sourceManager = new SourceManager(this);
+        disableTransformOptionMerge(this);
+    }
+
+    mergeOption(newOption: Opts, ecModel: GlobalModel): void {
+        super.mergeOption(newOption, ecModel);
+        disableTransformOptionMerge(this);
+    }
+
     optionUpdated() {
-        detectSourceFormat(this);
+        this._sourceManager.dirty();
+    }
+
+    getSourceManager() {
+        return this._sourceManager;
     }
 }
 
 ComponentModel.registerClass(DatasetModel);
 
-
 class DatasetView extends ComponentView {
     static type = 'dataset';
     type = 'dataset';
 }
 
 ComponentView.registerClass(DatasetView);
-
diff --git a/src/util/ecData.ts b/src/component/transform.ts
similarity index 63%
copy from src/util/ecData.ts
copy to src/component/transform.ts
index 2dc31fc..d679f8c 100644
--- a/src/util/ecData.ts
+++ b/src/component/transform.ts
@@ -17,19 +17,9 @@
 * under the License.
 */
 
-import Element from 'zrender/src/Element';
-import { DataModel, ECEventData, BlurScope, InnerFocus } from './types';
-import { makeInner } from './model';
-/**
- * ECData stored on graphic element
- */
-export interface ECData {
-    dataIndex?: number;
-    dataModel?: DataModel;
-    eventData?: ECEventData;
-    seriesIndex?: number;
-    dataType?: string;
-    focus?: InnerFocus;
-    blurScope?: BlurScope;
-}
-export const getECData = makeInner<ECData, Element>();
+import * as echarts from '../echarts';
+import {filterTransform} from './transform/filterTransform';
+import {sortTransform} from './transform/sortTransform';
+
+echarts.registerTransform(filterTransform);
+echarts.registerTransform(sortTransform);
diff --git a/src/component/transform/filterTransform.ts 
b/src/component/transform/filterTransform.ts
new file mode 100644
index 0000000..4b13008
--- /dev/null
+++ b/src/component/transform/filterTransform.ts
@@ -0,0 +1,101 @@
+/*
+* 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.
+*/
+
+import { DataTransformOption, ExternalDataTransform } from 
'../../data/helper/transform';
+import { DimensionIndex, OptionDataItem } from '../../util/types';
+import { parseConditionalExpression, ConditionalExpressionOption } from 
'../../util/conditionalExpression';
+import { hasOwn, createHashMap } from 'zrender/src/core/util';
+import { makePrintable, throwError } from '../../util/log';
+
+
+export interface FilterTransformOption extends DataTransformOption {
+    type: 'filter';
+    config: ConditionalExpressionOption;
+}
+
+export const filterTransform: ExternalDataTransform<FilterTransformOption> = {
+
+    type: 'echarts:filter',
+
+    // PEDING: enhance to filter by index rather than create new data
+    transform: function transform(params) {
+        // [Caveat] Fail-Fast:
+        // Do not return the whole dataset unless user config indicate it 
explicitly.
+        // 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;
+        let rawItem: OptionDataItem;
+
+        const condition = parseConditionalExpression<{ dimIdx: DimensionIndex 
}>(params.config, {
+
+            valueGetterAttrMap: createHashMap<boolean, string>({ dimension: 
true }),
+
+            prepareGetValue: function (exprOption) {
+                let errMsg = '';
+                const dimLoose = exprOption.dimension;
+                if (!hasOwn(exprOption, 'dimension')) {
+                    if (__DEV__) {
+                        errMsg = makePrintable(
+                            'Relation condition must has prop "dimension" 
specified.',
+                            'Illegal condition:', exprOption
+                        );
+                    }
+                    throwError(errMsg);
+                }
+
+                const dimInfo = source.getDimensionInfo(dimLoose);
+                if (!dimInfo) {
+                    if (__DEV__) {
+                        errMsg = makePrintable(
+                            'Can not find dimension info via: "' + dimLoose + 
'".\n',
+                            'Existing dimensions: ', source.dimensions, '.\n',
+                            'Illegal condition:', exprOption, '.\n'
+                        );
+                    }
+                    throwError(errMsg);
+                }
+
+                return { dimIdx: dimInfo.index };
+            },
+
+            getValue: function (param) {
+                return source.retrieveItemValue(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);
+            if (condition.evaluate()) {
+                resultData.push(rawItem);
+            }
+        }
+
+        return {
+            data: resultData,
+            dimensions: source.dimensions,
+            sourceHeader: sourceHeaderCount
+        };
+    }
+};
diff --git a/src/component/transform/sortTransform.ts 
b/src/component/transform/sortTransform.ts
new file mode 100644
index 0000000..9dfce97
--- /dev/null
+++ b/src/component/transform/sortTransform.ts
@@ -0,0 +1,223 @@
+/*
+* 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.
+*/
+
+import { DataTransformOption, ExternalDataTransform } from 
'../../data/helper/transform';
+import {
+    DimensionLoose, SOURCE_FORMAT_KEYED_COLUMNS, DimensionIndex, 
OptionDataValue
+} from '../../util/types';
+import { makePrintable, throwError } from '../../util/log';
+import { isArray, each, hasOwn } from 'zrender/src/core/util';
+import { normalizeToArray } from '../../util/model';
+import { parseDate } from '../../util/number';
+
+/**
+ * @usage
+ *
+ * ```js
+ * transform: {
+ *     type: 'sort',
+ *     config: { dimension: 'score', order: 'asc' }
+ * }
+ * transform: {
+ *     type: 'sort',
+ *     config: [
+ *         { dimension: 1, order: 'asc' },
+ *         { dimension: 'age', order: 'desc' }
+ *     ]
+ * }
+ * ```
+ */
+
+export interface SortTransformOption extends DataTransformOption {
+    type: 'sort';
+    config: OrderExpression | OrderExpression[];
+}
+
+// PENDING: whether support { dimension: 'score', order: 'asc' } ?
+type OrderExpression = {
+    dimension: DimensionLoose;
+    order: SortOrder;
+    parse?: 'time'
+};
+
+type SortOrder = 'asc' | 'desc';
+const SortOrderValidMap = { asc: true, desc: true } as const;
+
+let sampleLog = '';
+if (__DEV__) {
+    sampleLog = [
+        'Valid config is like:',
+        '{ dimension: "age", order: "asc" }',
+        'or [{ dimension: "age", order: "asc"], { dimension: "date", order: 
"desc" }]'
+    ].join('');
+}
+
+const timeParser = function (val: OptionDataValue): number {
+    return +parseDate(val);
+};
+
+
+export const sortTransform: ExternalDataTransform<SortTransformOption> = {
+
+    type: 'echarts:sort',
+
+    transform: function transform(params) {
+        const source = params.source;
+        const config = params.config;
+        let errMsg = '';
+
+        // Normalize
+        // const orderExprList: OrderExpression[] = isArray(config[0])
+        //     ? config as OrderExpression[]
+        //     : [config as OrderExpression];
+        const orderExprList: OrderExpression[] = normalizeToArray(config);
+
+        if (!orderExprList.length) {
+            if (__DEV__) {
+                errMsg = 'Empty `config` in sort transform.';
+            }
+            throwError(errMsg);
+        }
+
+        const orderDefList: {
+            dimIdx: DimensionIndex;
+            orderReturn: -1 | 1;
+            parser: (val: OptionDataValue) => number;
+        }[] = [];
+        each(orderExprList, function (orderExpr) {
+            const dimLoose = orderExpr.dimension;
+            const order = orderExpr.order;
+            const parserName = orderExpr.parse;
+
+            if (dimLoose == null) {
+                if (__DEV__) {
+                    errMsg = 'Sort transform config must has "dimension" 
specified.' + sampleLog;
+                }
+                throwError(errMsg);
+            }
+
+            if (!hasOwn(SortOrderValidMap, order)) {
+                if (__DEV__) {
+                    errMsg = 'Sort transform config must has "order" 
specified.' + sampleLog;
+                }
+                throwError(errMsg);
+            }
+
+            const dimInfo = source.getDimensionInfo(dimLoose);
+            if (!dimInfo) {
+                if (__DEV__) {
+                    errMsg = makePrintable(
+                        'Can not find dimension info via: "' + dimLoose + 
'".\n',
+                        'Existing dimensions: ', source.dimensions, '.\n',
+                        'Illegal config:', orderExpr, '.\n'
+                    );
+                }
+                throwError(errMsg);
+            }
+
+            let parser;
+            if (parserName) {
+                if (parserName !== 'time') {
+                    if (__DEV__) {
+                        errMsg = makePrintable(
+                            'Invalid parser name' + parserName + '.\n',
+                            'Illegal config:', orderExpr, '.\n'
+                        );
+                    }
+                    throwError(errMsg);
+                }
+                parser = timeParser;
+            }
+
+            orderDefList.push({
+                dimIdx: dimInfo.index,
+                orderReturn: order === 'asc' ? -1 : 1,
+                parser: parser
+            });
+        });
+
+        // TODO: support it?
+        if (!isArray(source.data)) {
+            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.';
+            }
+            throwError(errMsg);
+        }
+
+        // Other source format are all array.
+        const sourceHeaderCount = source.sourceHeaderCount;
+        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));
+        }
+
+        resultData.sort(function (item0, item1) {
+            if (item0 === headerPlaceholder) {
+                return -1;
+            }
+            if (item1 === headerPlaceholder) {
+                return 1;
+            }
+            // FIXME: check other empty?
+            // Always put empty item last?
+            if (item0 == null) {
+                return 1;
+            }
+            if (item1 == null) {
+                return -1;
+            }
+            // TODO Optimize a little: manually loop unrolling?
+            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);
+                if (orderDef.parser) {
+                    val0 = orderDef.parser(val0);
+                    val1 = orderDef.parser(val1);
+                }
+                if (val0 < val1) {
+                    return orderDef.orderReturn;
+                }
+                else if (val0 > val1) {
+                    return -orderDef.orderReturn;
+                }
+            }
+            return 0;
+        });
+
+        for (let i = 0; i < sourceHeaderCount; i++) {
+            resultData[i] = source.getRawHeaderItem(i);
+        }
+
+        return {
+            data: resultData,
+            dimensions: source.dimensions,
+            sourceHeader: sourceHeaderCount
+        };
+    }
+};
+
diff --git a/src/component/visualMap/PiecewiseModel.ts 
b/src/component/visualMap/PiecewiseModel.ts
index 64e5f5e..d8d0785 100644
--- a/src/component/visualMap/PiecewiseModel.ts
+++ b/src/component/visualMap/PiecewiseModel.ts
@@ -28,6 +28,7 @@ import ComponentModel from '../../model/Component';
 import { inheritDefaultOption } from '../../util/component';
 
 
+// TODO: use `relationExpression.ts` instead
 interface VisualPiece extends VisualOptionPiecewise {
     min?: number
     max?: number
diff --git a/src/data/List.ts b/src/data/List.ts
index b40492c..871f5fd 100644
--- a/src/data/List.ts
+++ b/src/data/List.ts
@@ -36,13 +36,13 @@ import {
     DimensionIndex, DimensionName, DimensionLoose, OptionDataItem,
     ParsedValue, ParsedValueNumeric, OrdinalNumber, DimensionUserOuput, 
ModelOption, SeriesDataType
 } from '../util/types';
-import {parseDate} from '../util/number';
 import {isDataItemOption} from '../util/model';
 import { getECData } from '../util/ecData';
 import { PathStyleProps } from 'zrender/src/graphic/Path';
 import type Graph from './Graph';
 import type Tree from './Tree';
 import type { VisualMeta } from '../component/visualMap/VisualMapModel';
+import { parseDataValue } from './helper/parseDataValue';
 
 
 const isObject = zrUtil.isObject;
@@ -1916,7 +1916,7 @@ class List<
             objectRows: function (
                 this: List, dataItem: Dictionary<any>, dimName: string, 
dataIndex: number, dimIndex: number
             ): ParsedValue {
-                return convertDataValue(dataItem[dimName], 
this._dimensionInfos[dimName]);
+                return parseDataValue(dataItem[dimName], 
this._dimensionInfos[dimName]);
             },
 
             keyedColumns: getDimValueSimply,
@@ -1934,7 +1934,7 @@ class List<
                 if (!this._rawData.pure && isDataItemOption(dataItem)) {
                     this.hasItemOption = true;
                 }
-                return convertDataValue(
+                return parseDataValue(
                     (value instanceof Array)
                         ? value[dimIndex]
                         // If value is a single number or something else not 
array.
@@ -1954,43 +1954,8 @@ class List<
         function getDimValueSimply(
             this: List, dataItem: any, dimName: string, dataIndex: number, 
dimIndex: number
         ): ParsedValue {
-            return convertDataValue(dataItem[dimIndex], 
this._dimensionInfos[dimName]);
-        }
-
-        /**
-         * Convert raw the value in to inner value in List.
-         * [Caution]: this is the key logic of user value parser.
-         * For backward compatibiliy, do not modify it until have to.
-         */
-        function convertDataValue(value: any, dimInfo: DataDimensionInfo): 
ParsedValue {
-            // Performance sensitive.
-            const dimType = dimInfo && dimInfo.type;
-            if (dimType === 'ordinal') {
-                // If given value is a category string
-                const ordinalMeta = dimInfo && dimInfo.ordinalMeta;
-                return ordinalMeta
-                    ? ordinalMeta.parseAndCollect(value)
-                    : value;
-            }
-
-            if (dimType === 'time'
-                // spead up when using timestamp
-                && typeof value !== 'number'
-                && value != null
-                && value !== '-'
-            ) {
-                value = +parseDate(value);
-            }
-
-            // dimType defaults 'number'.
-            // If dimType is not ordinal and value is null or undefined or NaN 
or '-',
-            // parse to NaN.
-            return (value == null || value === '')
-                ? NaN
-                // If string (like '-'), using '+' parse to NaN
-                // If object, also parse to NaN
-                : +value;
-        };
+            return parseDataValue(dataItem[dimIndex], 
this._dimensionInfos[dimName]);
+        }
 
         prepareInvertedIndex = function (list: List): void {
             const invertedIndicesMap = list._invertedIndicesMap;
diff --git a/src/data/Source.ts b/src/data/Source.ts
index 10d62af..bed37d6 100644
--- a/src/data/Source.ts
+++ b/src/data/Source.ts
@@ -67,11 +67,8 @@ import {
 
 class Source {
 
-    readonly fromDataset: boolean;
-
     /**
      * Not null/undefined.
-     * @type {Array|Object}
      */
     readonly data: OptionSourceData;
 
@@ -98,7 +95,7 @@ class Source {
      * can be null/undefined.
      * Might be specified outside.
      */
-    encodeDefine: HashMap<OptionEncodeValue, DimensionName>;
+    readonly encodeDefine: HashMap<OptionEncodeValue, DimensionName>;
 
     /**
      * Not null/undefined, uint.
@@ -112,27 +109,33 @@ class Source {
 
 
     constructor(fields: {
-        fromDataset: boolean,
-        data?: OptionSourceData,
-        sourceFormat?: SourceFormat, // default: SOURCE_FORMAT_UNKNOWN
+        data: OptionSourceData,
+        sourceFormat: SourceFormat, // default: SOURCE_FORMAT_UNKNOWN
+
+        // Visit config are optional:
         seriesLayoutBy?: SeriesLayoutBy, // default: 'column'
         dimensionsDefine?: DimensionDefinition[],
-        encodeDefine?: OptionEncode,
         startIndex?: number, // default: 0
-        dimensionsDetectCount?: number
+        dimensionsDetectCount?: number,
+
+        // [Caveat]
+        // This is the raw user defined `encode` in `series`.
+        // If user not defined, DO NOT make a empty object or hashMap here.
+        // An empty object or hashMap will prevent from auto generating encode.
+        encodeDefine?: HashMap<OptionEncodeValue, DimensionName>
     }) {
 
-        this.fromDataset = fields.fromDataset;
         this.data = fields.data || (
             fields.sourceFormat === SOURCE_FORMAT_KEYED_COLUMNS ? {} : []
         );
         this.sourceFormat = fields.sourceFormat || SOURCE_FORMAT_UNKNOWN;
+
+        // Visit config
         this.seriesLayoutBy = fields.seriesLayoutBy || SERIES_LAYOUT_BY_COLUMN;
-        this.dimensionsDefine = fields.dimensionsDefine;
-        this.encodeDefine = fields.encodeDefine
-            && createHashMap<OptionEncodeValue, 
DimensionName>(fields.encodeDefine);
         this.startIndex = fields.startIndex || 0;
+        this.dimensionsDefine = fields.dimensionsDefine;
         this.dimensionsDetectCount = fields.dimensionsDetectCount;
+        this.encodeDefine = fields.encodeDefine;
     }
 
     /**
@@ -143,8 +146,7 @@ class Source {
             data: data,
             sourceFormat: isTypedArray(data)
                 ? SOURCE_FORMAT_TYPED_ARRAY
-                : SOURCE_FORMAT_ORIGINAL,
-            fromDataset: false
+                : SOURCE_FORMAT_ORIGINAL
         });
     };
 }
diff --git a/src/data/helper/dataProvider.ts b/src/data/helper/dataProvider.ts
index 3363597..788610a 100644
--- a/src/data/helper/dataProvider.ts
+++ b/src/data/helper/dataProvider.ts
@@ -21,7 +21,7 @@
 // ??? refactor? check the outer usage of data provider.
 // merge with defaultDimValueGetter?
 
-import {isTypedArray, extend, assert, each, isObject} from 
'zrender/src/core/util';
+import {isTypedArray, extend, assert, each, isObject, bind} from 
'zrender/src/core/util';
 import {getDataItemValue} from '../../util/model';
 import Source from '../Source';
 import {ArrayLike, Dictionary} from 'zrender/src/core/types';
@@ -34,7 +34,7 @@ import {
     SERIES_LAYOUT_BY_COLUMN,
     SERIES_LAYOUT_BY_ROW,
     DimensionName, DimensionIndex, OptionSourceData,
-    DimensionIndexLoose, OptionDataItem, OptionDataValue
+    DimensionIndexLoose, OptionDataItem, OptionDataValue, DimensionDefinition, 
SourceFormat, SeriesLayoutBy
 } from '../../util/types';
 import List from '../List';
 
@@ -54,6 +54,7 @@ export interface DataProvider {
 
 
 let providerMethods: Dictionary<any>;
+let mountMethods: (provider: DefaultDataProvider, data: OptionSourceData, 
source: Source) => void;
 
 /**
  * If normal array used, mutable chunk size is supported.
@@ -90,12 +91,10 @@ export class DefaultDataProvider implements DataProvider {
 
         // declare source is Source;
         this._source = source;
-
         const data = this._data = source.data;
-        const sourceFormat = source.sourceFormat;
 
         // Typed array. TODO IE10+?
-        if (sourceFormat === SOURCE_FORMAT_TYPED_ARRAY) {
+        if (source.sourceFormat === SOURCE_FORMAT_TYPED_ARRAY) {
             if (__DEV__) {
                 if (dimSize == null) {
                     throw new Error('Typed array data must specify dimension 
size');
@@ -106,17 +105,7 @@ export class DefaultDataProvider implements DataProvider {
             this._data = data;
         }
 
-        const methods = providerMethods[
-            sourceFormat === SOURCE_FORMAT_ARRAY_ROWS
-            ? sourceFormat + '_' + source.seriesLayoutBy
-            : sourceFormat
-        ];
-
-        if (__DEV__) {
-            assert(methods, 'Invalide sourceFormat: ' + sourceFormat);
-        }
-
-        extend(this, methods);
+        mountMethods(this, data, source);
     }
 
     getSource(): Source {
@@ -127,7 +116,7 @@ export class DefaultDataProvider implements DataProvider {
         return 0;
     }
 
-    getItem(idx: number): OptionDataItem {
+    getItem(idx: number, out?: ArrayLike<number>): OptionDataItem {
         return;
     }
 
@@ -139,35 +128,58 @@ export class DefaultDataProvider implements DataProvider {
 
     private static internalField = (function () {
 
+        mountMethods = function (provider, data, source) {
+            const sourceFormat = source.sourceFormat;
+            const seriesLayoutBy = source.seriesLayoutBy;
+            const startIndex = source.startIndex;
+            const dimsDef = source.dimensionsDefine;
+
+            const methods = providerMethods[getMethodMapKey(sourceFormat, 
seriesLayoutBy)];
+            if (__DEV__) {
+                assert(methods, 'Invalide sourceFormat: ' + sourceFormat);
+            }
+
+            extend(provider, methods);
+
+            if (sourceFormat === SOURCE_FORMAT_TYPED_ARRAY) {
+                provider.getItem = getItemForTypedArray;
+                provider.count = countForTypedArray;
+            }
+            else {
+                const rawItemGetter = getRawSourceItemGetter(sourceFormat, 
seriesLayoutBy);
+                provider.getItem = bind(rawItemGetter, null, data, startIndex, 
dimsDef);
+                const rawCounter = getRawSourceDataCounter(sourceFormat, 
seriesLayoutBy);
+                provider.count = bind(rawCounter, null, data, startIndex, 
dimsDef);
+            }
+        };
+
+        const getItemForTypedArray: DefaultDataProvider['getItem'] = function (
+            this: DefaultDataProvider, idx: number, out: ArrayLike<number>
+        ): ArrayLike<number> {
+            idx = idx - this._offset;
+            out = out || [];
+            const offset = this._dimSize * idx;
+            for (let i = 0; i < this._dimSize; i++) {
+                out[i] = (this._data as ArrayLike<number>)[offset + i];
+            }
+            return out;
+        };
+
+        const countForTypedArray: DefaultDataProvider['count'] = function (
+            this: DefaultDataProvider
+        ) {
+            return this._data ? ((this._data as ArrayLike<number>).length / 
this._dimSize) : 0;
+        };
+
         providerMethods = {
 
             [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_COLUMN]: {
                 pure: true,
-                count: function (this: DefaultDataProvider): number {
-                    return Math.max(0, (this._data as 
OptionDataItem[][]).length - this._source.startIndex);
-                },
-                getItem: function (this: DefaultDataProvider, idx: number): 
OptionDataValue[] {
-                    return (this._data as OptionDataValue[][])[idx + 
this._source.startIndex];
-                },
                 appendData: appendDataSimply
             },
 
             [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_ROW]: {
                 pure: true,
-                count: function (this: DefaultDataProvider): number {
-                    const row = (this._data as OptionDataValue[][])[0];
-                    return row ? Math.max(0, row.length - 
this._source.startIndex) : 0;
-                },
-                getItem: function (this: DefaultDataProvider, idx: number): 
OptionDataValue[] {
-                    idx += this._source.startIndex;
-                    const item = [];
-                    const data = this._data as OptionDataValue[][];
-                    for (let i = 0; i < data.length; i++) {
-                        const row = data[i];
-                        item.push(row ? row[idx] : null);
-                    }
-                    return item;
-                },
                 appendData: function () {
                     throw new Error('Do not support appendData when set 
seriesLayoutBy: "row".');
                 }
@@ -175,27 +187,11 @@ export class DefaultDataProvider implements DataProvider {
 
             [SOURCE_FORMAT_OBJECT_ROWS]: {
                 pure: true,
-                count: countSimply,
-                getItem: getItemSimply,
                 appendData: appendDataSimply
             },
 
             [SOURCE_FORMAT_KEYED_COLUMNS]: {
                 pure: true,
-                count: function (this: DefaultDataProvider): number {
-                    const dimName = this._source.dimensionsDefine[0].name;
-                    const col = (this._data as 
Dictionary<OptionDataValue[]>)[dimName];
-                    return col ? col.length : 0;
-                },
-                getItem: function (this: DefaultDataProvider, idx: number): 
OptionDataValue[] {
-                    const item = [];
-                    const dims = this._source.dimensionsDefine;
-                    for (let i = 0; i < dims.length; i++) {
-                        const col = (this._data as 
Dictionary<OptionDataValue[]>)[dims[i].name];
-                        item.push(col ? col[idx] : null);
-                    }
-                    return item;
-                },
                 appendData: function (this: DefaultDataProvider, newData: 
Dictionary<OptionDataValue[]>) {
                     const data = this._data as Dictionary<OptionDataValue[]>;
                     each(newData, function (newCol, key) {
@@ -208,26 +204,12 @@ export class DefaultDataProvider implements DataProvider {
             },
 
             [SOURCE_FORMAT_ORIGINAL]: {
-                count: countSimply,
-                getItem: getItemSimply,
                 appendData: appendDataSimply
             },
 
             [SOURCE_FORMAT_TYPED_ARRAY]: {
                 persistent: false,
                 pure: true,
-                count: function (this: DefaultDataProvider): number {
-                    return this._data ? ((this._data as 
ArrayLike<number>).length / this._dimSize) : 0;
-                },
-                getItem: function (this: DefaultDataProvider, idx: number, 
out: ArrayLike<number>): ArrayLike<number> {
-                    idx = idx - this._offset;
-                    out = out || [];
-                    const offset = this._dimSize * idx;
-                    for (let i = 0; i < this._dimSize; i++) {
-                        out[i] = (this._data as ArrayLike<number>)[offset + i];
-                    }
-                    return out;
-                },
                 appendData: function (this: DefaultDataProvider, newData: 
ArrayLike<number>): void {
                     if (__DEV__) {
                         assert(
@@ -235,7 +217,6 @@ export class DefaultDataProvider implements DataProvider {
                             'Added data must be TypedArray if data in 
initialization is TypedArray'
                         );
                     }
-
                     this._data = newData;
                 },
 
@@ -248,12 +229,6 @@ export class DefaultDataProvider implements DataProvider {
             }
         };
 
-        function countSimply(this: DefaultDataProvider): number {
-            return (this._data as []).length;
-        }
-        function getItemSimply(this: DefaultDataProvider, idx: number): 
OptionDataItem {
-            return (this._data as [])[idx];
-        }
         function appendDataSimply(this: DefaultDataProvider, newData: 
ArrayLike<OptionDataItem>): void {
             for (let i = 0; i < newData.length; i++) {
                 (this._data as any[]).push(newData[i]);
@@ -262,23 +237,136 @@ export class DefaultDataProvider implements DataProvider 
{
 
     })();
 }
+
+
+
+type RawSourceItemGetter = (
+    rawData: OptionSourceData,
+    startIndex: number,
+    dimsDef: DimensionDefinition[],
+    idx: number
+) => OptionDataItem;
+
+const getItemSimply: RawSourceItemGetter = function (
+    rawData, startIndex, dimsDef, idx
+): OptionDataItem {
+    return (rawData as [])[idx];
+};
+
+const rawSourceItemGetterMap: Dictionary<RawSourceItemGetter> = {
+    [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_COLUMN]: function (
+        rawData, startIndex, dimsDef, idx
+    ): OptionDataValue[] {
+        return (rawData as OptionDataValue[][])[idx + startIndex];
+    },
+    [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_ROW]: function (
+        rawData, startIndex, dimsDef, idx
+    ): OptionDataValue[] {
+        idx += startIndex;
+        const item = [];
+        const data = rawData as OptionDataValue[][];
+        for (let i = 0; i < data.length; i++) {
+            const row = data[i];
+            item.push(row ? row[idx] : null);
+        }
+        return item;
+    },
+    [SOURCE_FORMAT_OBJECT_ROWS]: getItemSimply,
+    [SOURCE_FORMAT_KEYED_COLUMNS]: function (
+        rawData, startIndex, dimsDef, idx
+    ): OptionDataValue[] {
+        const item = [];
+        for (let i = 0; i < dimsDef.length; i++) {
+            const col = (rawData as 
Dictionary<OptionDataValue[]>)[dimsDef[i].name];
+            item.push(col ? col[idx] : null);
+        }
+        return item;
+    },
+    [SOURCE_FORMAT_ORIGINAL]: getItemSimply
+};
+
+export function getRawSourceItemGetter(
+    sourceFormat: SourceFormat, seriesLayoutBy: SeriesLayoutBy
+): RawSourceItemGetter {
+    const method = rawSourceItemGetterMap[getMethodMapKey(sourceFormat, 
seriesLayoutBy)];
+    if (__DEV__) {
+        assert(method, 'Do not suppport get item on "' + sourceFormat + '", "' 
+ seriesLayoutBy + '".');
+    }
+    return method;
+}
+
+
+
+
+type RawSourceDataCounter = (
+    rawData: OptionSourceData,
+    startIndex: number,
+    dimsDef: DimensionDefinition[]
+) => number;
+
+const countSimply: RawSourceDataCounter = function (
+    rawData, startIndex, dimsDef
+) {
+    return (rawData as []).length;
+};
+
+const rawSourceDataCounterMap: Dictionary<RawSourceDataCounter> = {
+    [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_COLUMN]: function (
+        rawData, startIndex, dimsDef
+    ) {
+        return Math.max(0, (rawData as OptionDataItem[][]).length - 
startIndex);
+    },
+    [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_ROW]: function (
+        rawData, startIndex, dimsDef
+    ) {
+        const row = (rawData as OptionDataValue[][])[0];
+        return row ? Math.max(0, row.length - startIndex) : 0;
+    },
+    [SOURCE_FORMAT_OBJECT_ROWS]: countSimply,
+    [SOURCE_FORMAT_KEYED_COLUMNS]: function (
+        rawData, startIndex, dimsDef
+    ) {
+        const dimName = dimsDef[0].name;
+        const col = (rawData as Dictionary<OptionDataValue[]>)[dimName];
+        return col ? col.length : 0;
+    },
+    [SOURCE_FORMAT_ORIGINAL]: countSimply
+};
+
+export function getRawSourceDataCounter(
+    sourceFormat: SourceFormat, seriesLayoutBy: SeriesLayoutBy
+): RawSourceDataCounter {
+    const method = rawSourceDataCounterMap[getMethodMapKey(sourceFormat, 
seriesLayoutBy)];
+    if (__DEV__) {
+        assert(method, 'Do not suppport count on "' + sourceFormat + '", "' + 
seriesLayoutBy + '".');
+    }
+    return method;
+}
+
+
+
 // TODO
 // merge it to dataProvider?
-type RawValueGetter = (
+type RawSourceValueGetter = (
     dataItem: OptionDataItem,
-    dataIndex: number,
     dimIndex: DimensionIndex,
     dimName: DimensionName
     // If dimIndex not provided, return OptionDataItem.
     // If dimIndex provided, return OptionDataPrimitive.
 ) => OptionDataValue | OptionDataItem;
 
-const rawValueGetters: {[sourceFormat: string]: RawValueGetter} = {
+const getRawValueSimply = function (
+    dataItem: ArrayLike<OptionDataValue>, dimIndex: number, dimName: string
+): OptionDataValue | ArrayLike<OptionDataValue> {
+    return dimIndex != null ? dataItem[dimIndex] : dataItem;
+};
+
+const rawSourceValueGetterMap: {[sourceFormat: string]: RawSourceValueGetter} 
= {
 
     [SOURCE_FORMAT_ARRAY_ROWS]: getRawValueSimply,
 
     [SOURCE_FORMAT_OBJECT_ROWS]: function (
-        dataItem: Dictionary<OptionDataValue>, dataIndex: number, dimIndex: 
number, dimName: string
+        dataItem: Dictionary<OptionDataValue>, dimIndex: number, dimName: 
string
     ): OptionDataValue | Dictionary<OptionDataValue> {
         return dimIndex != null ? dataItem[dimName] : dataItem;
     },
@@ -286,7 +374,7 @@ const rawValueGetters: {[sourceFormat: string]: 
RawValueGetter} = {
     [SOURCE_FORMAT_KEYED_COLUMNS]: getRawValueSimply,
 
     [SOURCE_FORMAT_ORIGINAL]: function (
-        dataItem: OptionDataItem, dataIndex: number, dimIndex: number, 
dimName: string
+        dataItem: OptionDataItem, dimIndex: number, dimName: string
     ): OptionDataValue | OptionDataItem {
         // FIXME: In some case (markpoint in geo (geo-map.html)),
         // dataItem is {coord: [...]}
@@ -299,12 +387,22 @@ const rawValueGetters: {[sourceFormat: string]: 
RawValueGetter} = {
     [SOURCE_FORMAT_TYPED_ARRAY]: getRawValueSimply
 };
 
-function getRawValueSimply(
-    dataItem: ArrayLike<OptionDataValue>, dataIndex: number, dimIndex: number, 
dimName: string
-): OptionDataValue | ArrayLike<OptionDataValue> {
-    return dimIndex != null ? dataItem[dimIndex] : dataItem;
+export function getRawSourceValueGetter(sourceFormat: SourceFormat): 
RawSourceValueGetter {
+    const method = rawSourceValueGetterMap[sourceFormat];
+    if (__DEV__) {
+        assert(method, 'Do not suppport get value on "' + sourceFormat + '".');
+    }
+    return method;
 }
 
+
+function getMethodMapKey(sourceFormat: SourceFormat, seriesLayoutBy: 
SeriesLayoutBy): string {
+    return sourceFormat === SOURCE_FORMAT_ARRAY_ROWS
+        ? sourceFormat + '_' + seriesLayoutBy
+        : sourceFormat;
+}
+
+
 // ??? FIXME can these logic be more neat: getRawValue, getRawDataItem,
 // Consider persistent.
 // Caution: why use raw value to display on label or tooltip?
@@ -336,9 +434,10 @@ export function retrieveRawValue(
         dimIndex = dimInfo.index;
     }
 
-    return rawValueGetters[sourceFormat](dataItem, dataIndex, dimIndex, 
dimName);
+    return getRawSourceValueGetter(sourceFormat)(dataItem, dimIndex, dimName);
 }
 
+
 /**
  * Compatible with some cases (in pie, map) like:
  * data: [{name: 'xx', value: 5, selected: true}, ...]
diff --git a/src/data/helper/parseDataValue.ts 
b/src/data/helper/parseDataValue.ts
new file mode 100644
index 0000000..6994378
--- /dev/null
+++ b/src/data/helper/parseDataValue.ts
@@ -0,0 +1,73 @@
+/*
+* 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.
+*/
+
+import { ParsedValue, DimensionType } from '../../util/types';
+import OrdinalMeta from '../OrdinalMeta';
+import { parseDate } from '../../util/number';
+
+
+/**
+ * Convert raw the value in to inner value in List.
+ *
+ * [Performance sensitive]
+ *
+ * [Caution]: this is the key logic of user value parser.
+ * For backward compatibiliy, do not modify it until have to !
+ */
+export function parseDataValue(
+    value: any,
+    // For high performance, do not omit the second param.
+    opt: {
+        // Default type: 'number'. There is no 'unknown' type. That is, a 
string
+        // will be parsed to NaN if do not set `type` as 'ordinal'. It has been
+        // the logic in `List.ts` for long time. Follow the same way if you 
need
+        // to get same result as List did from a raw value.
+        type?: DimensionType,
+        ordinalMeta?: OrdinalMeta
+    }
+): ParsedValue {
+    // Performance sensitive.
+    const dimType = opt && opt.type;
+    if (dimType === 'ordinal') {
+        // If given value is a category string
+        const ordinalMeta = opt && opt.ordinalMeta;
+        return ordinalMeta
+            ? ordinalMeta.parseAndCollect(value)
+            : value;
+    }
+
+    if (dimType === 'time'
+        // spead up when using timestamp
+        && typeof value !== 'number'
+        && value != null
+        && value !== '-'
+    ) {
+        value = +parseDate(value);
+    }
+
+    // dimType defaults 'number'.
+    // If dimType is not ordinal and value is null or undefined or NaN or '-',
+    // parse to NaN.
+    return (value == null || value === '')
+        ? NaN
+        // If string (like '-'), using '+' parse to NaN
+        // If object, also parse to NaN
+        : +value;
+};
+
diff --git a/src/data/helper/sourceHelper.ts b/src/data/helper/sourceHelper.ts
index 8f9d7cb..9ae0151 100644
--- a/src/data/helper/sourceHelper.ts
+++ b/src/data/helper/sourceHelper.ts
@@ -18,7 +18,7 @@
 */
 
 
-import {makeInner, getDataItemValue} from '../../util/model';
+import {makeInner, getDataItemValue, queryReferringComponents, 
SINGLE_REFERRING} from '../../util/model';
 import {
     createHashMap,
     each,
@@ -31,7 +31,9 @@ import {
     extend,
     assert,
     hasOwn,
-    HashMap
+    HashMap,
+    isNumber,
+    clone
 } from 'zrender/src/core/util';
 import Source from '../Source';
 
@@ -45,8 +47,6 @@ import {
     SOURCE_FORMAT_UNKNOWN,
     SourceFormat,
     Dictionary,
-    SeriesEncodeOptionMixin,
-    SeriesOption,
     OptionSourceData,
     SeriesLayoutBy,
     OptionSourceHeader,
@@ -59,9 +59,11 @@ import {
     OptionSourceDataOriginal,
     OptionSourceDataObjectRows,
     OptionEncode,
-    DimensionIndex
+    DimensionIndex,
+    SeriesEncodableModel,
+    OptionEncodeValue
 } from '../../util/types';
-import { DatasetModel } from '../../component/dataset';
+import { DatasetModel, DatasetOption } from '../../component/dataset';
 import SeriesModel from '../../model/Series';
 import GlobalModel from '../../model/Global';
 import { CoordDimensionDefinition } from './createDimensions';
@@ -74,16 +76,11 @@ export const BE_ORDINAL = {
 };
 type BeOrdinalValue = (typeof BE_ORDINAL)[keyof typeof BE_ORDINAL];
 
-const innerDatasetModel = makeInner<{
-    sourceFormat: SourceFormat;
-}, DatasetModel>();
-const innerSeriesModel = makeInner<{
-    source: Source;
-}, SeriesModel>();
 const innerGlobalModel = makeInner<{
     datasetMap: HashMap<DatasetRecord, string>
 }, GlobalModel>();
 
+
 interface DatasetRecord {
     categoryWayDim: number;
     valueWayDim: number;
@@ -93,11 +90,13 @@ type SeriesEncodeInternal = {
     [key in keyof OptionEncode]: DimensionIndex[];
 };
 
-type SeriesEncodableModel = SeriesModel<SeriesOption & 
SeriesEncodeOptionMixin>;
-
+export interface SourceMetaRawOption {
+    seriesLayoutBy: SeriesLayoutBy;
+    sourceHeader: OptionSourceHeader;
+    dimensions: DimensionDefinitionLoose[];
+}
 
-export function detectSourceFormat(datasetModel: DatasetModel): void {
-    const data = datasetModel.option.source;
+export function detectSourceFormat(data: DatasetOption['source']): 
SourceFormat {
     let sourceFormat: SourceFormat = SOURCE_FORMAT_UNKNOWN;
 
     if (isTypedArray(data)) {
@@ -137,33 +136,7 @@ export function detectSourceFormat(datasetModel: 
DatasetModel): void {
         throw new Error('Invalid data');
     }
 
-    innerDatasetModel(datasetModel).sourceFormat = sourceFormat;
-}
-
-/**
- * [Scenarios]:
- * (1) Provide source data directly:
- *     series: {
- *         encode: {...},
- *         dimensions: [...]
- *         seriesLayoutBy: 'row',
- *         data: [[...]]
- *     }
- * (2) Refer to datasetModel.
- *     series: [{
- *         encode: {...}
- *         // Ignore datasetIndex means `datasetIndex: 0`
- *         // and the dimensions defination in dataset is used
- *     }, {
- *         encode: {...},
- *         seriesLayoutBy: 'column',
- *         datasetIndex: 1
- *     }]
- *
- * Get data from series itself or datset.
- */
-export function getSource(seriesModel: SeriesModel): Source {
-    return innerSeriesModel(seriesModel).source;
+    return sourceFormat;
 }
 
 /**
@@ -174,65 +147,63 @@ export function resetSourceDefaulter(ecModel: 
GlobalModel): void {
     innerGlobalModel(ecModel).datasetMap = createHashMap();
 }
 
-/**
- * [Caution]:
- * MUST be called after series option merged and
- * before "series.getInitailData()" called.
- *
- * [The rule of making default encode]:
- * Category axis (if exists) alway map to the first dimension.
- * Each other axis occupies a subsequent dimension.
- *
- * [Why make default encode]:
- * Simplify the typing of encode in option, avoiding the case like that:
- * series: [{encode: {x: 0, y: 1}}, {encode: {x: 0, y: 2}}, {encode: {x: 0, y: 
3}}],
- * where the "y" have to be manually typed as "1, 2, 3, ...".
- */
-export function prepareSource(seriesModel: SeriesEncodableModel): void {
-    const seriesOption = seriesModel.option;
-
-    let data = seriesOption.data as OptionSourceData;
-    let sourceFormat: SourceFormat = isTypedArray(data)
-        ? SOURCE_FORMAT_TYPED_ARRAY : SOURCE_FORMAT_ORIGINAL;
-    let fromDataset = false;
-
-    let seriesLayoutBy = seriesOption.seriesLayoutBy;
-    let sourceHeader = seriesOption.sourceHeader;
-    let dimensionsDefine = seriesOption.dimensions;
-
-    const datasetModel = getDatasetModel(seriesModel);
-    if (datasetModel) {
-        const datasetOption = datasetModel.option;
-
-        data = datasetOption.source;
-        sourceFormat = innerDatasetModel(datasetModel).sourceFormat;
-        fromDataset = true;
-
-        // These settings from series has higher priority.
-        seriesLayoutBy = seriesLayoutBy || datasetOption.seriesLayoutBy;
-        sourceHeader == null && (sourceHeader = datasetOption.sourceHeader);
-        dimensionsDefine = dimensionsDefine || datasetOption.dimensions;
-    }
-
-    const completeResult = completeBySourceData(
-        data, sourceFormat, seriesLayoutBy, sourceHeader, dimensionsDefine
+export function createSource(
+    sourceData: OptionSourceData,
+    thisMetaRawOption: SourceMetaRawOption,
+    // can be null. If not provided, auto detect it from `sourceData`.
+    sourceFormat: SourceFormat,
+    encodeDefine: OptionEncode  // can be null
+): Source {
+    sourceFormat = sourceFormat || detectSourceFormat(sourceData);
+    const dimInfo = determineSourceDimensions(
+        sourceData,
+        sourceFormat,
+        thisMetaRawOption.seriesLayoutBy,
+        thisMetaRawOption.sourceHeader,
+        thisMetaRawOption.dimensions
     );
-
-    innerSeriesModel(seriesModel).source = new Source({
-        data: data,
-        fromDataset: fromDataset,
-        seriesLayoutBy: seriesLayoutBy,
+    const source = new Source({
+        data: sourceData,
         sourceFormat: sourceFormat,
-        dimensionsDefine: completeResult.dimensionsDefine,
-        startIndex: completeResult.startIndex,
-        dimensionsDetectCount: completeResult.dimensionsDetectCount,
-        // Note: dataset option does not have `encode`.
-        encodeDefine: seriesOption.encode
+
+        seriesLayoutBy: thisMetaRawOption.seriesLayoutBy,
+        dimensionsDefine: dimInfo.dimensionsDefine,
+        startIndex: dimInfo.startIndex,
+        dimensionsDetectCount: dimInfo.dimensionsDetectCount,
+        encodeDefine: makeEncodeDefine(encodeDefine)
+    });
+
+    return source;
+}
+
+/**
+ * Clone except source data.
+ */
+export function cloneSourceShallow(source: Source) {
+    return new Source({
+        data: source.data,
+        sourceFormat: source.sourceFormat,
+
+        seriesLayoutBy: source.seriesLayoutBy,
+        dimensionsDefine: clone(source.dimensionsDefine),
+        startIndex: source.startIndex,
+        dimensionsDetectCount: source.dimensionsDetectCount,
+        encodeDefine: makeEncodeDefine(source.encodeDefine)
     });
 }
 
+function makeEncodeDefine(
+    encodeDefine: OptionEncode | HashMap<OptionEncodeValue, DimensionName>
+): HashMap<OptionEncodeValue, DimensionName> {
+    // null means user not specify `series.encode`.
+    return encodeDefine
+        ? createHashMap<OptionEncodeValue, DimensionName>(encodeDefine)
+        : null;
+}
+
+
 // return {startIndex, dimensionsDefine, dimensionsCount}
-function completeBySourceData(
+export function determineSourceDimensions(
     data: OptionSourceData,
     sourceFormat: SourceFormat,
     seriesLayoutBy: SeriesLayoutBy,
@@ -248,7 +219,7 @@ function completeBySourceData(
 
     if (!data) {
         return {
-            dimensionsDefine: normalizeDimensionsDefine(dimensionsDefine),
+            dimensionsDefine: normalizeDimensionsOption(dimensionsDefine),
             startIndex,
             dimensionsDetectCount
         };
@@ -275,7 +246,7 @@ function completeBySourceData(
             }, seriesLayoutBy, dataArrayRows, 10);
         }
         else {
-            startIndex = sourceHeader ? 1 : 0;
+            startIndex = isNumber(sourceHeader) ? sourceHeader : sourceHeader 
? 1 : 0;
         }
 
         if (!dimensionsDefine && startIndex === 1) {
@@ -318,7 +289,7 @@ function completeBySourceData(
 
     return {
         startIndex: startIndex,
-        dimensionsDefine: normalizeDimensionsDefine(dimensionsDefine),
+        dimensionsDefine: normalizeDimensionsOption(dimensionsDefine),
         dimensionsDetectCount: dimensionsDetectCount
     };
 }
@@ -326,7 +297,7 @@ function completeBySourceData(
 // Consider dimensions defined like ['A', 'price', 'B', 'price', 'C', 'price'],
 // which is reasonable. But dimension name is duplicated.
 // Returns undefined or an array contains only object without null/undefiend 
or string.
-function normalizeDimensionsDefine(dimensionsDefine: 
DimensionDefinitionLoose[]): DimensionDefinition[] {
+function normalizeDimensionsOption(dimensionsDefine: 
DimensionDefinitionLoose[]): DimensionDefinition[] {
     if (!dimensionsDefine) {
         // The meaning of null/undefined is different from empty array.
         return;
@@ -419,7 +390,7 @@ export function makeSeriesEncodeForAxisCoordSys(
 ): SeriesEncodeInternal {
     const encode: SeriesEncodeInternal = {};
 
-    const datasetModel = getDatasetModel(seriesModel);
+    const datasetModel = querySeriesUpstreamDatasetModel(seriesModel);
     // Currently only make default when using dataset, util more reqirements 
occur.
     if (!datasetModel || !coordDimensions) {
         return encode;
@@ -512,7 +483,7 @@ export function makeSeriesEncodeForNameBased(
 ): SeriesEncodeInternal {
     const encode: SeriesEncodeInternal = {};
 
-    const datasetModel = getDatasetModel(seriesModel);
+    const datasetModel = querySeriesUpstreamDatasetModel(seriesModel);
     // Currently only make default when using dataset, util more reqirements 
occur.
     if (!datasetModel) {
         return encode;
@@ -600,19 +571,53 @@ export function makeSeriesEncodeForNameBased(
 }
 
 /**
- * If return null/undefined, indicate that should not use datasetModel.
+ * @return If return null/undefined, indicate that should not use datasetModel.
  */
-function getDatasetModel(seriesModel: SeriesEncodableModel): DatasetModel {
-    const option = seriesModel.option;
+export function querySeriesUpstreamDatasetModel(
+    seriesModel: SeriesEncodableModel
+): DatasetModel {
     // Caution: consider the scenario:
     // A dataset is declared and a series is not expected to use the dataset,
     // and at the beginning `setOption({series: { noData })` (just prepare 
other
     // option but no data), then `setOption({series: {data: [...]}); In this 
case,
     // the user should set an empty array to avoid that dataset is used by 
default.
-    const thisData = option.data;
+    const thisData = seriesModel.get('data', true);
     if (!thisData) {
-        return seriesModel.ecModel.getComponent('dataset', option.datasetIndex 
|| 0) as DatasetModel;
+        return queryReferringComponents(
+            seriesModel.ecModel,
+            'dataset',
+            {
+                index: seriesModel.get('datasetIndex', true),
+                id: seriesModel.get('datasetId', true)
+            },
+            SINGLE_REFERRING
+        ).models[0] as DatasetModel;
+    }
+}
+
+/**
+ * @return Always return an array event empty.
+ */
+export function queryDatasetUpstreamDatasetModels(
+    datasetModel: DatasetModel
+): DatasetModel[] {
+    // Only these attributes declared, we by defualt reference to 
`datasetIndex: 0`.
+    // Otherwise, no reference.
+    if (!datasetModel.get('transform', true)
+        && !datasetModel.get('fromTransformResult', true)
+    ) {
+        return [];
     }
+
+    return queryReferringComponents(
+        datasetModel.ecModel,
+        'dataset',
+        {
+            index: datasetModel.get('fromDatasetIndex', true),
+            id: datasetModel.get('fromDatasetId', true)
+        },
+        SINGLE_REFERRING
+    ).models as DatasetModel[];
 }
 
 /**
diff --git a/src/data/helper/sourceManager.ts b/src/data/helper/sourceManager.ts
new file mode 100644
index 0000000..74ed264
--- /dev/null
+++ b/src/data/helper/sourceManager.ts
@@ -0,0 +1,368 @@
+/*
+* 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.
+*/
+
+import { DatasetModel } from '../../component/dataset';
+import SeriesModel from '../../model/Series';
+import { setAsPrimitive, map, isTypedArray, defaults, assert, each } from 
'zrender/src/core/util';
+import Source from '../Source';
+import {
+    SeriesEncodableModel, OptionSourceData,
+    SOURCE_FORMAT_TYPED_ARRAY, SOURCE_FORMAT_ORIGINAL,
+    SourceFormat, SeriesLayoutBy, OptionSourceHeader, DimensionDefinitionLoose
+} from '../../util/types';
+import {
+    querySeriesUpstreamDatasetModel, queryDatasetUpstreamDatasetModels,
+    createSource, SourceMetaRawOption, cloneSourceShallow
+} from './sourceHelper';
+import { applyDataTransform } from './transform';
+
+
+/**
+ * [REQUIREMENT_MEMO]:
+ * (0) `metaRawOption` means `dimensions`/`sourceHeader`/`seriesLayoutBy` in 
raw option.
+ * (1) Keep support the feature: `metaRawOption` can be specified both on 
`series` and
+ * `root-dataset`. Them on `series` has higher priority.
+ * (2) Do not support to set `metaRawOption` on a `non-root-dataset`, because 
it might
+ * confuse users: whether those props indicate how to visit the upstream 
source or visit
+ * the transform result source, and some transforms has nothing to do with 
these props,
+ * and some transforms might have multiple upstream.
+ * (3) Transforms should specify `metaRawOption` in each output, just like 
they can be
+ * declared in `root-dataset`.
+ * (4) At present only support visit source in `SERIES_LAYOUT_BY_COLUMN` in 
transforms.
+ * That is for reducing complexity in transfroms.
+ * PENDING: Whether to provide transposition transform?
+ *
+ * [IMPLEMENTAION_MEMO]:
+ * "sourceVisitConfig" are calculated from `metaRawOption` and `data`.
+ * They will not be calculated until `source` is about to be visited (to 
prevent from
+ * duplicate calcuation). `source` is visited only in series and input to 
transforms.
+ *
+ * [SCENARIO]:
+ * (1) Provide source data directly:
+ * ```js
+ * series: {
+ *     encode: {...},
+ *     dimensions: [...]
+ *     seriesLayoutBy: 'row',
+ *     data: [[...]]
+ * }
+ * ```
+ * (2) Series refer to dataset.
+ * ```js
+ * series: [{
+ *     encode: {...}
+ *     // Ignore datasetIndex means `datasetIndex: 0`
+ *     // and the dimensions defination in dataset is used
+ * }, {
+ *     encode: {...},
+ *     seriesLayoutBy: 'column',
+ *     datasetIndex: 1
+ * }]
+ * ```
+ * (3) dataset transform
+ * ```js
+ * dataset: [{
+ *     source: [...]
+ * }, {
+ *     source: [...]
+ * }, {
+ *     // By default from 0.
+ *     transform: { type: 'filter', config: {...} }
+ * }, {
+ *     // Piped.
+ *     transform: [
+ *         { type: 'filter', config: {...} },
+ *         { type: 'sort', config: {...} }
+ *     ]
+ * }, {
+ *     id: 'regressionData',
+ *     fromDatasetIndex: 1,
+ *     // Third-party transform
+ *     transform: { type: 'ecStat:regression', config: {...} }
+ * }, {
+ *     // retrieve the extra result.
+ *     id: 'regressionFormula',
+ *     fromDatasetId: 'regressionData',
+ *     fromTransformResult: 1
+ * }]
+ * ```
+ */
+
+export class SourceManager {
+
+    // Currently only datasetModel can host `transform`
+    private _sourceHost: DatasetModel | SeriesModel;
+
+    // Cached source. Do not repeat calculating if not dirty.
+    private _sourceList: Source[] = [];
+
+    // version sign of each upstream source manager.
+    private _upstreamSignList: string[] = [];
+
+    private _versionSignBase = 0;
+
+    constructor(sourceHost: DatasetModel | SeriesModel) {
+        this._sourceHost = sourceHost;
+    }
+
+    /**
+     * Mark dirty.
+     */
+    dirty() {
+        this._setLocalSource([], []);
+    }
+
+    private _setLocalSource(
+        sourceList: Source[],
+        upstreamSignList: string[]
+    ): void {
+        this._sourceList = sourceList;
+        this._upstreamSignList = upstreamSignList;
+        this._versionSignBase++;
+        if (this._versionSignBase > 9e10) {
+            this._versionSignBase = 0;
+        }
+    }
+
+    /**
+     * For detecting whether the upstream source is dirty, so that
+     * the local cached source (in `_sourceList`) should be discarded.
+     */
+    private _getVersionSign(): string {
+        return this._sourceHost.uid + '_' + this._versionSignBase;
+    }
+
+    /**
+     * Always return a source instance. Otherwise throw error.
+     */
+    prepareSource(): void {
+        // For the case that call `setOption` multiple time but no data 
changed,
+        // cache the result source to prevent from repeating transform.
+        if (this._isDirty()) {
+            this._createSource();
+        }
+    }
+
+    private _createSource(): void {
+        this._setLocalSource([], []);
+        const sourceHost = this._sourceHost;
+
+        const upSourceMgrList = this._getUpstreamSourceManagers();
+        const hasUpstream = !!upSourceMgrList.length;
+        let resultSourceList: Source[];
+        let upstreamSignList: string[];
+
+        if (isSeries(sourceHost)) {
+            const seriesModel = sourceHost as SeriesEncodableModel;
+            let data;
+            let sourceFormat: SourceFormat;
+            let upMetaRawOption;
+
+            // Has upstream dataset
+            if (hasUpstream) {
+                const upSourceMgr = upSourceMgrList[0];
+                upSourceMgr.prepareSource();
+                const upSource = upSourceMgr.getSource();
+                data = upSource.data;
+                sourceFormat = upSource.sourceFormat;
+                upMetaRawOption = upSourceMgr._getSourceMetaRawOption();
+                upstreamSignList = [upSourceMgr._getVersionSign()];
+            }
+            // Series data is from own.
+            else {
+                data = seriesModel.get('data', true) as OptionSourceData;
+                sourceFormat = isTypedArray(data)
+                    ? SOURCE_FORMAT_TYPED_ARRAY : SOURCE_FORMAT_ORIGINAL;
+                upstreamSignList = [];
+            }
+
+            const thisMetaRawOption = defaults(
+                this._getSourceMetaRawOption(),
+                // See [REQUIREMENT MEMO], merge settings on series and parent 
dataset if it is root.
+                upMetaRawOption
+            );
+            resultSourceList = [createSource(
+                data,
+                thisMetaRawOption,
+                sourceFormat,
+                seriesModel.get('encode', true)
+            )];
+        }
+        else {
+            const datasetModel = sourceHost as DatasetModel;
+
+            // Has upstream dataset.
+            if (hasUpstream) {
+                const result = this._applyTransform(upSourceMgrList);
+                resultSourceList = result.sourceList;
+                upstreamSignList = result.upstreamSignList;
+            }
+            // Is root dataset.
+            else {
+                const sourceData = datasetModel.get('source', true);
+                resultSourceList = [createSource(
+                    sourceData,
+                    this._getSourceMetaRawOption(),
+                    null,
+                    // Note: dataset option does not have `encode`.
+                    null
+                )];
+                upstreamSignList = [];
+            }
+        }
+
+        if (__DEV__) {
+            assert(resultSourceList && upstreamSignList);
+        }
+
+        this._setLocalSource(resultSourceList, upstreamSignList);
+    }
+
+    private _applyTransform(
+        upMgrList: SourceManager[]
+    ): {
+        sourceList: Source[],
+        upstreamSignList: string[]
+    } {
+        const datasetModel = this._sourceHost as DatasetModel;
+        const transformOption = datasetModel.get('transform', true);
+        const fromTransformResult = datasetModel.get('fromTransformResult', 
true);
+        let sourceList: Source[];
+        let upstreamSignList: string[];
+
+        if (transformOption) {
+            const upSourceList: Source[] = [];
+            upstreamSignList = [];
+            each(upMgrList, upMgr => {
+                upMgr.prepareSource();
+                upSourceList.push(upMgr.getSource());
+                upstreamSignList.push(upMgr._getVersionSign());
+            });
+            sourceList = applyDataTransform(
+                transformOption,
+                upSourceList,
+                { datasetIndex: datasetModel.componentIndex }
+            );
+        }
+        else if (fromTransformResult != null) {
+            if (upMgrList.length !== 1) {
+                let errMsg = '';
+                if (__DEV__) {
+                    errMsg = 'When using `fromTransformResult`, there should 
be only one upstream dataset';
+                }
+                doThrow(errMsg);
+            }
+            const upMgr = upMgrList[0];
+            upMgr.prepareSource();
+            const upSource = upMgr.getSource(fromTransformResult);
+            upstreamSignList = [upMgr._getVersionSign()];
+            sourceList = [cloneSourceShallow(upSource)];
+        }
+
+        return { sourceList, upstreamSignList };
+    }
+
+    private _isDirty(): boolean {
+        const sourceList = this._sourceList;
+        if (!sourceList.length) {
+            return true;
+        }
+
+        // All sourceList is from the some upsteam.
+        const upSourceMgrList = this._getUpstreamSourceManagers();
+        for (let i = 0; i < upSourceMgrList.length; i++) {
+            const upSrcMgr = upSourceMgrList[i];
+            if (
+                // Consider the case that there is ancestor diry, call it 
recursively.
+                // The performance is probably not an issue because usually 
the chain is not long.
+                upSrcMgr._isDirty()
+                || this._upstreamSignList[i] !== upSrcMgr._getVersionSign()
+            ) {
+                return true;
+            }
+        }
+    }
+
+    /**
+     * @param sourceIndex By defualt 0, means "main source".
+     *                    Most cases there is only one source.
+     */
+    getSource(sourceIndex?: number) {
+        return this._sourceList[sourceIndex || 0];
+    }
+
+    /**
+     * PEDING: Is it fast enough?
+     * If no upstream, return empty array.
+     */
+    private _getUpstreamSourceManagers(): SourceManager[] {
+        // Always get the relationship from the raw option.
+        // Do not cache the link of the dependency graph, so that
+        // no need to update them when change happen.
+        const sourceHost = this._sourceHost;
+
+        if (isSeries(sourceHost)) {
+            const datasetModel = querySeriesUpstreamDatasetModel(sourceHost);
+            return !datasetModel ? [] : [datasetModel.getSourceManager()];
+        }
+        else {
+            return map(
+                queryDatasetUpstreamDatasetModels(sourceHost as DatasetModel),
+                datasetModel => datasetModel.getSourceManager()
+            );
+        }
+    }
+
+    private _getSourceMetaRawOption(): SourceMetaRawOption {
+        const sourceHost = this._sourceHost;
+        let seriesLayoutBy: SeriesLayoutBy;
+        let sourceHeader: OptionSourceHeader;
+        let dimensions: DimensionDefinitionLoose[];
+        if (isSeries(sourceHost)) {
+            seriesLayoutBy = sourceHost.get('seriesLayoutBy', true);
+            sourceHeader = sourceHost.get('sourceHeader', true);
+            dimensions = sourceHost.get('dimensions', true);
+        }
+        // See [REQUIREMENT MEMO], `non-root-dataset` do not support them.
+        else if (!this._getUpstreamSourceManagers().length) {
+            const model = sourceHost as DatasetModel;
+            seriesLayoutBy = model.get('seriesLayoutBy', true);
+            sourceHeader = model.get('sourceHeader', true);
+            dimensions = model.get('dimensions', true);
+        }
+        return { seriesLayoutBy, sourceHeader, dimensions };
+    }
+
+}
+
+// Call this method after `super.init` and `super.mergeOption` to
+// disable the transform merge, but do not disable transfrom clone from 
rawOption.
+export function disableTransformOptionMerge(datasetModel: DatasetModel): void {
+    const transformOption = datasetModel.option.transform;
+    transformOption && setAsPrimitive(datasetModel.option.transform);
+}
+
+function isSeries(sourceHost: SourceManager['_sourceHost']): sourceHost is 
SeriesEncodableModel {
+    // Avoid circular dependency with Series.ts
+    return (sourceHost as SeriesModel).mainType === 'series';
+}
+
+function doThrow(errMsg: string): void {
+    throw new Error(errMsg);
+}
diff --git a/src/data/helper/transform.ts b/src/data/helper/transform.ts
new file mode 100644
index 0000000..e6150b1
--- /dev/null
+++ b/src/data/helper/transform.ts
@@ -0,0 +1,317 @@
+/*
+* 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.
+*/
+
+import {
+    Dictionary, OptionSourceData, DimensionDefinitionLoose, OptionSourceHeader,
+    SourceFormat, DimensionDefinition, OptionDataItem, DimensionIndex,
+    OptionDataValue, DimensionLoose, DimensionName, ParsedValue, 
SERIES_LAYOUT_BY_COLUMN
+} from '../../util/types';
+import Source from '../Source';
+import { normalizeToArray } from '../../util/model';
+import {
+    assert, createHashMap, bind, each, hasOwn, map, clone, isObject,
+    isArrayLike
+} from 'zrender/src/core/util';
+import {
+    getRawSourceItemGetter, getRawSourceDataCounter, getRawSourceValueGetter
+} from './dataProvider';
+import { parseDataValue } from './parseDataValue';
+import { createSource } from './sourceHelper';
+import { consoleLog, makePrintable } from '../../util/log';
+
+
+export type PipedDataTransformOption = DataTransformOption[];
+export type DataTransformType = string;
+export type DataTransformConfig = unknown;
+
+export interface DataTransformOption {
+    type: DataTransformType;
+    config: DataTransformConfig;
+    // Print the result via `console.log` when transform performed. Only work 
in dev mode for debug.
+    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,
+    transform?: (
+        param: ExternalDataTransformParam<TO>
+    ) => ExternalDataTransformResultItem | ExternalDataTransformResultItem[]
+}
+
+interface ExternalDataTransformParam<TO extends DataTransformOption = 
DataTransformOption> {
+    // This is the first source in sourceList. In most cases,
+    // there is only one upstream source.
+    source: ExternalSource;
+    sourceList: ExternalSource[];
+    config: TO['config'];
+}
+export interface ExternalDataTransformResultItem {
+    data: OptionSourceData;
+    dimensions?: DimensionDefinitionLoose[];
+    sourceHeader?: OptionSourceHeader;
+}
+export interface ExternalDimensionDefinition extends DimensionDefinition {
+    // Mandatory
+    index: DimensionIndex;
+}
+
+/**
+ * TODO: disable writable.
+ * This structure will be exposed to users.
+ */
+class ExternalSource {
+    /**
+     * [Caveat]
+     * This instance is to be exposed to users.
+     * 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`.
+     */
+
+    data: OptionSourceData;
+    sourceFormat: SourceFormat;
+    dimensions: ExternalDimensionDefinition[];
+    sourceHeaderCount: number;
+
+    getDimensionInfo(dim: DimensionLoose): ExternalDimensionDefinition {
+        return;
+    }
+
+    getRawDataItem(dataIndex: number): OptionDataItem {
+        return;
+    }
+
+    getRawHeaderItem(dataIndex: number): OptionDataItem {
+        return;
+    }
+
+    count(): number {
+        return;
+    }
+
+    /**
+     * Only support by dimension index.
+     * 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 {
+        return;
+    }
+
+    convertDataValue(rawVal: unknown, dimInfo: ExternalDimensionDefinition): 
ParsedValue {
+        return parseDataValue(rawVal, dimInfo);
+    }
+}
+
+function createExternalSource(
+    data: OptionSourceData,
+    sourceFormat: SourceFormat,
+    dimsDef: DimensionDefinition[],
+    sourceHeaderCount: number
+): ExternalSource {
+    const extSource = new ExternalSource();
+
+    extSource.data = data;
+    extSource.sourceFormat = sourceFormat;
+    extSource.sourceHeaderCount = sourceHeaderCount;
+
+    // Create a new dimensions structure for exposing.
+    const dimensions = extSource.dimensions = [] as 
ExternalDimensionDefinition[];
+    const dimsByName = {} as Dictionary<ExternalDimensionDefinition>;
+    each(dimsDef, function (dimDef, idx) {
+        const name = dimDef.name;
+        const dimDefExt = {
+            index: idx,
+            name: name,
+            displayName: dimDef.displayName
+        };
+        dimensions.push(dimDefExt);
+        // Users probably not sepcify dimension name. For simplicity, data 
transform
+        // do not generate dimension name.
+        if (name != null) {
+            // Dimension name should not be duplicated.
+            // For simplicity, data transform forbid name duplication, do not 
generate
+            // new name like module `completeDimensions.ts` did, but just tell 
users.
+            assert(!hasOwn(dimsByName, name), 'dimension name "' + name + '" 
duplicated.');
+            dimsByName[name] = dimDefExt;
+        }
+    });
+
+    // 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);
+        }
+    };
+
+    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) {
+            return;
+        }
+        const dimDef = extSource.dimensions[dimIndex];
+        // When `dimIndex` is `null`, `rawValueGetter` return the whole item.
+        if (dimDef) {
+            return rawValueGetter(rawItem, dimIndex, dimDef.name) as 
OptionDataValue;
+        }
+    };
+
+    extSource.getDimensionInfo = bind(getDimensionInfo, null, dimensions, 
dimsByName);
+
+    return extSource;
+}
+
+
+function getDimensionInfo(
+    dimensions: ExternalDimensionDefinition[],
+    dimsByName: Dictionary<ExternalDimensionDefinition>,
+    dim: DimensionLoose
+): ExternalDimensionDefinition {
+    if (dim == null) {
+        return;
+    }
+    // Keep the same logic as `List::getDimension` did.
+    if (typeof dim === 'number'
+        // If being a number-like string but not being defined a dimension 
name.
+        || (!isNaN(dim as any) && !hasOwn(dimsByName, dim))
+    ) {
+        return dimensions[dim as DimensionIndex];
+    }
+    else if (hasOwn(dimsByName, dim)) {
+        return dimsByName[dim as DimensionName];
+    }
+}
+
+
+
+const externalTransformMap = createHashMap<ExternalDataTransform, string>();
+
+export function registerExternalTransform(
+    externalTransform: ExternalDataTransform
+): void {
+    externalTransform = clone(externalTransform);
+    let type = externalTransform.type;
+    assert(type, 'Must have a `type` when `registerTransform`.');
+    const typeParsed = type.split(':');
+    assert(typeParsed.length === 2, 'Name must include namespace like 
"ns:regression".');
+    // Namespace 'echarts:xxx' is official namespace, where the transforms 
should
+    // be called directly via 'xxx' rather than 'echarts:xxx'.
+    if (typeParsed[0] === 'echarts') {
+        type = typeParsed[1];
+    }
+    externalTransformMap.set(type, externalTransform);
+}
+
+export function applyDataTransform(
+    rawTransOption: DataTransformOption | PipedDataTransformOption,
+    sourceList: Source[],
+    infoForPrint: { datasetIndex: number }
+): Source[] {
+    const pipedTransOption: PipedDataTransformOption = 
normalizeToArray(rawTransOption);
+
+    for (let i = 0, len = pipedTransOption.length; i < len; i++) {
+        const transOption = pipedTransOption[i];
+        sourceList = applySingleDataTransform(transOption, sourceList);
+        // piped transform only support single input, except the fist one.
+        // piped transform only support single output, except the last one.
+        if (i < len - 1) {
+            sourceList.length = Math.max(sourceList.length, 1);
+        }
+
+        if (__DEV__) {
+            if (transOption.print) {
+                const printStrArr = map(sourceList, source => {
+                    return '--- datasetIndex: ' + infoForPrint.datasetIndex + 
', transform result: ---\n'
+                        + makePrintable(source.data);
+                }).join('\n');
+                consoleLog(printStrArr);
+            }
+        }
+    }
+
+    return sourceList;
+}
+
+function applySingleDataTransform(
+    rawTransOption: DataTransformOption,
+    upSourceList: Source[]
+): Source[] {
+    assert(upSourceList.length, 'Must have at least one upstream dataset.');
+
+    const transOption = rawTransOption;
+    const transType = transOption.type;
+    const externalTransform = externalTransformMap.get(transType);
+
+    assert(externalTransform, 'Can not find transform on type "' + transType + 
'".');
+
+    // Prepare source
+    const sourceList = map(upSourceList, function (source) {
+        return createExternalSource(
+            source.data,
+            source.sourceFormat,
+            source.dimensionsDefine,
+            source.startIndex
+        );
+    });
+
+    const resultList = normalizeToArray(
+        externalTransform.transform({
+            source: sourceList[0],
+            sourceList: sourceList,
+            config: clone(transOption.config)
+        })
+    );
+
+    return map(resultList, function (result) {
+        assert(
+            isObject(result),
+            'A transform should not return some empty results.'
+        );
+        assert(
+            isObject(result.data) || isArrayLike(result.data),
+            'Result data should be object or array in data transform.'
+        );
+
+        return createSource(
+            result.data,
+            {
+                seriesLayoutBy: SERIES_LAYOUT_BY_COLUMN,
+                sourceHeader: result.sourceHeader,
+                dimensions: result.dimensions
+            },
+            null,
+            null
+        );
+    });
+}
+
diff --git a/src/echarts.ts b/src/echarts.ts
index 76334a7..fa991fd 100644
--- a/src/echarts.ts
+++ b/src/echarts.ts
@@ -100,6 +100,7 @@ import { handleLegacySelectEvents } from 
'./legacy/dataSelectAction';
 
 // At least canvas renderer.
 import 'zrender/src/canvas/canvas';
+import { registerExternalTransform } from './data/helper/transform';
 
 declare let global: any;
 type ModelFinder = modelUtil.ModelFinder;
@@ -2716,6 +2717,8 @@ export function getMap(mapName: string) {
     };
 }
 
+export const registerTransform = registerExternalTransform;
+
 /**
  * Globa dispatchAction to a specified chart instance.
  */
diff --git a/src/model/Component.ts b/src/model/Component.ts
index 4fc656a..373ea68 100644
--- a/src/model/Component.ts
+++ b/src/model/Component.ts
@@ -29,7 +29,9 @@ import {
     ClassManager,
     mountExtend
 } from '../util/clazz';
-import {makeInner, ModelFinderIndexQuery, queryReferringComponents, 
ModelFinderIdQuery, QueryReferringOpt} from '../util/model';
+import {
+    makeInner, ModelFinderIndexQuery, queryReferringComponents, 
ModelFinderIdQuery, QueryReferringOpt
+} from '../util/model';
 import * as layout from '../util/layout';
 import GlobalModel from './Global';
 import {
diff --git a/src/model/OptionManager.ts b/src/model/OptionManager.ts
index 33f0742..b92d5b4 100644
--- a/src/model/OptionManager.ts
+++ b/src/model/OptionManager.ts
@@ -36,6 +36,7 @@ import {
     each, clone, map, isTypedArray, setAsPrimitive
     // , HashMap , createHashMap, extend, merge,
 } from 'zrender/src/core/util';
+import { DatasetOption } from '../component/dataset';
 
 const QUERY_REG = /^(min|max)?(.+)$/;
 
@@ -98,6 +99,9 @@ class OptionManager {
             each(normalizeToArray((rawOption as ECUnitOption).series), 
function (series: SeriesOption) {
                 series && series.data && isTypedArray(series.data) && 
setAsPrimitive(series.data);
             });
+            each(normalizeToArray((rawOption as ECUnitOption).dataset), 
function (dataset: DatasetOption) {
+                dataset && dataset.source && isTypedArray(dataset.source) && 
setAsPrimitive(dataset.source);
+            });
         }
 
         // Caution: some series modify option data, if do not clone,
diff --git a/src/model/Series.ts b/src/model/Series.ts
index 6fdd691..b697c56 100644
--- a/src/model/Series.ts
+++ b/src/model/Series.ts
@@ -41,10 +41,6 @@ import {
     fetchLayoutMode
 } from '../util/layout';
 import {createTask} from '../stream/task';
-import {
-    prepareSource,
-    getSource
-} from '../data/helper/sourceHelper';
 import {retrieveRawValue} from '../data/helper/dataProvider';
 import GlobalModel from './Global';
 import { CoordinateSystem } from '../coord/CoordinateSystem';
@@ -57,10 +53,12 @@ import Axis from '../coord/Axis';
 import { GradientObject } from 'zrender/src/graphic/Gradient';
 import type { BrushCommonSelectorsForSeries, BrushSelectableArea } from 
'../component/brush/selector';
 import makeStyleMapper from './mixin/makeStyleMapper';
+import { SourceManager } from '../data/helper/sourceManager';
 
 const inner = modelUtil.makeInner<{
     data: List
     dataBeforeProcessed: List
+    sourceManager: SourceManager
 }, SeriesModel>();
 
 function getSelectionKey(data: List, dataIndex: number): string {
@@ -139,7 +137,6 @@ class SeriesModel<Opt extends SeriesOption = SeriesOption> 
extends ComponentMode
     // Injected outside
     pipelineContext: PipelineContext;
 
-
     // ---------------------------------------
     // Props to tell visual/style.ts about how to do visual encoding.
     // ---------------------------------------
@@ -197,7 +194,8 @@ class SeriesModel<Opt extends SeriesOption = SeriesOption> 
extends ComponentMode
 
         this.mergeDefaultAndTheme(option, ecModel);
 
-        prepareSource(this);
+        const sourceManager = inner(this).sourceManager = new 
SourceManager(this);
+        sourceManager.prepareSource();
 
         const data = this.getInitialData(option, ecModel);
         wrapData(data, this);
@@ -273,7 +271,9 @@ class SeriesModel<Opt extends SeriesOption = SeriesOption> 
extends ComponentMode
             );
         }
 
-        prepareSource(this);
+        const sourceManager = inner(this).sourceManager;
+        sourceManager.dirty();
+        sourceManager.prepareSource();
 
         const data = this.getInitialData(newSeriesOption, ecModel);
         wrapData(data, this);
@@ -377,7 +377,7 @@ class SeriesModel<Opt extends SeriesOption = SeriesOption> 
extends ComponentMode
     }
 
     getSource(): Source {
-        return getSource(this);
+        return inner(this).sourceManager.getSource();
     }
 
     /**
diff --git a/src/util/conditionalExpression.ts 
b/src/util/conditionalExpression.ts
new file mode 100644
index 0000000..b48a896
--- /dev/null
+++ b/src/util/conditionalExpression.ts
@@ -0,0 +1,536 @@
+/*
+* 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.
+*/
+
+import { OptionDataValue, DimensionLoose, Dictionary } from './types';
+import {
+    createHashMap, keys, isArray, map, isObject, isString, trim, HashMap, 
isRegExp, isArrayLike
+} from 'zrender/src/core/util';
+import { throwError, makePrintable } from './log';
+import { parseDate } from './number';
+
+
+// PENDING:
+// (1) Support more parser like: `parse: 'trim'`, `parse: 'lowerCase'`, 
`parse: 'year'`, `parse: 'dayOfWeek'`?
+// (2) Support piped parser ?
+// (3) Support callback parser or callback condition?
+// (4) At present do not support string expression yet but only stuctured 
expression.
+
+
+/**
+ * The structured expression considered:
+ * (1) Literal simplicity
+ * (2) Sementic displayed clearly
+ *
+ * Sementic supports:
+ * (1) relational expression
+ * (2) logical expression
+ *
+ * For example:
+ * ```js
+ * {
+ *     and: [{
+ *         or: [{
+ *             dimension: 'Year', gt: 2012, lt: 2019
+ *         }, {
+ *             dimension: 'Year', '>': 2002, '<=': 2009
+ *         }]
+ *     }, {
+ *         dimension: 'Product', eq: 'Tofu'
+ *     }]
+ * }
+ *
+ * { dimension: 'Product', eq: 'Tofu' }
+ *
+ * {
+ *     or: [
+ *         { dimension: 'Product', value: 'Tofu' },
+ *         { dimension: 'Product', value: 'Biscuit' }
+ *     ]
+ * }
+ *
+ * {
+ *     and: [true]
+ * }
+ * ```
+ *
+ * [PARSER]
+ * In an relation expression object, we can specify some built-in parsers:
+ * ```js
+ * // Trim if string
+ * {
+ *     parse: 'trim',
+ *     eq: 'Flowers'
+ * }
+ * // Parse as time and enable arithmetic relation comparison.
+ * {
+ *     parse: 'time',
+ *     lt: '2012-12-12'
+ * }
+ * // RegExp, include the feature in SQL: `like '%xxx%'`.
+ * {
+ *     reg: /^asdf$/
+ * }
+ * {
+ *     reg: '^asdf$' // Serializable reg exp, will be `new RegExp(...)`
+ * }
+ * ```
+ *
+ *
+ * [EMPTY_RULE]
+ * (1) If a relational expression set value as `null`/`undefined` like:
+ * `{ dimension: 'Product', lt: undefined }`,
+ * The result will be `false` rather than `true`.
+ * Consider the case like "filter condition", return all result when 
null/undefined
+ * is probably not expected and even dangours.
+ * (2) If a relational expression has no operator like:
+ * `{ dimension: 'Product' }`,
+ * An error will be thrown. Because it is probably a mistake.
+ * (3) If a logical expression has no children like
+ * `{ and: undefined }` or `{ and: [] }`,
+ * An error will be thrown. Because it is probably an mistake.
+ * (4) If intending have a condition that always `true` or always `false`,
+ * Use `true` or `flase`.
+ * The entire condition can be `true`/`false`,
+ * or also can be `{ and: [true] }`, `{ or: [false] }`
+ */
+
+
+// --------------------------------------------------
+// --- Relational Expression --------------------------
+// --------------------------------------------------
+
+/**
+ * Date string and ordinal string can be accepted.
+ */
+interface RelationalExpressionOptionByOp {
+    lt?: OptionDataValue; // less than
+    lte?: OptionDataValue; // less than or equal
+    gt?: OptionDataValue; // greater than
+    gte?: OptionDataValue; // greater than or equal
+    eq?: OptionDataValue; // equal
+    ne?: OptionDataValue; // not equal
+    reg?: RegExp | string; // RegExp
+};
+interface RelationalExpressionOptionByOpAlias {
+    value?: RelationalExpressionOptionByOp['eq'];
+
+    '<'?: OptionDataValue; // lt
+    '<='?: OptionDataValue; // lte
+    '>'?: OptionDataValue; // gt
+    '>='?: OptionDataValue; // gte
+    '='?: OptionDataValue; // eq
+    '!='?: OptionDataValue; // ne
+    '<>'?: OptionDataValue; // ne (SQL style)
+
+    // '=='?: OptionDataValue; // eq
+    // '==='?: OptionDataValue; // eq
+    // '!=='?: OptionDataValue; // eq
+
+    // ge: RelationalExpressionOptionByOp['gte'];
+    // le: RelationalExpressionOptionByOp['lte'];
+    // neq: RelationalExpressionOptionByOp['ne'];
+};
+const aliasToOpMap = createHashMap<RelationalExpressionOp, 
RelationalExpressionOpAlias>({
+    value: 'eq',
+
+    // PENDING: not good for literal semantic?
+    '<': 'lt',
+    '<=': 'lte',
+    '>': 'gt',
+    '>=': 'gte',
+    '=': 'eq',
+    '!=': 'ne',
+    '<>': 'ne'
+
+    // Might mileading for sake of the different between '==' and '===',
+    // So dont support them.
+    // '==': 'eq',
+    // '===': 'seq',
+    // '!==': 'sne'
+
+    // PENDING: Whether support some common alias "ge", "le", "neq"?
+    // ge: 'gte',
+    // le: 'lte',
+    // neq: 'ne',
+});
+
+type RelationalExpressionOp = keyof RelationalExpressionOptionByOp;
+type RelationalExpressionOpAlias = keyof RelationalExpressionOptionByOpAlias;
+
+interface RelationalExpressionOption extends
+        RelationalExpressionOptionByOp, RelationalExpressionOptionByOpAlias {
+    dimension?: DimensionLoose;
+    parse?: RelationalExpressionValueParserType;
+}
+
+type RelationalExpressionOpEvaluate = (tarVal: unknown, condVal: unknown) => 
boolean;
+
+const relationalOpEvaluateMap = createHashMap<RelationalExpressionOpEvaluate, 
RelationalExpressionOp>({
+    // PENDING: should keep supporting string compare?
+    lt: function (tarVal, condVal) {
+        return tarVal < condVal;
+    },
+    lte: function (tarVal, condVal) {
+        return tarVal <= condVal;
+    },
+    gt: function (tarVal, condVal) {
+        return tarVal > condVal;
+    },
+    gte: function (tarVal, condVal) {
+        return tarVal >= condVal;
+    },
+    eq: function (tarVal, condVal) {
+        // eq is probably most used, DO NOT use JS ==,
+        // the rule is too complicated.
+        return tarVal === condVal;
+    },
+    ne: function (tarVal, condVal) {
+        return tarVal !== condVal;
+    },
+    reg: function (tarVal, condVal: RegExp) {
+        const type = typeof tarVal;
+        return type === 'string' ? condVal.test(tarVal as string)
+            : type === 'number' ? condVal.test(tarVal + '')
+            : false;
+    }
+});
+
+function parseRegCond(condVal: unknown): RegExp {
+    // Support condVal: RegExp | string
+    return isString(condVal) ? new RegExp(condVal)
+        : isRegExp(condVal) ? condVal as RegExp
+        : null;
+}
+
+type RelationalExpressionValueParserType = 'time' | 'trim';
+type RelationalExpressionValueParser = (val: unknown) => unknown;
+const valueParserMap = createHashMap<RelationalExpressionValueParser, 
RelationalExpressionValueParserType>({
+    time: function (val): number {
+        // return timestamp.
+        return +parseDate(val);
+    },
+    trim: function (val) {
+        return typeof val === 'string' ? trim(val) : val;
+    }
+});
+
+
+// --------------------------------------------------
+// --- Logical Expression ---------------------------
+// --------------------------------------------------
+
+
+interface LogicalExpressionOption {
+    and?: LogicalExpressionSubOption[];
+    or?: LogicalExpressionSubOption[];
+    not?: LogicalExpressionSubOption;
+}
+type LogicalExpressionSubOption =
+    LogicalExpressionOption | RelationalExpressionOption | 
TrueFalseExpressionOption;
+
+
+
+// -----------------------------------------------------
+// --- Conditional Expression --------------------------
+// -----------------------------------------------------
+
+
+export type TrueExpressionOption = true;
+export type FalseExpressionOption = false;
+export type TrueFalseExpressionOption = TrueExpressionOption | 
FalseExpressionOption;
+
+export type ConditionalExpressionOption =
+    LogicalExpressionOption
+    | RelationalExpressionOption
+    | TrueFalseExpressionOption;
+
+type ValueGetterParam = Dictionary<unknown>;
+export interface ConditionalExpressionValueGetterParamGetter<VGP extends 
ValueGetterParam = ValueGetterParam> {
+    (relExpOption: RelationalExpressionOption): VGP
+}
+export interface ConditionalExpressionValueGetter<VGP extends ValueGetterParam 
= ValueGetterParam> {
+    (param: VGP): OptionDataValue
+}
+
+interface ParsedConditionInternal {
+    evaluate(): boolean;
+}
+class ConstConditionInternal implements ParsedConditionInternal {
+    value: boolean;
+    evaluate(): boolean {
+        return this.value;
+    }
+}
+class AndConditionInternal implements ParsedConditionInternal {
+    children: ParsedConditionInternal[];
+    evaluate() {
+        const children = this.children;
+        for (let i = 0; i < children.length; i++) {
+            if (!children[i].evaluate()) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
+class OrConditionInternal implements ParsedConditionInternal {
+    children: ParsedConditionInternal[];
+    evaluate() {
+        const children = this.children;
+        for (let i = 0; i < children.length; i++) {
+            if (children[i].evaluate()) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
+class NotConditionInternal implements ParsedConditionInternal {
+    child: ParsedConditionInternal;
+    evaluate() {
+        return !this.child.evaluate();
+    }
+}
+class RelationalConditionInternal implements ParsedConditionInternal {
+    valueGetterParam: ValueGetterParam;
+    valueParser: RelationalExpressionValueParser;
+    // If no parser, be null/undefined.
+    getValue: ConditionalExpressionValueGetter;
+    subCondList: {
+        condValue: unknown;
+        evaluate: RelationalExpressionOpEvaluate;
+    }[];
+
+    evaluate() {
+        const getValue = this.getValue;
+        const needParse = !!this.valueParser;
+        // Call getValue with no `this`.
+        const tarValRaw = getValue(this.valueGetterParam);
+        const tarValParsed = needParse ? this.valueParser(tarValRaw) : null;
+
+        // Relational cond follow "and" logic internally.
+        for (let i = 0; i < this.subCondList.length; i++) {
+            const subCond = this.subCondList[i];
+            if (
+                !subCond.evaluate(
+                    needParse ? tarValParsed : tarValRaw,
+                    subCond.condValue
+                )
+            ) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
+
+function parseOption(
+    exprOption: ConditionalExpressionOption,
+    getters: ConditionalGetters
+): ParsedConditionInternal {
+    if (exprOption === true || exprOption === false) {
+        const cond = new ConstConditionInternal();
+        cond.value = exprOption as boolean;
+        return cond;
+    }
+
+    let errMsg = '';
+    if (!isObjectNotArray(exprOption)) {
+        if (__DEV__) {
+            errMsg = makePrintable(
+                'Illegal config. Expect a plain object but actually', 
exprOption
+            );
+        }
+        throwError(errMsg);
+    }
+
+    if ((exprOption as LogicalExpressionOption).and) {
+        return parseAndOrOption('and', exprOption as LogicalExpressionOption, 
getters);
+    }
+    else if ((exprOption as LogicalExpressionOption).or) {
+        return parseAndOrOption('or', exprOption as LogicalExpressionOption, 
getters);
+    }
+    else if ((exprOption as LogicalExpressionOption).not) {
+        return parseNotOption(exprOption as LogicalExpressionOption, getters);
+    }
+
+    return parseRelationalOption(exprOption as RelationalExpressionOption, 
getters);
+}
+
+function parseAndOrOption(
+    op: 'and' | 'or',
+    exprOption: LogicalExpressionOption,
+    getters: ConditionalGetters
+): ParsedConditionInternal {
+    const subOptionArr = exprOption[op] as ConditionalExpressionOption[];
+    let errMsg = '';
+    if (__DEV__) {
+        errMsg = makePrintable(
+            '"and"/"or" condition should only be `' + op + ': [...]` and must 
not be empty array.',
+            'Illegal condition:', exprOption
+        );
+    }
+    if (!isArray(subOptionArr)) {
+        throwError(errMsg);
+    }
+    if (!(subOptionArr as []).length) {
+        throwError(errMsg);
+    }
+    const cond = op === 'and' ? new AndConditionInternal() : new 
OrConditionInternal();
+    cond.children = map(subOptionArr, subOption => parseOption(subOption, 
getters));
+    if (!cond.children.length) {
+        throwError(errMsg);
+    }
+    return cond;
+}
+
+function parseNotOption(
+    exprOption: LogicalExpressionOption,
+    getters: ConditionalGetters
+): ParsedConditionInternal {
+    const subOption = exprOption.not as ConditionalExpressionOption;
+    let errMsg = '';
+    if (__DEV__) {
+        errMsg = makePrintable(
+            '"not" condition should only be `not: {}`.',
+            'Illegal condition:', exprOption
+        );
+    }
+    if (!isObjectNotArray(subOption)) {
+        throwError(errMsg);
+    }
+    const cond = new NotConditionInternal();
+    cond.child = parseOption(subOption, getters);
+    if (!cond.child) {
+        throwError(errMsg);
+    }
+    return cond;
+}
+
+function parseRelationalOption(
+    exprOption: RelationalExpressionOption,
+    getters: ConditionalGetters
+): ParsedConditionInternal {
+    let errMsg = '';
+
+    const valueGetterParam = getters.prepareGetValue(exprOption);
+
+    const subCondList = [] as RelationalConditionInternal['subCondList'];
+    const exprKeys = keys(exprOption);
+
+    const parserName = exprOption.parse;
+    const valueParser = parserName ? valueParserMap.get(parserName) : null;
+
+    for (let i = 0; i < exprKeys.length; i++) {
+        const keyRaw = exprKeys[i];
+        if (keyRaw === 'parse' || getters.valueGetterAttrMap.get(keyRaw)) {
+            continue;
+        }
+
+        const op: RelationalExpressionOp = aliasToOpMap.get(keyRaw as 
RelationalExpressionOpAlias)
+            || (keyRaw as RelationalExpressionOp);
+        const evaluateHandler = relationalOpEvaluateMap.get(op);
+
+        if (!evaluateHandler) {
+            if (__DEV__) {
+                errMsg = makePrintable(
+                    'Illegal relational operation: "' + keyRaw + '" in 
condition:', exprOption
+                );
+            }
+            throwError(errMsg);
+        }
+
+        const condValueRaw = exprOption[keyRaw];
+        let condValue;
+        if (keyRaw === 'reg') {
+            condValue = parseRegCond(condValueRaw);
+            if (condValue == null) {
+                let errMsg = '';
+                if (__DEV__) {
+                    errMsg = makePrintable('Illegal regexp', condValueRaw, 
'in', exprOption);
+                }
+                throwError(errMsg);
+            }
+        }
+        else {
+            // At present, all other operators are applicable 
`RelationalExpressionValueParserType`.
+            // But if adding new parser, we should check it again.
+            condValue = valueParser ? valueParser(condValueRaw) : condValueRaw;
+        }
+
+        subCondList.push({
+            condValue: condValue,
+            evaluate: evaluateHandler
+        });
+    }
+
+    if (!subCondList.length) {
+        if (__DEV__) {
+            errMsg = makePrintable(
+                'Relational condition must have at least one operator.',
+                'Illegal condition:', exprOption
+            );
+        }
+        // No relational operator always disabled in case of dangers result.
+        throwError(errMsg);
+    }
+
+    const cond = new RelationalConditionInternal();
+    cond.valueGetterParam = valueGetterParam;
+    cond.valueParser = valueParser;
+    cond.getValue = getters.getValue;
+    cond.subCondList = subCondList;
+
+    return cond;
+}
+
+function isObjectNotArray(val: unknown): boolean {
+    return isObject(val) && !isArrayLike(val);
+}
+
+
+class ConditionalExpressionParsed {
+
+    private _cond: ParsedConditionInternal;
+
+    constructor(
+        exprOption: ConditionalExpressionOption,
+        getters: ConditionalGetters
+    ) {
+        this._cond = parseOption(exprOption, getters);
+    }
+
+    evaluate(): boolean {
+        return this._cond.evaluate();
+    }
+};
+
+interface ConditionalGetters<VGP extends ValueGetterParam = ValueGetterParam> {
+    prepareGetValue: ConditionalExpressionValueGetterParamGetter<VGP>;
+    getValue: ConditionalExpressionValueGetter<VGP>;
+    valueGetterAttrMap: HashMap<boolean, string>;
+}
+
+export function parseConditionalExpression<VGP extends ValueGetterParam = 
ValueGetterParam>(
+    exprOption: ConditionalExpressionOption,
+    getters: ConditionalGetters<VGP>
+): ConditionalExpressionParsed {
+    return new ConditionalExpressionParsed(exprOption, getters);
+}
+
diff --git a/src/util/ecData.ts b/src/util/ecData.ts
index 2dc31fc..fcd176d 100644
--- a/src/util/ecData.ts
+++ b/src/util/ecData.ts
@@ -18,7 +18,7 @@
 */
 
 import Element from 'zrender/src/Element';
-import { DataModel, ECEventData, BlurScope, InnerFocus } from './types';
+import { DataModel, ECEventData, BlurScope, InnerFocus, SeriesDataType } from 
'./types';
 import { makeInner } from './model';
 /**
  * ECData stored on graphic element
@@ -28,7 +28,7 @@ export interface ECData {
     dataModel?: DataModel;
     eventData?: ECEventData;
     seriesIndex?: number;
-    dataType?: string;
+    dataType?: SeriesDataType;
     focus?: InnerFocus;
     blurScope?: BlurScope;
 }
diff --git a/src/util/log.ts b/src/util/log.ts
index c841bac..3aaab2d 100644
--- a/src/util/log.ts
+++ b/src/util/log.ts
@@ -18,6 +18,7 @@
 */
 
 import { Dictionary } from './types';
+import { map, isString, isFunction, eqNaN, isRegExp } from 
'zrender/src/core/util';
 
 const storedLogs: Dictionary<boolean> = {};
 
@@ -37,4 +38,68 @@ export function deprecateReplaceLog(oldOpt: string, newOpt: 
string, scope?: stri
     if (__DEV__) {
         deprecateLog((scope ? `[${scope}]` : '') + `${oldOpt} is deprecated, 
use ${newOpt} instead.`);
     }
-}
\ No newline at end of file
+}
+
+export function consoleLog(...args: unknown[]) {
+    if (__DEV__) {
+        /* eslint-disable no-console */
+        if (typeof console !== 'undefined' && console.log) {
+            console.log.apply(console, args);
+        }
+        /* eslint-enable no-console */
+    }
+}
+
+/**
+ * If in __DEV__ environment, get console printable message for users hint.
+ * Parameters are separated by ' '.
+ * @usuage
+ * makePrintable('This is an error on', someVar, someObj);
+ *
+ * @param hintInfo anything about the current execution context to hint users.
+ * @throws Error
+ */
+export function makePrintable(...hintInfo: unknown[]) {
+    let msg = '';
+
+    if (__DEV__) {
+        // Fuzzy stringify for print.
+        // This code only exist in dev environment.
+        msg = map(hintInfo, arg => {
+            if (isString(arg)) {
+                // Print without quotation mark for some statement.
+                return arg;
+            }
+            else if (typeof JSON !== 'undefined' && JSON.stringify) {
+                try {
+                    return JSON.stringify(arg, function (n, val) {
+                        return val === void 0 ? 'undefined'
+                            : val === Infinity ? 'Infinity'
+                            : val === -Infinity ? '-Infinity'
+                            : eqNaN(val) ? 'NaN'
+                            : val instanceof Date ? 'Date(' + 
val.toISOString() + ')'
+                            : isFunction(val) ? 'function () { ... }'
+                            : isRegExp(val) ? val + ''
+                            : val;
+                    });
+                    // In most cases the info object is small, so do not line 
break.
+                }
+                catch (err) {
+                    return '?';
+                }
+            }
+            else {
+                return '?';
+            }
+        }).join(' ');
+    }
+
+    return msg;
+}
+
+/**
+ * @throws Error
+ */
+export function throwError(msg?: string) {
+    throw new Error(msg);
+}
diff --git a/src/util/model.ts b/src/util/model.ts
index ee975c5..699b297 100644
--- a/src/util/model.ts
+++ b/src/util/model.ts
@@ -791,7 +791,7 @@ export function parseFinder(
     }
 
     const defaultMainType = opt ? opt.defaultMainType : null;
-    const queryOptionMap = createHashMap<QueryReferringOption, 
ComponentMainType>();
+    const queryOptionMap = createHashMap<QueryReferringUserOption, 
ComponentMainType>();
     const result = {} as ParsedModelFinder;
 
     each(finder, function (value, key) {
@@ -803,7 +803,7 @@ export function parseFinder(
 
         const parsedKey = key.match(/^(\w+)(Index|Id|Name)$/) || [];
         const mainType = parsedKey[1];
-        const queryType = (parsedKey[2] || '').toLowerCase() as keyof 
QueryReferringOption;
+        const queryType = (parsedKey[2] || '').toLowerCase() as keyof 
QueryReferringUserOption;
 
         if (
             !mainType
@@ -836,7 +836,7 @@ export function parseFinder(
     return result;
 }
 
-type QueryReferringOption = {
+export type QueryReferringUserOption = {
     index?: ModelFinderIndexQuery,
     id?: ModelFinderIdQuery,
     name?: ModelFinderNameQuery,
@@ -857,7 +857,7 @@ export type QueryReferringOpt = {
 export function queryReferringComponents(
     ecModel: GlobalModel,
     mainType: ComponentMainType,
-    userOption: QueryReferringOption,
+    userOption: QueryReferringUserOption,
     opt: QueryReferringOpt
 ): {
     // Always be array rather than null/undefined, which is convenient to use.
diff --git a/src/util/number.ts b/src/util/number.ts
index d303534..68cad8b 100644
--- a/src/util/number.ts
+++ b/src/util/number.ts
@@ -286,7 +286,8 @@ export function isRadianAroundZero(val: number): boolean {
 const TIME_REG = /^(?:(\d{4})(?:[-\/](\d{1,2})(?:[-\/](\d{1,2})(?:[T 
](\d{1,2})(?::(\d{1,2})(?::(\d{1,2})(?:[.,](\d+))?)?)?(Z|[\+\-]\d\d:?\d\d)?)?)?)?)?$/;
 // jshint ignore:line
 
 /**
- * @param value These values can be accepted:
+ * @param value valid type: number | string | Date, otherwise return `new 
Date(NaN)`
+ *   These values can be accepted:
  *   + An instance of Date, represent a time in its own time zone.
  *   + Or string in a subset of ISO 8601, only including:
  *     + only year, month, date: '2012-03', '2012-03-01', '2012-03-01 05', 
'2012-03-01 05:06',
@@ -298,9 +299,9 @@ const TIME_REG = 
/^(?:(\d{4})(?:[-\/](\d{1,2})(?:[-\/](\d{1,2})(?:[T ](\d{1,2})(
  *     '2012', '2012-3-1', '2012/3/1', '2012/03/01',
  *     '2009/6/12 2:00', '2009/6/12 2:05:08', '2009/6/12 2:05:08.123'
  *   + a timestamp, which represent a time in UTC.
- * @return date
+ * @return date Never be null/undefined. If invalid, return `new Date(NaN)`.
  */
-export function parseDate(value: number | string | Date): Date {
+export function parseDate(value: unknown): Date {
     if (value instanceof Date) {
         return value;
     }
@@ -358,7 +359,7 @@ export function parseDate(value: number | string | Date): 
Date {
         return new Date(NaN);
     }
 
-    return new Date(Math.round(value));
+    return new Date(Math.round(value as number));
 }
 
 /**
diff --git a/src/util/types.ts b/src/util/types.ts
index c40b281..f475a7c 100644
--- a/src/util/types.ts
+++ b/src/util/types.ts
@@ -372,7 +372,9 @@ export const SERIES_LAYOUT_BY_ROW = 'row' as const;
 
 export type SeriesLayoutBy = typeof SERIES_LAYOUT_BY_COLUMN | typeof 
SERIES_LAYOUT_BY_ROW;
 // null/undefined/'auto': auto detect header, see 
"src/data/helper/sourceHelper".
-export type OptionSourceHeader = boolean | 'auto';
+// If number, means header lines count, or say, `startIndex`.
+// Like `sourceHeader: 2`, means line 0 and line 1 are header, data start from 
line 2.
+export type OptionSourceHeader = boolean | 'auto' | number;
 
 export type SeriesDataType = 'main' | 'node' | 'edge';
 
@@ -1395,8 +1397,11 @@ export interface SeriesSamplingOptionMixin {
 
 export interface SeriesEncodeOptionMixin {
     datasetIndex?: number;
+    datasetId?: string | number;
     seriesLayoutBy?: SeriesLayoutBy;
     sourceHeader?: OptionSourceHeader;
     dimensions?: DimensionDefinitionLoose[];
     encode?: OptionEncode
 }
+
+export type SeriesEncodableModel = SeriesModel<SeriesOption & 
SeriesEncodeOptionMixin>;
diff --git a/test/data-transform.html b/test/data-transform.html
new file mode 100644
index 0000000..05ada21
--- /dev/null
+++ b/test/data-transform.html
@@ -0,0 +1,661 @@
+<!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/facePrint.js"></script>
+        <script src="lib/testHelper.js"></script>
+        <!-- <script src="ut/lib/canteen.js"></script> -->
+        <link rel="stylesheet" href="lib/reset.css" />
+    </head>
+    <body>
+        <style>
+        </style>
+
+
+
+        <!-- <div id="main_simplest_pies"></div>
+        <div id="main_pies_encode_price"></div>
+        <div id="main_cartesian_parse_trim_time_reg"></div> -->
+        <div id="main_cartesian_sort"></div>
+
+
+
+        <script>
+            var FOOD_SALES_PRICE_HEADER =
+                ['Product', 'Sales', 'Price', 'Year'];
+            var FOOD_SALES_PRICE_NO_HEADER = [
+                ['Cake', 123, 32.12, 2011],
+                ['Cereal', 231, 14.41, 2011],
+                ['Tofu', 235, 5.14, 2011],
+                ['Dumpling', 341, 25.53, 2011],
+                ['Biscuit', 122, 29.36, 2011],
+                ['Cake', 143, 30.21, 2012],
+                ['Cereal', 201, 19.85, 2012],
+                ['Tofu', 255, 7.61, 2012],
+                ['Dumpling', 241, 27.89, 2012],
+                ['Biscuit', 102, 34.53, 2012],
+                ['Cake', 153, 28.82, 2013],
+                ['Cereal', 181, 21.16, 2013],
+                ['Tofu', 295, 4.24, 2013],
+                ['Dumpling', 281, 31.66, 2013],
+                ['Biscuit', 92, 39.82, 2013],
+                ['Cake', 223, 29.22, 2014],
+                ['Cereal', 211, 17.88, 2014],
+                ['Tofu', 345, 3.09, 2014],
+                ['Dumpling', 211, 35.05, 2014],
+                ['Biscuit', 72, 24.19, 2014]
+            ];
+            var FOOD_SALES_PRICE_WITH_HEADER =
+                [FOOD_SALES_PRICE_HEADER]
+                .concat(FOOD_SALES_PRICE_NO_HEADER);
+
+            var NAME_SCORE_DIM = {
+                Name: 0,
+                Age: 1,
+                Sex: 2,
+                Score: 3,
+                Date: 4
+            };
+            var NAME_SCORE_DIRTY_DATA_HEADER =
+                ['Name', 'Age', 'Sex', 'Score', 'Date'];
+            var NAME_SCORE_DIRTY_DATA_NO_HEADER = [
+                // This is for trim testing.
+                [' Jobs Mat ', 41, 'male', 314, '2011-02-12'],
+                // This is for edge testing (03-01, 20)
+                ['Hottlyuipe Xu ', 20, 'female', 351, '2011-03-01'],
+                [' Jone Mat ', 52, 'male', 287, '2011-02-14'],
+                ['Uty Xu', 19, 'male', 219, '2011-02-18'],
+                ['Tatum von Godden', 25, 'female', 301, '2011-04-02'],
+                ['Must Godden', 31, 'female', 235, '2011-03-19'],
+                ['Caoas Xu', 71, 'male', 318, '2011-02-24'],
+                ['Malise Mat', 67, 'female', 366, '2011-03-12'],
+            ];
+            var NAME_SCORE_DIRTY_DATA_WITH_HEADER =
+                [NAME_SCORE_DIRTY_DATA_HEADER]
+                .concat(NAME_SCORE_DIRTY_DATA_NO_HEADER);
+
+        </script>
+
+
+
+
+        <!-- ------------------------------- -->
+        <!-- ------------------------------- -->
+        <!-- ------------------------------- -->
+        <!-- ------------------------------- -->
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option = {
+                dataset: [{
+                    source: FOOD_SALES_PRICE_WITH_HEADER
+                }, {
+                    transform: {
+                        type: 'filter',
+                        config: { dimension: 'Year', value: 2011 }
+                    }
+                }, {
+                    transform: {
+                        type: 'filter',
+                        config: { dimension: 'Year', value: 2012 }
+                    }
+                }, {
+                    transform: {
+                        type: 'filter',
+                        config: { dimension: 'Year', value: 2013 }
+                    }
+                }],
+                tooltip: {},
+                series: [{
+                    type: 'pie',
+                    datasetIndex: 1,
+                    radius: 50,
+                    center: ['25%', '50%']
+                }, {
+                    type: 'pie',
+                    datasetIndex: 2,
+                    radius: 50,
+                    center: ['50%', '50%']
+                }, {
+                    type: 'pie',
+                    datasetIndex: 3,
+                    radius: 50,
+                    center: ['75%', '50%']
+                }]
+            };
+
+            var chart = testHelper.create(echarts, 'main_simplest_pies', {
+                title: [
+                    '**3 pies** should shoud **Sales data** (interger about 
hundreds)',
+                    'Pie by "Year", Sector by "Product"'
+                ],
+                height: 300,
+                option: option
+            });
+        });
+        </script>
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option = {
+                dataset: [{
+                    source: FOOD_SALES_PRICE_WITH_HEADER
+                }, {
+                    transform: {
+                        type: 'filter',
+                        config: { dimension: 'Product', value: 'Tofu' }
+                    }
+                }, {
+                    transform: {
+                        type: 'filter',
+                        config: { dimension: 'Product', value: 'Biscuit' }
+                    }
+                }, {
+                    transform: {
+                        type: 'filter',
+                        config: { dimension: 'Product', value: 'Dumpling' }
+                    }
+                }],
+                series: [{
+                    type: 'pie',
+                    datasetIndex: 1,
+                    center: ['25%', '50%'],
+                    radius: 50,
+                    encode: { itemName: 'Year', value: 'Price' },
+                }, {
+                    type: 'pie',
+                    datasetIndex: 2,
+                    center: ['50%', '50%'],
+                    radius: 50,
+                    encode: { itemName: 'Year', value: 'Price' }
+                }, {
+                    type: 'pie',
+                    datasetIndex: 3,
+                    center: ['75%', '50%'],
+                    radius: 50,
+                    encode: { itemName: 'Year', value: 'Price' }
+                }]
+            };
+
+            var chart = testHelper.create(echarts, 'main_pies_encode_price', {
+                title: [
+                    '**3 pies** should shoud **Prices data** (float like 
xx.xx)',
+                    'Pie by "Product", Sector by "Year"'
+                ],
+                height: 300,
+                option: option
+            });
+        });
+        </script>
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option = {
+                dataset: [{ source: NAME_SCORE_DIRTY_DATA_WITH_HEADER }],
+                tooltip: {},
+                grid: [],
+                xAxis: [],
+                yAxis: [],
+                series: []
+            };
+
+            var leftStart = 50;
+            var leftBase = leftStart;
+            var topBase = 30;
+            var gridWidth = 100;
+            var gridHeight = 100;
+            var gapWidth = 70;
+            var gapHeight = 80;
+            var chartWidth = 800;
+
+            function addCartesian(opt) {
+                option.grid.push({
+                    left: leftBase,
+                    top: topBase,
+                    width: gridWidth,
+                    height: gridHeight
+                });
+
+                leftBase += gridWidth + gapWidth;
+                if (leftBase + gridWidth > chartWidth) {
+                    leftBase = leftStart;
+                    topBase += gridHeight + gapHeight;
+                }
+
+                option.xAxis.push({
+                    name: opt.xAxis.name,
+                    type: 'category',
+                    nameLocation: 'middle',
+                    nameGap: 30,
+                    gridIndex: option.grid.length - 1
+                });
+                option.yAxis.push({
+                    gridIndex: option.grid.length - 1
+                });
+
+                var series = opt.series;
+
+                series.type = 'scatter';
+                series.xAxisIndex = option.xAxis.length - 1;
+                series.yAxisIndex = option.yAxis.length - 1;
+                series.label = { show: true, position: 'bottom' };
+                series.encode = {
+                    x: NAME_SCORE_DIM.Date,
+                    y: NAME_SCORE_DIM.Score,
+                    label: series.encode && series.encode.label || 
[NAME_SCORE_DIM.Name],
+                    tooltip: [
+                        NAME_SCORE_DIM.Name,
+                        NAME_SCORE_DIM.Date,
+                        NAME_SCORE_DIM.Score,
+                        NAME_SCORE_DIM.Sex,
+                        NAME_SCORE_DIM.Age
+                    ]
+                };
+                option.series.push(series);
+            }
+
+
+            option.dataset.push({
+                id: 'a',
+                transform: {
+                    type: 'filter',
+                    // print: true,
+                    config: { dimension: NAME_SCORE_DIM.Name, eq: 'Jobs Mat', 
parse: 'trim' }
+                }
+            });
+            addCartesian({
+                series: {
+                    datasetId: 'a'
+                },
+                xAxis: { name: 'Only show "Jobs Mat"' }
+            });
+
+            option.dataset.push({
+                id: 'b',
+                transform: {
+                    type: 'filter',
+                    // print: true,
+                    config: { dimension: NAME_SCORE_DIM.Date, lt: '2011-03', 
gte: '2011-02', parse: 'time' }
+                }
+            });
+            addCartesian({
+                series: {
+                    datasetId: 'b'
+                },
+                xAxis: { name: 'Show four points\nDate in 2011-02' }
+            });
+
+            option.dataset.push({
+                id: 'c',
+                transform: {
+                    type: 'filter',
+                    // print: true,
+                    config: { dimension: NAME_SCORE_DIM.Date, lte: '2011-03', 
gte: '2011-02-29', parse: 'time' }
+                }
+            });
+            addCartesian({
+                series: {
+                    datasetId: 'c'
+                },
+                xAxis: { name: 'Show "Hottlyuipe Xu"' }
+            });
+
+            option.dataset.push({
+                id: 'd',
+                transform: {
+                    type: 'filter',
+                    // print: true,
+                    config: { dimension: NAME_SCORE_DIM.Name, reg: /\sXu$/, 
parse: 'trim' }
+                }
+            });
+            addCartesian({
+                series: {
+                    datasetId: 'd'
+                },
+                xAxis: { name: 'Show three points\nname reg /sXu$/' }
+            });
+
+            option.dataset.push({
+                id: 'e',
+                transform: {
+                    type: 'filter',
+                    // print: true,
+                    config: { dimension: NAME_SCORE_DIM.Sex, ne: 'male', 
parse: 'trim' }
+                }
+            });
+            addCartesian({
+                series: {
+                    datasetId: 'e',
+                    encode: { label: [NAME_SCORE_DIM.Sex] }
+                },
+                xAxis: { name: 'Show four points\n!male' }
+            });
+
+            option.dataset.push({
+                id: 'f',
+                transform: {
+                    type: 'filter',
+                    // print: true,
+                    config: {
+                        and: [
+                            { dimension: NAME_SCORE_DIM.Sex, eq: 'male', 
parse: 'trim' },
+                            { dimension: NAME_SCORE_DIM.Score, '>': 300 }
+                        ]
+                    }
+                }
+            });
+            addCartesian({
+                series: {
+                    datasetId: 'f',
+                    encode: { label: [NAME_SCORE_DIM.Sex] }
+                },
+                xAxis: { name: 'Show two points\nmale > 300' }
+            });
+
+
+            option.dataset.push({
+                id: 'g',
+                transform: {
+                    type: 'filter',
+                    // print: true,
+                    config: {
+                        and: [
+                            { dimension: NAME_SCORE_DIM.Sex, eq: 'female' },
+                            {
+                                or: [
+                                    { dimension: NAME_SCORE_DIM.Age, '>=': 20, 
'<=': 30 },
+                                    { dimension: NAME_SCORE_DIM.Age, '>=': 60 }
+                                ]
+                            }
+                        ]
+                    }
+                }
+            });
+            addCartesian({
+                series: {
+                    datasetId: 'g',
+                    encode: { label: [NAME_SCORE_DIM.Sex] }
+                },
+                xAxis: { name: 'Show three points\nfemale && (20-30 || 60)' }
+            });
+
+            option.dataset.push({
+                id: 'h',
+                transform: {
+                    type: 'filter',
+                    // print: true,
+                    config: {
+                        not: {
+                            and: [
+                                { dimension: NAME_SCORE_DIM.Sex, eq: 'female' 
},
+                                {
+                                    or: [
+                                        { dimension: NAME_SCORE_DIM.Age, '>=': 
20, '<=': 30 },
+                                        { dimension: NAME_SCORE_DIM.Age, '>=': 
60 }
+                                    ]
+                                }
+                            ]
+                        }
+                    }
+                }
+            });
+            addCartesian({
+                series: {
+                    datasetId: 'h',
+                    encode: { label: [NAME_SCORE_DIM.Sex] }
+                },
+                xAxis: { name: 'Show five points\n!(female && (20-30 || 60))' }
+            });
+
+
+            option.dataset.push({
+                id: 'i',
+                transform: {
+                    type: 'filter',
+                    // print: true,
+                    config: true
+                }
+            });
+            addCartesian({
+                series: {
+                    datasetId: 'i',
+                    encode: { label: [NAME_SCORE_DIM.Sex] }
+                },
+                xAxis: { name: 'Show all eight points\nconfig: true' }
+            });
+
+
+
+
+            var chart = testHelper.create(echarts, 
'main_cartesian_parse_trim_time_reg', {
+                title: [
+                    'Check each cartesians.',
+                    'The expectationa are below each cartesian.'
+                ],
+                width: chartWidth,
+                height: 600,
+                option: option
+            });
+        });
+        </script>
+
+
+
+
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            var option = {
+                dataset: [{ source: NAME_SCORE_DIRTY_DATA_WITH_HEADER }],
+                tooltip: {},
+                grid: [],
+                xAxis: [],
+                yAxis: [],
+                series: []
+            };
+
+            var leftStart = 50;
+            var leftBase = leftStart;
+            var topBase = 30;
+            var gridWidth = 100;
+            var gridHeight = 100;
+            var gapWidth = 70;
+            var gapHeight = 80;
+            var chartWidth = 800;
+
+            function addCartesian(opt) {
+                option.grid.push({
+                    left: leftBase,
+                    top: topBase,
+                    width: gridWidth,
+                    height: gridHeight
+                });
+
+                leftBase += gridWidth + gapWidth;
+                if (leftBase + gridWidth > chartWidth) {
+                    leftBase = leftStart;
+                    topBase += gridHeight + gapHeight;
+                }
+
+                option.xAxis.push({
+                    name: opt.xAxis.name,
+                    type: 'category',
+                    nameLocation: 'middle',
+                    nameGap: 30,
+                    gridIndex: option.grid.length - 1
+                });
+                option.yAxis.push({
+                    gridIndex: option.grid.length - 1
+                });
+
+                var series = opt.series;
+
+                series.type = 'bar';
+                series.xAxisIndex = option.xAxis.length - 1;
+                series.yAxisIndex = option.yAxis.length - 1;
+                series.label = { show: true, position: 'top' };
+                series.encode = {
+                    x: NAME_SCORE_DIM.Date,
+                    y: NAME_SCORE_DIM.Score,
+                    label: series.encode && series.encode.label || 
[NAME_SCORE_DIM.Name],
+                    tooltip: [
+                        NAME_SCORE_DIM.Name,
+                        NAME_SCORE_DIM.Date,
+                        NAME_SCORE_DIM.Score,
+                        NAME_SCORE_DIM.Sex,
+                        NAME_SCORE_DIM.Age
+                    ]
+                };
+                option.series.push(series);
+            }
+
+
+            option.dataset.push({
+                id: 'a',
+                transform: {
+                    type: 'sort',
+                    // print: true,
+                    config: { dimension: NAME_SCORE_DIM.Score, order: 'asc' }
+                }
+            });
+            addCartesian({
+                series: {
+                    datasetId: 'a'
+                },
+                xAxis: { name: 'Show all eight\norder by Score asc' }
+            });
+
+            option.dataset.push({
+                id: 'b',
+                transform: {
+                    type: 'sort',
+                    // print: true,
+                    config: { dimension: NAME_SCORE_DIM.Age, order: 'desc' }
+                }
+            });
+            addCartesian({
+                series: {
+                    datasetId: 'b',
+                    encode: { label: NAME_SCORE_DIM.Age }
+                },
+                xAxis: { name: 'Show all eight\norder by Age desc' }
+            });
+
+            option.dataset.push({
+                id: 'c',
+                transform: {
+                    type: 'sort',
+                    // print: true,
+                    config: [
+                        { dimension: NAME_SCORE_DIM.Sex, order: 'asc' },
+                        { dimension: NAME_SCORE_DIM.Score, order: 'desc' }
+                    ]
+                }
+            });
+            addCartesian({
+                series: {
+                    datasetId: 'c',
+                    encode: { label: NAME_SCORE_DIM.Sex }
+                },
+                xAxis: { name: 'Show all eight\nSex asc, Score desc' }
+            });
+
+            option.dataset.push({
+                id: 'd',
+                transform: {
+                    type: 'sort',
+                    // print: true,
+                    config: [
+                        { dimension: NAME_SCORE_DIM.Date, order: 'asc', parse: 
'time' }
+                    ]
+                }
+            });
+            addCartesian({
+                series: {
+                    datasetId: 'd'
+                },
+                xAxis: { name: 'Show all eight\nDate asc' }
+            });
+
+
+            option.dataset.push({
+                id: 'e',
+                transform: [{
+                    type: 'filter',
+                    // print: true,
+                    config: { dimension: NAME_SCORE_DIM.Age, lte: 40, gte: 20 }
+                }, {
+                    type: 'sort',
+                    // print: true,
+                    config: { dimension: NAME_SCORE_DIM.Score, order: 'asc' }
+                }]
+            });
+            addCartesian({
+                series: {
+                    datasetId: 'e'
+                },
+                xAxis: { name: 'Show three ponits\nFilter by Age 20-40\nOrder 
by Score' }
+            });
+
+
+            var chart = testHelper.create(echarts, 'main_cartesian_sort', {
+                title: [
+                    'Check each cartesians.',
+                    'The expectationa are below each cartesian.'
+                ],
+                width: chartWidth,
+                height: 600,
+                option: option
+            });
+        });
+        </script>
+
+
+
+
+
+    </body>
+</html>
+


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

Reply via email to