You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by su...@apache.org on 2020/08/14 04:35:29 UTC

[incubator-echarts] 02/02: fix: [data-transform] (1) clarity the detail of value comparison. (2) rename "parse" to "parser".

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

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

commit 81d1306b5fc7dc528b01a360fe042b2b2ed72736
Author: 100pah <su...@gmail.com>
AuthorDate: Fri Aug 14 05:09:54 2020 +0800

    fix: [data-transform] (1) clarity the detail of value comparison. (2) rename "parse" to "parser".
---
 src/component/transform/sortTransform.ts  |  59 ++--
 src/data/helper/dataValueHelper.ts        | 206 ++++++------
 src/util/conditionalExpression.ts         |  32 +-
 src/util/number.ts                        |  15 +-
 test/data-transform.html                  | 173 ++++++++--
 test/ut/spec/data/dataValueHelper.test.js | 531 ++++++++++++++++++++----------
 6 files changed, 662 insertions(+), 354 deletions(-)

diff --git a/src/component/transform/sortTransform.ts b/src/component/transform/sortTransform.ts
index cd47616..8f6f184 100644
--- a/src/component/transform/sortTransform.ts
+++ b/src/component/transform/sortTransform.ts
@@ -22,10 +22,10 @@ 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 { isArray, each } from 'zrender/src/core/util';
 import { normalizeToArray } from '../../util/model';
 import {
-    RawValueParserType, getRawValueParser, createRelationalComparator
+    RawValueParserType, getRawValueParser, SortOrderComparator
 } from '../../data/helper/dataValueHelper';
 
 /**
@@ -54,12 +54,13 @@ export interface SortTransformOption extends DataTransformOption {
 // PENDING: whether support { dimension: 'score', order: 'asc' } ?
 type OrderExpression = {
     dimension: DimensionLoose;
-    order: SortOrder;
-    parse?: RawValueParserType;
+    order: 'asc' | 'desc';
+    parser?: RawValueParserType;
+    // Value that is not comparable (like null/undefined) will be
+    // put to head or tail.
+    incomparable?: 'min' | 'max';
 };
 
-type SortOrder = 'asc' | 'desc';
-const SortOrderValidMap = { asc: true, desc: true } as const;
 
 let sampleLog = '';
 if (__DEV__) {
@@ -95,13 +96,14 @@ export const sortTransform: ExternalDataTransform<SortTransformOption> = {
 
         const orderDefList: {
             dimIdx: DimensionIndex;
-            orderReturn: -1 | 1;
             parser: ReturnType<typeof getRawValueParser>;
+            comparator: SortOrderComparator
         }[] = [];
         each(orderExprList, function (orderExpr) {
             const dimLoose = orderExpr.dimension;
             const order = orderExpr.order;
-            const parserName = orderExpr.parse;
+            const parserName = orderExpr.parser;
+            const incomparable = orderExpr.incomparable;
 
             if (dimLoose == null) {
                 if (__DEV__) {
@@ -110,13 +112,28 @@ export const sortTransform: ExternalDataTransform<SortTransformOption> = {
                 throwError(errMsg);
             }
 
-            if (!hasOwn(SortOrderValidMap, order)) {
+            if (order !== 'asc' && order !== 'desc') {
                 if (__DEV__) {
                     errMsg = 'Sort transform config must has "order" specified.' + sampleLog;
                 }
                 throwError(errMsg);
             }
 
+            if (incomparable && (incomparable !== 'min' && incomparable !== 'max')) {
+                let errMsg = '';
+                if (__DEV__) {
+                    errMsg = 'incomparable must be "min" or "max" rather than "' + incomparable + '".';
+                }
+                throwError(errMsg);
+            }
+            if (order !== 'asc' && order !== 'desc') {
+                let errMsg = '';
+                if (__DEV__) {
+                    errMsg = 'order must be "asc" or "desc" rather than "' + order + '".';
+                }
+                throwError(errMsg);
+            }
+
             const dimInfo = source.getDimensionInfo(dimLoose);
             if (!dimInfo) {
                 if (__DEV__) {
@@ -142,8 +159,8 @@ export const sortTransform: ExternalDataTransform<SortTransformOption> = {
 
             orderDefList.push({
                 dimIdx: dimInfo.index,
-                orderReturn: order === 'asc' ? -1 : 1,
-                parser: parser
+                parser: parser,
+                comparator: new SortOrderComparator(order, incomparable)
             });
         });
 
@@ -170,9 +187,6 @@ export const sortTransform: ExternalDataTransform<SortTransformOption> = {
             resultData.push(source.getRawDataItem(i));
         }
 
-        const lt = createRelationalComparator('lt');
-        const gt = createRelationalComparator('gt');
-
         resultData.sort(function (item0, item1) {
             if (item0 === headerPlaceholder) {
                 return -1;
@@ -180,15 +194,6 @@ export const sortTransform: ExternalDataTransform<SortTransformOption> = {
             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);
@@ -197,11 +202,9 @@ export const sortTransform: ExternalDataTransform<SortTransformOption> = {
                     val0 = orderDef.parser(val0) as OptionDataValue;
                     val1 = orderDef.parser(val1) as OptionDataValue;
                 }
-                if (lt.evaluate(val0, val1)) {
-                    return orderDef.orderReturn;
-                }
-                else if (gt.evaluate(val0, val1)) {
-                    return -orderDef.orderReturn;
+                const result = orderDef.comparator.evaluate(val0, val1);
+                if (result !== 0) {
+                    return result;
                 }
             }
             return 0;
diff --git a/src/data/helper/dataValueHelper.ts b/src/data/helper/dataValueHelper.ts
index 9214bb3..07c98e0 100644
--- a/src/data/helper/dataValueHelper.ts
+++ b/src/data/helper/dataValueHelper.ts
@@ -21,6 +21,7 @@ import { ParsedValue, DimensionType } from '../../util/types';
 import OrdinalMeta from '../OrdinalMeta';
 import { parseDate, numericToNumber } from '../../util/number';
 import { createHashMap, trim, hasOwn } from 'zrender/src/core/util';
+import { throwError } from '../../util/log';
 
 
 /**
@@ -99,51 +100,99 @@ export function getRawValueParser(type: RawValueParserType): RawValueParser {
 
 
 
-export interface UnaryExpression {
-    evaluate(val: unknown): unknown;
-}
-export interface BinaryExpression {
-    evaluate(lval: unknown, rval: unknown): unknown;
+export interface FilterComparator {
+    evaluate(val: unknown): boolean;
 }
 
-class OrderComparatorUnary implements UnaryExpression {
-    _rval: unknown;
-    _rvalTypeof: string; // typeof rval
-    _rvalFloat: number;
-    _rvalIsNumeric: boolean;
-    _opFn: (lval: unknown, rval: unknown) => boolean;
+const ORDER_COMPARISON_OP_MAP: {
+    [key in OrderRelationOperator]: ((lval: unknown, rval: unknown) => boolean)
+} = {
+    lt: (lval, rval) => lval < rval,
+    lte: (lval, rval) => lval <= rval,
+    gt: (lval, rval) => lval > rval,
+    gte: (lval, rval) => lval >= rval
+};
+
+class FilterOrderComparator implements FilterComparator {
+    private _rvalFloat: number;
+    private _opFn: (lval: unknown, rval: unknown) => boolean;
+    constructor(op: OrderRelationOperator, rval: unknown) {
+        if (typeof rval !== 'number') {
+            let errMsg = '';
+            if (__DEV__) {
+                errMsg = 'rvalue of "<", ">", "<=", ">=" can only be number in filter.';
+            }
+            throwError(errMsg);
+        }
+        this._opFn = ORDER_COMPARISON_OP_MAP[op];
+        this._rvalFloat = numericToNumber(rval);
+    }
     // Performance sensitive.
     evaluate(lval: unknown): boolean {
         // Most cases is 'number', and typeof maybe 10 times faseter than parseFloat.
-        const lvalIsNumber = typeof lval === 'number';
-        return (lvalIsNumber && this._rvalIsNumeric)
+        return typeof lval === 'number'
             ? this._opFn(lval, this._rvalFloat)
-            : (lvalIsNumber || this._rvalTypeof === 'number')
-            ? this._opFn(numericToNumber(lval), this._rvalFloat)
-            : false;
+            : this._opFn(numericToNumber(lval), this._rvalFloat);
     }
 }
-class OrderComparatorBinary implements BinaryExpression {
-    _opFn: (lval: unknown, rval: unknown) => boolean;
+
+export class SortOrderComparator {
+    private _incomparable: number;
+    private _resultLT: -1 | 1;
+    /**
+     * @param order by defualt: 'asc'
+     * @param incomparable by defualt: Always on the tail.
+     *        That is, if 'asc' => 'max', if 'desc' => 'min'
+     */
+    constructor(order: 'asc' | 'desc', incomparable: 'min' | 'max') {
+        const isDesc = order === 'desc';
+        this._resultLT = isDesc ? 1 : -1;
+        if (incomparable == null) {
+            incomparable = isDesc ? 'min' : 'max';
+        }
+        this._incomparable = incomparable === 'min' ? -Infinity : Infinity;
+    }
     // Performance sensitive.
-    evaluate(lval: unknown, rval: unknown): boolean {
+    evaluate(lval: unknown, rval: unknown): -1 | 0 | 1 {
         // Most cases is 'number', and typeof maybe 10 times faseter than parseFloat.
-        const lvalIsNumber = typeof lval === 'number';
-        const rvalIsNumber = typeof rval === 'number';
-        return (lvalIsNumber && rvalIsNumber)
-            ? this._opFn(lval, rval)
-            : (lvalIsNumber || rvalIsNumber)
-            ? this._opFn(numericToNumber(lval), numericToNumber(rval))
-            : false;
+        const lvalTypeof = typeof lval;
+        const rvalTypeof = typeof rval;
+        let lvalFloat = lvalTypeof === 'number' ? lval : numericToNumber(lval);
+        let rvalFloat = rvalTypeof === 'number' ? rval : numericToNumber(rval);
+        const lvalIncmpr = isNaN(lvalFloat as number);
+        const rvalIncmpr = isNaN(rvalFloat as number);
+        if (lvalIncmpr) {
+            lvalFloat = this._incomparable;
+        }
+        if (rvalIncmpr) {
+            rvalFloat = this._incomparable;
+        }
+        // In most cases, pure string sort has no meanings. But it can exists when need to
+        // group two categories (and order by anthor dimension meanwhile).
+        // But if we support string sort, we still need to avoid the misleading of `'2' > '12'`,
+        // and support '-' means empty, and trade `'abc' > 2` as incomparable.
+        // So we support string comparison only if both lval and rval are string and not numeric.
+        if (lvalIncmpr && rvalIncmpr && lvalTypeof === 'string' && rvalTypeof === 'string') {
+            lvalFloat = lval;
+            rvalFloat = rval;
+        }
+        return lvalFloat < rvalFloat ? this._resultLT
+            : lvalFloat > rvalFloat ? (-this._resultLT as -1 | 1)
+            : 0;
     }
 }
 
-class EqualityComparatorUnary implements UnaryExpression {
-    _rval: unknown;
-    _rvalTypeof: string; // typeof rval
-    _rvalFloat: number;
-    _rvalIsNumeric: boolean;
-    _isEq: boolean;
+class FilterEqualityComparator implements FilterComparator {
+    private _isEQ: boolean;
+    private _rval: unknown;
+    private _rvalTypeof: string;
+    private _rvalFloat: number;
+    constructor(isEq: boolean, rval: unknown) {
+        this._rval = rval;
+        this._isEQ = isEq;
+        this._rvalTypeof = typeof rval;
+        this._rvalFloat = numericToNumber(rval);
+    }
     // Performance sensitive.
     evaluate(lval: unknown): boolean {
         let eqResult = lval === this._rval;
@@ -153,80 +202,47 @@ class EqualityComparatorUnary implements UnaryExpression {
                 eqResult = numericToNumber(lval) === this._rvalFloat;
             }
         }
-        return this._isEq ? eqResult : !eqResult;
+        return this._isEQ ? eqResult : !eqResult;
     }
 }
 
-class EqualityComparatorBinary implements BinaryExpression {
-    _isEq: boolean;
-    // Performance sensitive.
-    evaluate(lval: unknown, rval: unknown): boolean {
-        let eqResult = lval === rval;
-        if (!eqResult) {
-            const lvalTypeof = typeof lval;
-            const rvalTypeof = typeof rval;
-            if (lvalTypeof !== rvalTypeof && (lvalTypeof === 'number' || rvalTypeof === 'number')) {
-                eqResult = numericToNumber(lval) === numericToNumber(rval);
-            }
-        }
-        return this._isEq ? eqResult : !eqResult;
-    }
-}
-
-const ORDER_COMPARISON_OP_MAP = {
-    lt: (tarVal: unknown, condVal: unknown) => tarVal < condVal,
-    lte: (tarVal: unknown, condVal: unknown) => tarVal <= condVal,
-    gt: (tarVal: unknown, condVal: unknown) => tarVal > condVal,
-    gte: (tarVal: unknown, condVal: unknown) => tarVal >= condVal
-} as const;
-
-export type RelationalOperator = 'lt' | 'lte' | 'gt' | 'gte' | 'eq' | 'ne';
+type OrderRelationOperator = 'lt' | 'lte' | 'gt' | 'gte';
+export type RelationalOperator = OrderRelationOperator | 'eq' | 'ne';
 
 /**
- * [COMPARISON_RULE]
- * `lt`, `lte`, `gt`, `gte`:
- * + If two "number" or a "number" and a "numeric": convert to number and compare.
- * + Else return `false`.
+ * [FILTER_COMPARISON_RULE]
+ * `lt`|`lte`|`gt`|`gte`:
+ * + rval must be a number. And lval will be converted to number (`numericToNumber`) to compare.
  * `eq`:
- * + If same type, compare with ===.
- * + If two "number" or a "number" and a "numeric": convert to number and compare.
+ * + If same type, compare with `===`.
+ * + If there is one number, convert to number (`numericToNumber`) to compare.
  * + Else return `false`.
  * `ne`:
  * + Not `eq`.
  *
- * Definition of "numeric": see `util/number.ts#numericToNumber`.
+ * [SORT_COMPARISON_RULE]
+ * Only `lt`|`gt`.
+ * Always convert to number (`numericToNumer`) to compare.
+ * (e.g., consider case: [12, " 13 ", " 14 ", null, 15])
+ *
+ * [CHECK_LIST_OF_THE_RULE_DESIGN]
+ * + Do not support string comparison until required. And also need to
+ *   void the misleading of "2" > "12".
+ * + Should avoid the misleading case:
+ *   `" 22 " gte "22"` is `true` but `" 22 " eq "22"` is `false`.
+ * + JS bad case should be avoided: null <= 0, [] <= 0, ' ' <= 0, ...
+ * + Only "numeric" can be converted to comparable number, otherwise converted to NaN.
+ *   See `util/number.ts#numericToNumber`.
  *
- * [MEMO]
- * + Do not support string comparison until required. And also need to consider the
- *   misleading of "2" > "12".
- * + JS bad case considered: null <= 0, [] <= 0, ' ' <= 0, ...
+ * @return If `op` is not `RelationalOperator`, return null;
  */
-export function createRelationalComparator(op: RelationalOperator): BinaryExpression;
-export function createRelationalComparator(op: RelationalOperator, isUnary: true, rval: unknown): UnaryExpression;
-export function createRelationalComparator(
-    op: RelationalOperator,
-    isUnary?: true,
+export function createFilterComparator(
+    op: string,
     rval?: unknown
-): UnaryExpression | BinaryExpression {
-    let comparator;
-    if (op === 'eq' || op === 'ne') {
-        comparator = isUnary ? new EqualityComparatorUnary() : new EqualityComparatorBinary();
-        comparator._isEq = op === 'eq';
-    }
-    else {
-        comparator = isUnary ? new OrderComparatorUnary() : new OrderComparatorBinary();
-        comparator._opFn = ORDER_COMPARISON_OP_MAP[op];
-    }
-    if (isUnary) {
-        const unaryComp = comparator as OrderComparatorUnary | EqualityComparatorUnary;
-        unaryComp._rval = rval;
-        unaryComp._rvalTypeof = typeof rval;
-        const rvalFloat = unaryComp._rvalFloat = numericToNumber(rval);
-        unaryComp._rvalIsNumeric = !isNaN(rvalFloat); // eslint-disable-line eqeqeq
-    }
-    return comparator;
-}
-
-export function isRelationalOperator(op: string): op is RelationalOperator {
-    return hasOwn(ORDER_COMPARISON_OP_MAP, op) || op === 'eq' || op === 'ne';
+): FilterComparator {
+    return (op === 'eq' || op === 'ne')
+        ? new FilterEqualityComparator(op === 'eq', rval)
+        : hasOwn(ORDER_COMPARISON_OP_MAP, op)
+        ? new FilterOrderComparator(op as OrderRelationOperator, rval)
+        : null;
 }
diff --git a/src/util/conditionalExpression.ts b/src/util/conditionalExpression.ts
index b871b09..2d9b752 100644
--- a/src/util/conditionalExpression.ts
+++ b/src/util/conditionalExpression.ts
@@ -23,13 +23,13 @@ import {
 } from 'zrender/src/core/util';
 import { throwError, makePrintable } from './log';
 import {
-    RawValueParserType, getRawValueParser, isRelationalOperator,
-    createRelationalComparator, RelationalOperator, UnaryExpression
+    RawValueParserType, getRawValueParser,
+    RelationalOperator, FilterComparator, createFilterComparator
 } from '../data/helper/dataValueHelper';
 
 
 // PENDING:
-// (1) Support more parser like: `parse: 'trim'`, `parse: 'lowerCase'`, `parse: 'year'`, `parse: 'dayOfWeek'`?
+// (1) Support more parser like: `parser: 'trim'`, `parser: 'lowerCase'`, `parser: 'year'`, `parser: '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.
@@ -77,24 +77,24 @@ import {
  * ```js
  * // Trim if string
  * {
- *     parse: 'trim',
+ *     parser: 'trim',
  *     eq: 'Flowers'
  * }
  * // Parse as time and enable arithmetic relation comparison.
  * {
- *     parse: 'time',
+ *     parser: 'time',
  *     lt: '2012-12-12'
  * }
  * // Normalize number-like string and make '-' to Null.
  * {
- *     parse: 'time',
+ *     parser: 'time',
  *     lt: '2012-12-12'
  * }
  * // Normalize to number:
  * // + number-like string (like '  123  ') can be converted to a number.
  * // + where null/undefined or other string will be converted to NaN.
  * {
- *     parse: 'number',
+ *     parser: 'number',
  *     eq: 2011
  * }
  * // RegExp, include the feature in SQL: `like '%xxx%'`.
@@ -164,13 +164,13 @@ type RelationalExpressionOptionByOpAlias = Record<keyof typeof RELATIONAL_EXPRES
 interface RelationalExpressionOption extends
         RelationalExpressionOptionByOp, RelationalExpressionOptionByOpAlias {
     dimension?: DimensionLoose;
-    parse?: RawValueParserType;
+    parser?: RawValueParserType;
 }
 
 type RelationalExpressionOpEvaluate = (tarVal: unknown, condVal: unknown) => boolean;
 
 
-class RegExpEvaluator implements UnaryExpression {
+class RegExpEvaluator implements FilterComparator {
     private _condVal: RegExp;
 
     constructor(rVal: unknown) {
@@ -279,7 +279,7 @@ class RelationalConditionInternal implements ParsedConditionInternal {
     valueParser: ReturnType<typeof getRawValueParser>;
     // If no parser, be null/undefined.
     getValue: ConditionalExpressionValueGetter;
-    subCondList: UnaryExpression[];
+    subCondList: FilterComparator[];
 
     evaluate() {
         const needParse = !!this.valueParser;
@@ -392,12 +392,12 @@ function parseRelationalOption(
     const subCondList = [] as RelationalConditionInternal['subCondList'];
     const exprKeys = keys(exprOption);
 
-    const parserName = exprOption.parse;
+    const parserName = exprOption.parser;
     const valueParser = parserName ? getRawValueParser(parserName) : null;
 
     for (let i = 0; i < exprKeys.length; i++) {
         const keyRaw = exprKeys[i];
-        if (keyRaw === 'parse' || getters.valueGetterAttrMap.get(keyRaw)) {
+        if (keyRaw === 'parser' || getters.valueGetterAttrMap.get(keyRaw)) {
             continue;
         }
 
@@ -406,12 +406,8 @@ function parseRelationalOption(
             : (keyRaw as keyof RelationalExpressionOptionByOp);
         const condValueRaw = exprOption[keyRaw];
         const condValueParsed = valueParser ? valueParser(condValueRaw) : condValueRaw;
-        const evaluator =
-            isRelationalOperator(op)
-            ? createRelationalComparator(op, true, condValueParsed)
-            : op === 'reg'
-            ? new RegExpEvaluator(condValueParsed)
-            : null;
+        const evaluator = createFilterComparator(op, condValueParsed)
+            || (op === 'reg' && new RegExpEvaluator(condValueParsed));
 
         if (!evaluator) {
             if (__DEV__) {
diff --git a/src/util/number.ts b/src/util/number.ts
index b2ae043..775d51a 100644
--- a/src/util/number.ts
+++ b/src/util/number.ts
@@ -549,22 +549,19 @@ export function reformIntervals(list: IntervalItem[]): IntervalItem[] {
  *     non-string, ...
  *
  * @test See full test cases in `test/ut/spec/util/number.js`.
+ * @return Must be a typeof number. If not numeric, return NaN.
  */
 export function numericToNumber(val: unknown): number {
     const valFloat = parseFloat(val as string);
-    return isNumericHavingParseFloat(val, valFloat) ? valFloat : NaN;
+    return (
+        valFloat == val // eslint-disable-line eqeqeq
+        && (valFloat !== 0 || typeof val !== 'string' || val.indexOf('x') <= 0) // For case ' 0x0 '.
+    ) ? valFloat : NaN;
 }
 
 /**
  * Definition of "numeric": see `numericToNumber`.
  */
 export function isNumeric(val: unknown): val is number {
-    return isNumericHavingParseFloat(val, parseFloat(val as string));
-}
-
-function isNumericHavingParseFloat(val: unknown, valFloat: number): val is number {
-    return (
-        valFloat == val // eslint-disable-line eqeqeq
-        && (valFloat !== 0 || typeof val !== 'string' || val.indexOf('x') <= 0) // For case ' 0x0 '.
-    );
+    return !isNaN(numericToNumber(val));
 }
diff --git a/test/data-transform.html b/test/data-transform.html
index 9a11dc7..0f23efe 100644
--- a/test/data-transform.html
+++ b/test/data-transform.html
@@ -80,21 +80,24 @@ under the License.
                 Age: 1,
                 Sex: 2,
                 Score: 3,
-                Date: 4
+                Date: 4,
+                DirtyNumber: 5,
+                Numeric: 6,
+                HasEmpty: 7
             };
             var NAME_SCORE_DIRTY_DATA_HEADER =
-                ['Name', 'Age', 'Sex', 'Score', 'Date'];
+                ['Name', 'Age', 'Sex', 'Score', 'Date', 'DirtyNumber', 'Numeric', 'HasEmpty'];
             var NAME_SCORE_DIRTY_DATA_NO_HEADER = [
                 // This is for trim testing.
-                [' Jobs Mat ', 41, 'male', 314, '2011-02-12'],
+                [' Jobs Mat ', 41, 'male', 314, '2011-02-12', '13', ' 91000 ', 45 ],
                 // 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'],
+                ['Hottlyuipe Xu ', 20, 'female', 351, '2011-03-01', 44, ' 83000 ', 13 ],
+                [' Jone Mat ', 52, 'male', 287, '2011-02-14', null, ' 43000 ', null ],
+                ['Uty Xu', 19, 'male', 219, '2011-02-18', undefined, ' 63000 ', 81 ],
+                ['Tatum von Godden', 25, 'female', 301, '2011-04-02', '-', ' 13000 ', undefined ],
+                ['Must Godden', 31, 'female', 235, '2011-03-19', ' 454', '-', 32 ],
+                ['Caoas Xu', 71, 'male', 318, '2011-02-24', NaN, ' 73000 ', '-' ],
+                ['Malise Mat', 67, 'female', 366, '2011-03-12', '232a', ' 23000 ', 19 ]
             ];
             var NAME_SCORE_DIRTY_DATA_WITH_HEADER =
                 [NAME_SCORE_DIRTY_DATA_HEADER]
@@ -287,7 +290,8 @@ under the License.
                         NAME_SCORE_DIM.Date,
                         NAME_SCORE_DIM.Score,
                         NAME_SCORE_DIM.Sex,
-                        NAME_SCORE_DIM.Age
+                        NAME_SCORE_DIM.Age,
+                        NAME_SCORE_DIM.DirtyNumber
                     ]
                 };
                 option.series.push(series);
@@ -299,7 +303,7 @@ under the License.
                 transform: {
                     type: 'filter',
                     // print: true,
-                    config: { dimension: NAME_SCORE_DIM.Name, eq: 'Jobs Mat', parse: 'trim' }
+                    config: { dimension: NAME_SCORE_DIM.Name, eq: 'Jobs Mat', parser: 'trim' }
                 }
             });
             addCartesian({
@@ -314,7 +318,7 @@ under the License.
                 transform: {
                     type: 'filter',
                     // print: true,
-                    config: { dimension: NAME_SCORE_DIM.Date, lt: '2011-03', gte: '2011-02', parse: 'time' }
+                    config: { dimension: NAME_SCORE_DIM.Date, lt: '2011-03', gte: '2011-02', parser: 'time' }
                 }
             });
             addCartesian({
@@ -329,7 +333,7 @@ under the License.
                 transform: {
                     type: 'filter',
                     // print: true,
-                    config: { dimension: NAME_SCORE_DIM.Date, lte: '2011-03', gte: '2011-02-29', parse: 'time' }
+                    config: { dimension: NAME_SCORE_DIM.Date, lte: '2011-03', gte: '2011-02-29', parser: 'time' }
                 }
             });
             addCartesian({
@@ -344,7 +348,7 @@ under the License.
                 transform: {
                     type: 'filter',
                     // print: true,
-                    config: { dimension: NAME_SCORE_DIM.Name, reg: /\sXu$/, parse: 'trim' }
+                    config: { dimension: NAME_SCORE_DIM.Name, reg: /\sXu$/, parser: 'trim' }
                 }
             });
             addCartesian({
@@ -359,7 +363,7 @@ under the License.
                 transform: {
                     type: 'filter',
                     // print: true,
-                    config: { dimension: NAME_SCORE_DIM.Sex, ne: 'male', parse: 'trim' }
+                    config: { dimension: NAME_SCORE_DIM.Sex, ne: 'male', parser: 'trim' }
                 }
             });
             addCartesian({
@@ -377,7 +381,7 @@ under the License.
                     // print: true,
                     config: {
                         and: [
-                            { dimension: NAME_SCORE_DIM.Sex, eq: 'male', parse: 'trim' },
+                            { dimension: NAME_SCORE_DIM.Sex, eq: 'male', parser: 'trim' },
                             { dimension: NAME_SCORE_DIM.Score, '>': 300 }
                         ]
                     }
@@ -464,6 +468,30 @@ under the License.
             });
 
 
+            option.dataset.push({
+                id: 'j',
+                transform: {
+                    type: 'filter',
+                    // print: true,
+                    config: {
+                        or: [{
+                            dimension: NAME_SCORE_DIM.DirtyNumber,
+                            eq: 454
+                        }, {
+                            dimension: NAME_SCORE_DIM.DirtyNumber,
+                            eq: 232
+                        }]
+                    }
+                }
+            });
+            addCartesian({
+                series: {
+                    datasetId: 'j',
+                    encode: { label: [NAME_SCORE_DIM.DirtyNumber] }
+                },
+                xAxis: { name: 'Show only "Must Godden"' }
+            });
+
 
 
             var chart = testHelper.create(echarts, 'main_cartesian_parse_trim_time_reg', {
@@ -501,7 +529,7 @@ under the License.
             var leftStart = 50;
             var leftBase = leftStart;
             var topBase = 30;
-            var gridWidth = 100;
+            var gridWidth = 200;
             var gridHeight = 100;
             var gapWidth = 70;
             var gapHeight = 80;
@@ -537,7 +565,13 @@ under the License.
                 series.type = 'bar';
                 series.xAxisIndex = option.xAxis.length - 1;
                 series.yAxisIndex = option.yAxis.length - 1;
-                series.label = { show: true, position: 'top' };
+                series.label = {
+                    show: true,
+                    position: 'insideBottom',
+                    rotate: 90,
+                    align: 'left',
+                    verticalAlign: 'middle'
+                };
                 series.encode = {
                     x: NAME_SCORE_DIM.Date,
                     y: NAME_SCORE_DIM.Score,
@@ -547,7 +581,10 @@ under the License.
                         NAME_SCORE_DIM.Date,
                         NAME_SCORE_DIM.Score,
                         NAME_SCORE_DIM.Sex,
-                        NAME_SCORE_DIM.Age
+                        NAME_SCORE_DIM.Age,
+                        NAME_SCORE_DIM.DirtyNumber,
+                        NAME_SCORE_DIM.Numeric,
+                        NAME_SCORE_DIM.HasEmpty
                     ]
                 };
                 option.series.push(series);
@@ -564,9 +601,10 @@ under the License.
             });
             addCartesian({
                 series: {
+                    encode: { label: NAME_SCORE_DIM.Score },
                     datasetId: 'a'
                 },
-                xAxis: { name: 'Show all eight\norder by Score asc' }
+                xAxis: { name: 'Show all eight bars\norder by Score asc' }
             });
 
             option.dataset.push({
@@ -582,7 +620,7 @@ under the License.
                     datasetId: 'b',
                     encode: { label: NAME_SCORE_DIM.Age }
                 },
-                xAxis: { name: 'Show all eight\norder by Age desc' }
+                xAxis: { name: 'Show all eight bars\norder by Age desc' }
             });
 
             option.dataset.push({
@@ -599,9 +637,9 @@ under the License.
             addCartesian({
                 series: {
                     datasetId: 'c',
-                    encode: { label: NAME_SCORE_DIM.Sex }
+                    encode: { label: [NAME_SCORE_DIM.Sex, NAME_SCORE_DIM.Score] }
                 },
-                xAxis: { name: 'Show all eight\nSex asc, Score desc' }
+                xAxis: { name: 'Show all eight bars\nSex asc (all female left)\nScore desc in each Sex' }
             });
 
             option.dataset.push({
@@ -610,15 +648,16 @@ under the License.
                     type: 'sort',
                     // print: true,
                     config: [
-                        { dimension: NAME_SCORE_DIM.Date, order: 'asc', parse: 'time' }
+                        { dimension: NAME_SCORE_DIM.Date, order: 'asc', parser: 'time' }
                     ]
                 }
             });
             addCartesian({
                 series: {
+                    encode: { label: NAME_SCORE_DIM.Date },
                     datasetId: 'd'
                 },
-                xAxis: { name: 'Show all eight\nDate asc' }
+                xAxis: { name: 'Show all eight bars\nDate asc' }
             });
 
 
@@ -626,26 +665,98 @@ under the License.
                 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: {
+                    encode: { label: [NAME_SCORE_DIM.Age, NAME_SCORE_DIM.Score] },
                     datasetId: 'e'
                 },
-                xAxis: { name: 'Show three ponits\nFilter by Age 20-40\nOrder by Score' }
+                xAxis: { name: 'Show three bars\nFilter by Age 20-40\nOrder by Score asc' }
+            });
+
+
+            option.dataset.push({
+                id: 'f',
+                transform: {
+                    type: 'sort',
+                    config: [
+                        { dimension: NAME_SCORE_DIM.DirtyNumber, order: 'desc', parser: 'number' }
+                    ]
+                }
+            });
+            addCartesian({
+                series: {
+                    encode: { label: NAME_SCORE_DIM.DirtyNumber },
+                    datasetId: 'f'
+                },
+                xAxis: { name: 'Show all eight bars\nOrder by DirtyNumber desc' }
+            });
+
+
+            option.dataset.push({
+                id: 'g',
+                transform: {
+                    type: 'sort',
+                    config: [
+                        { dimension: NAME_SCORE_DIM.Numeric, order: 'asc' }
+                    ]
+                }
+            });
+            addCartesian({
+                series: {
+                    encode: { label: NAME_SCORE_DIM.Numeric },
+                    datasetId: 'g'
+                },
+                xAxis: { name: 'Show all eight bars\nOrder by Numeric asc\nOnly one empty at right' }
+            });
+
+
+            option.dataset.push({
+                id: 'h',
+                transform: {
+                    type: 'sort',
+                    config: [
+                        { dimension: NAME_SCORE_DIM.HasEmpty, order: 'desc' }
+                    ]
+                }
+            });
+            addCartesian({
+                series: {
+                    encode: { label: NAME_SCORE_DIM.HasEmpty },
+                    datasetId: 'h'
+                },
+                xAxis: { name: 'Show all eight bars\nOrder by HasEmpty desc\nempty at right' }
+            });
+
+
+            option.dataset.push({
+                id: 'i',
+                transform: {
+                    type: 'sort',
+                    config: [
+                        { dimension: NAME_SCORE_DIM.HasEmpty, order: 'desc', incomparable: 'max' }
+                    ]
+                }
+            });
+            addCartesian({
+                series: {
+                    encode: { label: NAME_SCORE_DIM.HasEmpty },
+                    datasetId: 'i'
+                },
+                xAxis: { name: 'Show all eight bars\nOrder by HasEmpty desc\nempty at left' }
             });
 
 
             var chart = testHelper.create(echarts, 'main_cartesian_sort', {
                 title: [
-                    'Check each cartesians.',
-                    'The expectationa are below each cartesian.'
+                    'Test sort transform. Check each cartesians.',
+                    'The expectationa are below each cartesian.',
+                    'Ordered dimension is on **bar label **'
                 ],
                 width: chartWidth,
                 height: 600,
diff --git a/test/ut/spec/data/dataValueHelper.test.js b/test/ut/spec/data/dataValueHelper.test.js
index 8eb7309..cf0c627 100644
--- a/test/ut/spec/data/dataValueHelper.test.js
+++ b/test/ut/spec/data/dataValueHelper.test.js
@@ -20,189 +20,374 @@
 
 const dataValueHelper = require('../../../../lib/data/helper/dataValueHelper');
 
-describe('data/helper/dataValueHelper', function () {
+const NO_SUCH_CASE = 'NO_SUCH_CASE';
+
+// Tags for relational comparison cases.
+// LT: less than, GT: greater than, INCMPR: incomparable
+const TAG = {
+    ONE_OR_TWO_NUMBER_L_LT_R: 'ONE_OR_TWO_NUMBER_L_LT_R',
+    ONE_OR_TWO_NUMBER_L_GT_R: 'ONE_OR_TWO_NUMBER_L_GT_R',
+    TWO_STRING_L_LT_R: 'TWO_STRING_L_LT_R',
+    TWO_STRING_L_GT_R: 'TWO_STRING_L_GT_R',
+    TWO_STRING_ONLY_NUMERIC_EQ: 'TWO_STRING_ONLY_NUMERIC_EQ',
+    STRICT_EQ: 'STRICT_EQ',
+    ONE_NUMBER_NUMERIC_EQ: 'ONE_NUMBER_NUMERIC_EQ',
+    BOTH_INCMPR_NOT_EQ: 'BOTH_INCMPR_NOT_EQ',
+    ONLY_L_INCMPR: 'ONLY_L_INCMPR',
+    ONLY_R_INCMPR: 'ONLY_R_INCMPR'
+};
+const tagRevertPairs = [
+    ['ONE_OR_TWO_NUMBER_L_LT_R', 'ONE_OR_TWO_NUMBER_L_GT_R'],
+    ['TWO_STRING_L_LT_R', 'TWO_STRING_L_GT_R'],
+    ['TWO_STRING_ONLY_NUMERIC_EQ', 'TWO_STRING_ONLY_NUMERIC_EQ'],
+    ['STRICT_EQ', 'STRICT_EQ'],
+    ['ONE_NUMBER_NUMERIC_EQ', 'ONE_NUMBER_NUMERIC_EQ'],
+    ['BOTH_INCMPR_NOT_EQ', 'BOTH_INCMPR_NOT_EQ'],
+    ['ONLY_L_INCMPR', 'ONLY_R_INCMPR']
+];
+
+const filterResultMap = {
+    ONE_OR_TWO_NUMBER_L_LT_R: {
+        lt: true,
+        lte: true,
+        gt: false,
+        gte: false,
+        eq: false,
+        ne: true
+    },
+    ONE_OR_TWO_NUMBER_L_GT_R: {
+        lt: false,
+        lte: false,
+        gt: true,
+        gte: true,
+        eq: false,
+        ne: true
+    },
+    TWO_STRING_L_LT_R: {
+        lt: NO_SUCH_CASE,
+        lte: NO_SUCH_CASE,
+        gt: NO_SUCH_CASE,
+        gte: NO_SUCH_CASE,
+        eq: false,
+        ne: true
+    },
+    TWO_STRING_L_GT_R: {
+        lt: NO_SUCH_CASE,
+        lte: NO_SUCH_CASE,
+        gt: NO_SUCH_CASE,
+        gte: NO_SUCH_CASE,
+        eq: false,
+        ne: true
+    },
+    TWO_STRING_ONLY_NUMERIC_EQ: {
+        lt: NO_SUCH_CASE,
+        lte: NO_SUCH_CASE,
+        gt: NO_SUCH_CASE,
+        gte: NO_SUCH_CASE,
+        eq: false,
+        ne: true
+    },
+    STRICT_EQ: {
+        lt: false,
+        lte: true,
+        gt: false,
+        gte: true,
+        eq: true,
+        ne: false
+    },
+    ONE_NUMBER_NUMERIC_EQ: {
+        lt: false,
+        lte: true,
+        gt: false,
+        gte: true,
+        eq: true,
+        ne: false
+    },
+    BOTH_INCMPR_NOT_EQ: {
+        lt: false,
+        lte: false,
+        gt: false,
+        gte: false,
+        eq: false,
+        ne: true
+    },
+    ONLY_L_INCMPR: {
+        lt: false,
+        lte: false,
+        gt: false,
+        gte: false,
+        eq: false,
+        ne: true
+    },
+    ONLY_R_INCMPR: {
+        lt: false,
+        lte: false,
+        gt: false,
+        gte: false,
+        eq: false,
+        ne: true
+    }
+};
+
+const sortResultMap = {
+    ONE_OR_TWO_NUMBER_L_LT_R: {
+        asc_incmprmin: -1,
+        asc_incmprmax: -1,
+        desc_incmprmin: 1,
+        desc_incmprmax: 1
+    },
+    ONE_OR_TWO_NUMBER_L_GT_R: {
+        asc_incmprmin: 1,
+        asc_incmprmax: 1,
+        desc_incmprmin: -1,
+        desc_incmprmax: -1
+    },
+    TWO_STRING_L_LT_R: {
+        asc_incmprmin: -1,
+        asc_incmprmax: -1,
+        desc_incmprmin: 1,
+        desc_incmprmax: 1
+    },
+    TWO_STRING_L_GT_R: {
+        asc_incmprmin: 1,
+        asc_incmprmax: 1,
+        desc_incmprmin: -1,
+        desc_incmprmax: -1
+    },
+    TWO_STRING_ONLY_NUMERIC_EQ: {
+        asc_incmprmin: 0,
+        asc_incmprmax: 0,
+        desc_incmprmin: 0,
+        desc_incmprmax: 0
+    },
+    STRICT_EQ: {
+        asc_incmprmin: 0,
+        asc_incmprmax: 0,
+        desc_incmprmin: 0,
+        desc_incmprmax: 0
+    },
+    ONE_NUMBER_NUMERIC_EQ: {
+        asc_incmprmin: 0,
+        asc_incmprmax: 0,
+        desc_incmprmin: 0,
+        desc_incmprmax: 0
+    },
+    BOTH_INCMPR_NOT_EQ: {
+        asc_incmprmin: 0,
+        asc_incmprmax: 0,
+        desc_incmprmin: 0,
+        desc_incmprmax: 0
+    },
+    ONLY_L_INCMPR: {
+        asc_incmprmin: -1,
+        asc_incmprmax: 1,
+        desc_incmprmin: 1,
+        desc_incmprmax: -1
+    },
+    ONLY_R_INCMPR: {
+        asc_incmprmin: 1,
+        asc_incmprmax: -1,
+        desc_incmprmin: -1,
+        desc_incmprmax: 1
+    }
+};
+
+/**
+ * @param {(lval: unknown, rval: unknown, caseTag: TAG) => void} evalFn
+ */
+function eachRelationalComparisonCase(evalFn) {
+
+    const FULL_WIDTH_SPACE = String.fromCharCode(12288);
+
+    const testerMap = {
+        notEqualAndHasOrder: function () {
+            expectDual(123, 555, TAG.ONE_OR_TWO_NUMBER_L_LT_R);
+            expectDual(-123, -555, TAG.ONE_OR_TWO_NUMBER_L_GT_R);
+            expectDual(-123, 123, TAG.ONE_OR_TWO_NUMBER_L_LT_R);
 
-    describe('relational_comparison', function () {
+            expectDual(Infinity, 123, TAG.ONE_OR_TWO_NUMBER_L_GT_R);
+            expectDual(-Infinity, -123, TAG.ONE_OR_TWO_NUMBER_L_LT_R);
+            expectDual('Infinity', 123, TAG.ONE_OR_TWO_NUMBER_L_GT_R);
+            expectDual('-Infinity', 123, TAG.ONE_OR_TWO_NUMBER_L_LT_R);
+            expectDual(123, '555', TAG.ONE_OR_TWO_NUMBER_L_LT_R);
+            expectDual(555, '555.6', TAG.ONE_OR_TWO_NUMBER_L_LT_R);
+            expectDual('-555', -555.6, TAG.ONE_OR_TWO_NUMBER_L_GT_R);
+            expectDual(123, ' 555 ', TAG.ONE_OR_TWO_NUMBER_L_LT_R);
+            expectDual(' -555 ', 123, TAG.ONE_OR_TWO_NUMBER_L_LT_R);
+            expectDual(123, ' \r \n 555 \t ' + FULL_WIDTH_SPACE, TAG.ONE_OR_TWO_NUMBER_L_LT_R);
+        },
 
-        function expectDual(evalFn, lval, rval, resultLR, resultRL) {
-            expect(evalFn(lval, rval)).toEqual(resultLR);
-            expect(evalFn(rval, lval)).toEqual(resultRL);
+        notEqualAndNoOrder: function () {
+            const makeDate = () => new Date(2012, 5, 12);
+            const makeFn = () => function () {};
+
+            expectDual(NaN, NaN, TAG.BOTH_INCMPR_NOT_EQ);
+            expectDual(NaN, -NaN, TAG.BOTH_INCMPR_NOT_EQ);
+            expectDual(NaN, 0, TAG.ONLY_L_INCMPR);
+            expectDual(NaN, 2, TAG.ONLY_L_INCMPR);
+            expectDual('NaN', NaN, TAG.BOTH_INCMPR_NOT_EQ);
+            expectDual('NaN', 0, TAG.ONLY_L_INCMPR);
+            expectDual('NaN', 2, TAG.ONLY_L_INCMPR);
+            expectDual('-NaN', -NaN, TAG.BOTH_INCMPR_NOT_EQ);
+            expectDual('-NaN', 0, TAG.ONLY_L_INCMPR);
+            expectDual('-NaN', 2, TAG.ONLY_L_INCMPR);
+            expectDual(true, 0, TAG.ONLY_L_INCMPR);
+            expectDual(false, 1, TAG.ONLY_L_INCMPR);
+            expectDual('true', 0, TAG.ONLY_L_INCMPR);
+            expectDual('false', 1, TAG.ONLY_L_INCMPR);
+            expectDual(undefined, 2, TAG.ONLY_L_INCMPR);
+            expectDual(undefined, 0, TAG.ONLY_L_INCMPR);
+            expectDual(null, 2, TAG.ONLY_L_INCMPR);
+            expectDual(null, 0, TAG.ONLY_L_INCMPR);
+            expectDual(makeDate(), 0, TAG.ONLY_L_INCMPR);
+            expectDual(makeDate(), makeDate(), TAG.BOTH_INCMPR_NOT_EQ);
+            expectDual(makeDate(), +makeDate(), TAG.ONLY_L_INCMPR);
+            expectDual([], 1, TAG.ONLY_L_INCMPR);
+            expectDual([], 0, TAG.ONLY_L_INCMPR);
+            expectDual({}, 1, TAG.ONLY_L_INCMPR);
+            expectDual([], '0', TAG.ONLY_L_INCMPR);
+            expectDual({}, '1', TAG.ONLY_L_INCMPR);
+            expectDual({}, 0, TAG.ONLY_L_INCMPR);
+            expectDual({}, '1', TAG.ONLY_L_INCMPR);
+            expectDual({}, '0', TAG.ONLY_L_INCMPR);
+            expectDual(/1/, 0, TAG.ONLY_L_INCMPR);
+            expectDual(/0/, 0, TAG.ONLY_L_INCMPR);
+            expectDual('555a', 123, TAG.ONLY_L_INCMPR);
+            expectDual('abc', 123, TAG.ONLY_L_INCMPR);
+            expectDual('abc', '123', TAG.ONLY_L_INCMPR);
+            expectDual('abc', 'abcde', TAG.TWO_STRING_L_LT_R);
+            expectDual('abc', 'abc', TAG.STRICT_EQ);
+            expectDual('2', '12', TAG.TWO_STRING_L_LT_R); // '2' > '12' in JS but should not happen here.
+            expectDual(' ', '', TAG.TWO_STRING_L_GT_R);
+            expectDual(0.5, '0. 5', TAG.ONLY_R_INCMPR);
+            expectDual('0.5', '0. 5', TAG.ONLY_R_INCMPR);
+            expectDual('- 5', -5, TAG.ONLY_L_INCMPR);
+            expectDual('-123.5', ' -123.5 ', TAG.TWO_STRING_ONLY_NUMERIC_EQ);
+            expectDual('0x11', 17, TAG.ONLY_L_INCMPR); // not 17 in int16.
+            expectDual('0x11', 0, TAG.ONLY_L_INCMPR);
+            expectDual('0x0', 0, TAG.ONLY_L_INCMPR);
+            expectDual('0. 5', 0.5, TAG.ONLY_L_INCMPR);
+            expectDual('0 .5', 0.5, TAG.ONLY_L_INCMPR);
+            expectDual('', 2, TAG.ONLY_L_INCMPR);
+            expectDual('', 0, TAG.ONLY_L_INCMPR);
+            expectDual(' ', 2, TAG.ONLY_L_INCMPR);
+            expectDual(' ', 0, TAG.ONLY_L_INCMPR);
+            expectDual(' \n', '\n', TAG.TWO_STRING_L_GT_R);
+            expectDual('\n', 0, TAG.ONLY_L_INCMPR);
+            expectDual('\n', 2, TAG.ONLY_L_INCMPR);
+            expectDual({}, {}, TAG.BOTH_INCMPR_NOT_EQ);
+            expectDual({}, [], TAG.BOTH_INCMPR_NOT_EQ);
+            expectDual(makeFn(), makeFn(), TAG.BOTH_INCMPR_NOT_EQ);
+            expectDual(makeFn(), 0, TAG.ONLY_L_INCMPR);
+            expectDual(makeFn(), 1, TAG.ONLY_L_INCMPR);
+            expectDual(makeFn(), makeFn().toString(), TAG.BOTH_INCMPR_NOT_EQ);
+        },
+
+        equalNumeric: function () {
+            expectDual(123, 123, TAG.STRICT_EQ);
+            expectDual(1e3, 1000, TAG.STRICT_EQ);
+            expectDual(-1e3, -1000, TAG.STRICT_EQ);
+            expectDual('1e3', 1000, TAG.ONE_NUMBER_NUMERIC_EQ);
+            expectDual('-1e3', -1000, TAG.ONE_NUMBER_NUMERIC_EQ);
+            expectDual(123, '123', TAG.ONE_NUMBER_NUMERIC_EQ);
+            expectDual(123, ' 123 ', TAG.ONE_NUMBER_NUMERIC_EQ);
+            expectDual(123.5, ' \n \r 123.5 \t ', TAG.ONE_NUMBER_NUMERIC_EQ);
+            expectDual(123.5, 123.5 + FULL_WIDTH_SPACE, TAG.ONE_NUMBER_NUMERIC_EQ);
+            expectDual(' -123.5 ', -123.5, TAG.ONE_NUMBER_NUMERIC_EQ);
+            expectDual('011', 11, TAG.ONE_NUMBER_NUMERIC_EQ); // not 9 in int8.
+        },
+
+        equalOtherTypes: function () {
+            const emptyObj = {};
+            const emptyArr = [];
+            const date = new Date(2012, 5, 12);
+            const fn = function () {};
+            expectDual(emptyObj, emptyObj, TAG.STRICT_EQ);
+            expectDual(emptyArr, emptyArr, TAG.STRICT_EQ);
+            expectDual(date, date, TAG.STRICT_EQ);
+            expectDual(fn, fn, TAG.STRICT_EQ);
         }
+    };
+
+    function expectDual(lval, rval, caseTag) {
+        validateCaseTag(caseTag);
+        evalFn(lval, rval, caseTag);
+
+        const revertedCaseTag = findRevertTag(caseTag);
+        validateCaseTag(revertedCaseTag);
+        evalFn(rval, lval, revertedCaseTag);
+    }
+
+    function validateCaseTag(caseTag) {
+        expect(TAG.hasOwnProperty(caseTag)).toEqual(true);
+    }
 
-        const testerMap = {
-
-            notEqualAndHasOrder: function (evalFn, op) {
-                let asc;
-                let desc;
-                if (op === 'lt' || op === 'lte') {
-                    asc = true;
-                    desc = false;
-                }
-                else if (op === 'gt' || op === 'gte') {
-                    asc = false;
-                    desc = true;
-                }
-                else if (op === 'eq') {
-                    asc = desc = false;
-                }
-                else if (op === 'ne') {
-                    asc = desc = true;
-                }
-
-                expectDual(evalFn, 123, 555, asc, desc);
-                expectDual(evalFn, -123, -555, desc, asc);
-                expectDual(evalFn, -123, 123, asc, desc);
-
-                expectDual(evalFn, Infinity, 123, desc, asc);
-                expectDual(evalFn, -Infinity, -123, asc, desc);
-                expectDual(evalFn, 'Infinity', 123, desc, asc);
-                expectDual(evalFn, '-Infinity', 123, asc, desc);
-                expectDual(evalFn, 123, '555', asc, desc);
-                expectDual(evalFn, 555, '555.6', asc, desc);
-                expectDual(evalFn, '-555', -555.6, desc, asc);
-                expectDual(evalFn, 123, ' 555 ', asc, desc);
-                expectDual(evalFn, ' -555 ', 123, asc, desc);
-                expectDual(evalFn, 123, ' \r \n 555 \t ' + String.fromCharCode(12288), asc, desc);
-            },
-
-            notEqualAndNoOrder: function (evalFn, op) {
-                const result = op === 'ne';
-                const makeDate = () => new Date(2012, 5, 12);
-                const makeFn = () => function () {};
-
-                expectDual(evalFn, NaN, NaN, result, result);
-                expectDual(evalFn, NaN, -NaN, result, result);
-                expectDual(evalFn, NaN, 0, result, result);
-                expectDual(evalFn, NaN, 2, result, result);
-                expectDual(evalFn, 'NaN', NaN, result, result);
-                expectDual(evalFn, 'NaN', 0, result, result);
-                expectDual(evalFn, 'NaN', 2, result, result);
-                expectDual(evalFn, '-NaN', -NaN, result, result);
-                expectDual(evalFn, '-NaN', 0, result, result);
-                expectDual(evalFn, '-NaN', 2, result, result);
-                expectDual(evalFn, true, 0, result, result);
-                expectDual(evalFn, false, 1, result, result);
-                expectDual(evalFn, 'true', 0, result, result);
-                expectDual(evalFn, 'false', 1, result, result);
-                expectDual(evalFn, undefined, 2, result, result);
-                expectDual(evalFn, undefined, 0, result, result);
-                expectDual(evalFn, null, 2, result, result);
-                expectDual(evalFn, null, 0, result, result);
-                expectDual(evalFn, makeDate(), 0, result, result);
-                expectDual(evalFn, makeDate(), makeDate(), result, result);
-                expectDual(evalFn, makeDate(), +makeDate(), result, result);
-                expectDual(evalFn, [], 1, result, result);
-                expectDual(evalFn, [], 0, result, result);
-                expectDual(evalFn, {}, 1, result, result);
-                expectDual(evalFn, [], '0', result, result);
-                expectDual(evalFn, {}, '1', result, result);
-                expectDual(evalFn, {}, 0, result, result);
-                expectDual(evalFn, {}, '1', result, result);
-                expectDual(evalFn, {}, '0', result, result);
-                expectDual(evalFn, /1/, 0, result, result);
-                expectDual(evalFn, /0/, 0, result, result);
-                expectDual(evalFn, '555a', 123, result, result);
-                expectDual(evalFn, '2', '12', result, result); // '2' > '12' in JS but should not happen here.
-                expectDual(evalFn, ' ', '', result, result);
-                expectDual(evalFn, 0.5, '0. 5', result, result);
-                expectDual(evalFn, '0.5', '0. 5', result, result);
-                expectDual(evalFn, '- 5', -5, result, result);
-                expectDual(evalFn, '-123.5', ' -123.5 ', result, result);
-                expectDual(evalFn, '0x11', 17, result, result); // not 17 in int16.
-                expectDual(evalFn, '0x11', 0, result, result);
-                expectDual(evalFn, '0x0', 0, result, result);
-                expectDual(evalFn, '0. 5', 0.5, result, result);
-                expectDual(evalFn, '0 .5', 0.5, result, result);
-                expectDual(evalFn, '', 2, result, result);
-                expectDual(evalFn, '', 0, result, result);
-                expectDual(evalFn, ' ', 2, result, result);
-                expectDual(evalFn, ' ', 0, result, result);
-                expectDual(evalFn, ' \n', '\n', result, result);
-                expectDual(evalFn, '\n', 0, result, result);
-                expectDual(evalFn, '\n', 2, result, result);
-                expectDual(evalFn, {}, {}, result, result);
-                expectDual(evalFn, {}, [], result, result);
-                expectDual(evalFn, makeFn(), makeFn(), result, result);
-                expectDual(evalFn, makeFn(), 0, result, result);
-                expectDual(evalFn, makeFn(), 1, result, result);
-                expectDual(evalFn, makeFn(), makeFn().toString(), result, result);
-            },
-
-            numericEqual: function (evalFn, op) {
-                const result = op === 'eq' || op === 'lte' || op === 'gte';
-
-                expectDual(evalFn, 123, 123, result, result);
-                expectDual(evalFn, 1e3, 1000, result, result);
-                expectDual(evalFn, -1e3, -1000, result, result);
-                expectDual(evalFn, '1e3', 1000, result, result);
-                expectDual(evalFn, '-1e3', -1000, result, result);
-                expectDual(evalFn, 123, '123', result, result);
-                expectDual(evalFn, 123, ' 123 ', result, result);
-                expectDual(evalFn, 123.5, ' \n \r 123.5 \t ', result, result);
-                expectDual(evalFn, 123.5, 123.5 + String.fromCharCode(12288), result, result);
-                expectDual(evalFn, ' -123.5 ', -123.5, result, result);
-                expectDual(evalFn, '011', 11, result, result); // not 9 in int8.
-            },
-
-            otherTypesEqual: function (evalFn, op) {
-                const result = op === 'eq';
-
-                const emptyObj = {};
-                const emptyArr = [];
-                const date = new Date(2012, 5, 12);
-                const fn = function () {};
-                expectDual(evalFn, emptyObj, emptyObj, result, result);
-                expectDual(evalFn, emptyArr, emptyArr, result, result);
-                expectDual(evalFn, date, date, result, result);
-                expectDual(evalFn, fn, fn, result, result);
+    function findRevertTag(caseTag) {
+        for (let i = 0; i < tagRevertPairs.length; i++) {
+            const item = tagRevertPairs[i];
+            if (item[0] === caseTag) {
+                return item[1];
             }
-        };
-
-        function doTest(op) {
-            expect(['lt', 'lte', 'gt', 'gte', 'eq', 'ne'].indexOf(op) >= 0).toEqual(true);
-            it(op, () => {
-                const comparator0 = dataValueHelper.createRelationalComparator(op);
-                Object.keys(testerMap).forEach(name => {
-                    const evalFn = (lVal, rVal) => {
-                        return comparator0.evaluate(lVal, rVal);
-                    };
-                    testerMap[name](evalFn, op);
-                });
+            else if (item[1] === caseTag) {
+                return item[0];
+            }
+        }
+    }
+
+    Object.keys(testerMap).forEach(name => testerMap[name]());
+}
+
+
+describe('data/helper/dataValueHelper', function () {
 
-                Object.keys(testerMap).forEach(name => {
-                    const evalFn = (lVal, rVal) => {
-                        const comparator1 = dataValueHelper.createRelationalComparator(op, true, rVal);
-                        return comparator1.evaluate(lVal);
-                    };
-                    testerMap[name](evalFn, op);
+    describe('filter_relational_comparison', function () {
+        function testFilterComparator(op) {
+            it(op + '_filter_comparator', () => {
+                eachRelationalComparisonCase((lval, rval, caseTag) => {
+                    expect(filterResultMap.hasOwnProperty[caseTag]);
+                    expect(filterResultMap[caseTag].hasOwnProperty[op]);
+                    const expectedResult = filterResultMap[caseTag][op];
+
+                    if ((op === 'lt' || op === 'lte' || op === 'gt' || op === 'gte')
+                        && typeof rval !== 'number'
+                    ) {
+                        expect(() => {
+                            dataValueHelper.createFilterComparator(op, rval);
+                        }).toThrow();
+                    }
+                    else {
+                        const comparator = dataValueHelper.createFilterComparator(op, rval);
+                        expect(comparator.evaluate(lval, rval)).toEqual(expectedResult);
+                    }
                 });
             });
         }
+        testFilterComparator('lt');
+        testFilterComparator('lte');
+        testFilterComparator('gt');
+        testFilterComparator('gte');
+        testFilterComparator('eq');
+        testFilterComparator('ne');
+    });
 
-        doTest('lt');
-        doTest('lte');
-        doTest('gt');
-        doTest('gte');
-        doTest('eq');
-        doTest('ne');
-
-        it('isRelationalOperator', function () {
-            expect(dataValueHelper.isRelationalOperator('lt')).toEqual(true);
-            expect(dataValueHelper.isRelationalOperator('lte')).toEqual(true);
-            expect(dataValueHelper.isRelationalOperator('gt')).toEqual(true);
-            expect(dataValueHelper.isRelationalOperator('gte')).toEqual(true);
-            expect(dataValueHelper.isRelationalOperator('eq')).toEqual(true);
-            expect(dataValueHelper.isRelationalOperator('ne')).toEqual(true);
-            expect(dataValueHelper.isRelationalOperator('')).toEqual(false);
-
-            expect(dataValueHelper.isRelationalOperator(null)).toEqual(false);
-            expect(dataValueHelper.isRelationalOperator(undefined)).toEqual(false);
-            expect(dataValueHelper.isRelationalOperator(NaN)).toEqual(false);
-            expect(dataValueHelper.isRelationalOperator('neq')).toEqual(false);
-            expect(dataValueHelper.isRelationalOperator('ge')).toEqual(false);
-            expect(dataValueHelper.isRelationalOperator('le')).toEqual(false);
-        });
-
+    describe('sort_relational_comparison', function () {
+        function testSortComparator(order, incomparable) {
+            const key = order + '_incmpr' + incomparable;
+            const SortOrderComparator = dataValueHelper.SortOrderComparator;
+            const sortOrderComparator = new SortOrderComparator(order, incomparable);
+            it(key + '_sort_comparator', () => {
+                eachRelationalComparisonCase((lval, rval, caseTag) => {
+                    expect(sortResultMap.hasOwnProperty[caseTag]);
+                    expect(sortResultMap[caseTag].hasOwnProperty[key]);
+                    const expectedResult = sortResultMap[caseTag][key];
+                    expect(sortOrderComparator.evaluate(lval, rval)).toEqual(expectedResult);
+                });
+            });
+        }
+        testSortComparator('asc', 'min');
+        testSortComparator('asc', 'max');
+        testSortComparator('desc', 'min');
+        testSortComparator('desc', 'max');
     });
 
 });


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