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/06/02 17:54:31 UTC

[incubator-echarts] 01/10: feat: custom series compat.

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

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

commit d0e5f43998996909fcc6d4c0ddc7c886b2796386
Author: 100pah <su...@gmail.com>
AuthorDate: Wed Apr 29 01:57:05 2020 +0800

    feat: custom series compat.
---
 src/chart/custom.ts                                | 1252 +++++++++++++++-----
 src/coord/CoordinateSystem.ts                      |    3 +
 src/echarts.ts                                     |    4 +
 src/model/Component.ts                             |    5 +
 src/model/mixin/itemStyle.ts                       |    2 +-
 src/util/graphic.ts                                |   39 +-
 src/util/styleCompat.ts                            |  256 ++++
 src/util/types.ts                                  |    2 +
 ...-d3.html => circle-packing-with-d3.compat.html} |    0
 test/circle-packing-with-d3.html                   |   50 +-
 test/custom-feature.html                           |    1 +
 test/custom-text-content.html                      | 1193 +++++++++++++++++++
 test/hoverStyle.html                               |    3 +-
 13 files changed, 2488 insertions(+), 322 deletions(-)

diff --git a/src/chart/custom.ts b/src/chart/custom.ts
index 3b801ff..3ed15b9 100644
--- a/src/chart/custom.ts
+++ b/src/chart/custom.ts
@@ -17,7 +17,6 @@
 * under the License.
 */
 
-// @ts-nocheck
 
 import {__DEV__} from '../config';
 import * as zrUtil from 'zrender/src/core/util';
@@ -30,24 +29,261 @@ import SeriesModel from '../model/Series';
 import Model from '../model/Model';
 import ChartView from '../view/Chart';
 import {createClipPath} from './helper/createClipPathFromCoordSys';
-import {EventQueryItem, ECEvent} from '../util/types';
-import Element from 'zrender/src/Element';
-
+import {
+    EventQueryItem, ECEvent, SeriesOption, SeriesOnCartesianOptionMixin,
+    SeriesOnPolarOptionMixin, SeriesOnSingleOptionMixin, SeriesOnGeoOptionMixin,
+    SeriesOnCalendarOptionMixin, ItemStyleOption, SeriesEncodeOptionMixin,
+    SeriesTooltipOption,
+    DimensionLoose,
+    ParsedValue,
+    Dictionary,
+    CallbackDataParams,
+    Payload,
+    StageHandlerProgressParams,
+    LabelOption,
+    ViewRootGroup,
+    OptionDataValue,
+    ZRStyleProps,
+    DisplayState,
+    ECElement,
+    DisplayStateNonNormal
+} from '../util/types';
+import Element, { ElementProps, ElementTextConfig } from 'zrender/src/Element';
 import prepareCartesian2d from '../coord/cartesian/prepareCustom';
 import prepareGeo from '../coord/geo/prepareCustom';
 import prepareSingleAxis from '../coord/single/prepareCustom';
 import preparePolar from '../coord/polar/prepareCustom';
 import prepareCalendar from '../coord/calendar/prepareCustom';
+import ComponentModel from '../model/Component';
+import List, { DefaultDataVisual } from '../data/List';
+import GlobalModel from '../model/Global';
+import { makeInner } from '../util/model';
+import ExtensionAPI from '../ExtensionAPI';
+import Displayable from 'zrender/src/graphic/Displayable';
+import Axis2D from '../coord/cartesian/Axis2D';
+import { RectLike } from 'zrender/src/core/BoundingRect';
+import { PathProps } from 'zrender/src/graphic/Path';
+import { ImageStyleProps } from 'zrender/src/graphic/Image';
+import { CoordinateSystem } from '../coord/CoordinateSystem';
+import { TextStyleProps } from 'zrender/src/graphic/Text';
+import {
+    convertToEC4StyleForCustomSerise,
+    isEC4CompatibleStyle,
+    convertFromEC4CompatibleStyle,
+    LegacyStyleProps,
+    warnDeprecated
+} from '../util/styleCompat';
+import Transformable from 'zrender/src/core/Transformable';
+import { ItemStyleProps } from '../model/mixin/itemStyle';
+
+
+const inner = makeInner<{
+    info: CustomExtraElementInfo;
+    customPathData: string;
+    customGraphicType: string;
+    customImagePath: CustomImageOption['style']['image'];
+    customText: string;
+    txConZ2Set: number;
+}, Element>();
+
+type CustomExtraElementInfo = Dictionary<unknown>;
+type TransformPropsX = 'x' | 'scaleX' | 'originX';
+type TransformPropsY = 'y' | 'scaleY' | 'originY';
+type TransformProps = TransformPropsX | TransformPropsY | 'rotation';
+
+
+interface CustomBaseElementOption extends Partial<Pick<
+    Element, TransformProps | 'silent' | 'ignore' | 'textConfig'
+>> {
+    // element type, mandatory.
+    type: string;
+    id?: string;
+    // For animation diff.
+    name?: string;
+    info?: CustomExtraElementInfo;
+    // `false` means remove the textContent.
+    textContent?: CustomTextOption | false;
+};
+interface CustomDisplayableOption extends CustomBaseElementOption, Partial<Pick<
+    Displayable, 'zlevel' | 'z' | 'z2' | 'invisible'
+>> {
+    style?: ZRStyleProps;
+    // `false` means remove emphasis trigger.
+    styleEmphasis?: ZRStyleProps | false;
+    emphasis?: CustomDisplayableOptionOnState;
+}
+interface CustomDisplayableOptionOnState extends Partial<Pick<
+    Displayable, TransformProps | 'textConfig' | 'z2'
+>> {
+    // `false` means remove emphasis trigger.
+    style?: ZRStyleProps | false;
+}
+interface CustomGroupOption extends CustomBaseElementOption {
+    type: 'group';
+    width?: number;
+    height?: number;
+    diffChildrenByName?: boolean;
+    children: CustomElementOption[];
+    $mergeChildren: false | 'byName' | 'byIndex';
+}
+interface CustomZRPathOption extends CustomDisplayableOption, Pick<PathProps, 'shape'> {
+}
+interface CustomSVGPathOption extends CustomDisplayableOption {
+    type: 'path';
+    shape?: {
+        // SVG Path, like 'M0,0 L0,-20 L70,-1 L70,0 Z'
+        pathData?: string;
+        // "d" is the alias of `pathData` follows the SVG convention.
+        d?: string;
+        layout?: 'center' | 'cover';
+        x?: number;
+        y?: number;
+        width?: number;
+        height?: number;
+    };
+}
+interface CustomImageOption extends CustomDisplayableOption {
+    type: 'image';
+    style?: ImageStyleProps;
+    emphasis?: CustomImageOptionOnState;
+}
+interface CustomImageOptionOnState extends CustomDisplayableOptionOnState {
+    style?: ImageStyleProps;
+}
+interface CustomTextOption extends CustomDisplayableOption {
+    type: 'text';
+}
+type CustomElementOption = CustomZRPathOption | CustomSVGPathOption | CustomImageOption | CustomTextOption;
+type CustomElementOptionOnState = CustomDisplayableOptionOnState | CustomImageOptionOnState;
+type StyleOption = ZRStyleProps | ImageStyleProps | false;
+
+
+interface CustomSeriesRenderItemAPI extends
+        CustomSeriesRenderItemCoordinateSystemAPI,
+        Pick<ExtensionAPI, 'getWidth' | 'getHeight' | 'getZr' | 'getDevicePixelRatio'> {
+    value(dim: DimensionLoose, dataIndexInside?: number): ParsedValue;
+    style(extra?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps;
+    styleEmphasis(extra?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps;
+    visual(visualType: string, dataIndexInside?: number): ReturnType<List['getItemVisual']>;
+    barLayout(opt: Omit<Parameters<typeof getLayoutOnAxis>[0], 'axis'>): ReturnType<typeof getLayoutOnAxis>;
+    currentSeriesIndices(): ReturnType<GlobalModel['getCurrentSeriesIndices']>;
+    font(opt: Parameters<typeof graphicUtil.getFont>[0]): ReturnType<typeof graphicUtil.getFont>;
+}
+interface CustomSeriesRenderItemParamsCoordSys {
+    type: string;
+    // And extra params for each coordinate systems.
+}
+interface CustomSeriesRenderItemCoordinateSystemAPI {
+    coord(
+        data: OptionDataValue | OptionDataValue[],
+        clamp?: boolean
+    ): number[];
+    size?(
+        dataSize: OptionDataValue | OptionDataValue[],
+        dataItem: OptionDataValue | OptionDataValue[]
+    ): number | number[];
+}
+interface CustomSeriesRenderItemParams {
+    context: {};
+    seriesId: string;
+    seriesName: string;
+    seriesIndex: number;
+    coordSys: CustomSeriesRenderItemParamsCoordSys;
+    dataInsideLength: number;
+    encode: ReturnType<typeof wrapEncodeDef>
+}
+type CustomSeriesRenderItem = (
+    params: CustomSeriesRenderItemParams,
+    api: CustomSeriesRenderItemAPI
+) => CustomElementOption;
+
+
+interface CustomSeriesOption extends
+    SeriesOption,
+    SeriesEncodeOptionMixin,
+    SeriesOnCartesianOptionMixin,
+    SeriesOnPolarOptionMixin,
+    SeriesOnSingleOptionMixin,
+    SeriesOnGeoOptionMixin,
+    SeriesOnCalendarOptionMixin {
+
+    // If set as 'none', do not depends on coord sys.
+    coordinateSystem?: string | 'none';
+
+    renderItem?: CustomSeriesRenderItem;
+
+    // Only works on polar and cartesian2d coordinate system.
+    clip?: boolean;
 
-const CACHED_LABEL_STYLE_PROPERTIES = graphicUtil.CACHED_LABEL_STYLE_PROPERTIES;
-const ITEM_STYLE_NORMAL_PATH = ['itemStyle'];
-const ITEM_STYLE_EMPHASIS_PATH = ['emphasis', 'itemStyle'];
-const LABEL_NORMAL = ['label'];
-const LABEL_EMPHASIS = ['emphasis', 'label'];
+    // FIXME needed?
+    tooltip?: SeriesTooltipOption;
+
+    itemStyle?: ItemStyleOption;
+    label?: LabelOption;
+    emphasis?: {
+        itemStyle?: ItemStyleOption;
+        label?: LabelOption;
+    };
+}
+
+// Also compat with ec4, where
+// `visual('color') visual('borderColor')` is supported.
+const STYLE_VISUAL_TYPE = {
+    color: 'fill',
+    borderColor: 'stroke'
+} as const;
+
+const VISUAL_PROPS = {
+    symbol: 1,
+    symbolSize: 1,
+    symbolKeepAspect: 1,
+    legendSymbol: 1,
+    visualMeta: 1,
+    liftZ: 1
+} as const;
+
+const EMPHASIS = 'emphasis' as const;
+const NORMAL = 'normal' as const;
+const PATH_ITEM_STYLE = {
+    normal: ['itemStyle'],
+    emphasis: [EMPHASIS, 'itemStyle']
+} as const;
+const PATH_LABEL = {
+    normal: ['label'],
+    emphasis: [EMPHASIS, 'label']
+} as const;
 // Use prefix to avoid index to be the same as el.name,
-// which will cause weird udpate animation.
+// which will cause weird update animation.
 const GROUP_DIFF_PREFIX = 'e\0\0';
 
+type AttachedTxInfo = {
+    isLegacy: boolean;
+    normal: {
+        cfg: ElementTextConfig;
+        conOpt: CustomElementOption | false;
+    };
+    emphasis: {
+        cfg: ElementTextConfig;
+        conOpt: CustomElementOptionOnState;
+    };
+};
+const attachedTxInfoTmp = {
+    normal: {},
+    emphasis: {}
+} as AttachedTxInfo;
+
+const Z2_SPECIFIED_BIT = {
+    normal: 0,
+    emphasis: 1
+} as const;
+
+
+
+
+export type PrepareCustomInfo = (coordSys: CoordinateSystem) => {
+    coordSys: CustomSeriesRenderItemParamsCoordSys;
+    api: CustomSeriesRenderItemCoordinateSystemAPI
+};
 
 /**
  * To reduce total package size of each coordinate systems, the modules `prepareCustom`
@@ -60,7 +296,7 @@ const GROUP_DIFF_PREFIX = 'e\0\0';
  *         size: function (dataSize, dataItem) {} // return size of each axis in coordSys.
  *     }}
  */
-const prepareCustoms = {
+const prepareCustoms: Dictionary<PrepareCustomInfo> = {
     cartesian2d: prepareCartesian2d,
     geo: prepareGeo,
     singleAxis: prepareSingleAxis,
@@ -68,29 +304,27 @@ const prepareCustoms = {
     calendar: prepareCalendar
 };
 
+class CustomSeriesModel extends SeriesModel<CustomSeriesOption> {
 
-// ------
-// Model
-// ------
+    static type = 'series.custom';
+    readonly type = CustomSeriesModel.type;
 
-SeriesModel.extend({
+    static dependencies = ['grid', 'polar', 'geo', 'singleAxis', 'calendar'];
 
-    type: 'series.custom',
+    preventAutoZ = true;
 
-    dependencies: ['grid', 'polar', 'geo', 'singleAxis', 'calendar'],
+    currentZLevel: number;
+    currentZ: number;
 
-    defaultOption: {
+    static defaultOption: CustomSeriesOption = {
         coordinateSystem: 'cartesian2d', // Can be set as 'none'
         zlevel: 0,
         z: 2,
         legendHoverLink: true,
 
-        useTransform: true,
-
         // Custom series will not clip by default.
         // Some case will use custom series to draw label
         // For example https://echarts.apache.org/examples/en/editor.html?c=custom-gantt-flight
-        // Only works on polar and cartesian2d coordinate system.
         clip: false
 
         // Cartesian coordinate system
@@ -105,43 +339,44 @@ SeriesModel.extend({
 
         // label: {}
         // itemStyle: {}
-    },
+    };
 
-    /**
-     * @override
-     */
-    getInitialData: function (option, ecModel) {
+    optionUpdated() {
+        this.currentZLevel = this.get('zlevel', true);
+        this.currentZ = this.get('z', true);
+    }
+
+    getInitialData(option: CustomSeriesOption, ecModel: GlobalModel): List {
         return createListFromArray(this.getSource(), this);
-    },
+    }
 
-    /**
-     * @override
-     */
-    getDataParams: function (dataIndex, dataType, el) {
-        const params = SeriesModel.prototype.getDataParams.apply(this, arguments);
-        el && (params.info = el.info);
+    getDataParams(dataIndex: number, dataType: string, el: Element): CallbackDataParams & {
+        info: CustomExtraElementInfo
+    } {
+        const params = super.getDataParams(dataIndex, dataType, el) as ReturnType<CustomSeriesModel['getDataParams']>;
+        el && (params.info = inner(el).info);
         return params;
     }
-});
+}
 
-// -----
-// View
-// -----
+ComponentModel.registerClass(CustomSeriesModel);
 
-ChartView.extend({
 
-    type: 'custom',
 
-    /**
-     * @private
-     * @type {module:echarts/data/List}
-     */
-    _data: null,
+class CustomSeriesView extends ChartView {
 
-    /**
-     * @override
-     */
-    render: function (customSeries, ecModel, api, payload) {
+    static type = 'custom';
+    readonly type = CustomSeriesView.type;
+
+    private _data: List;
+
+
+    render(
+        customSeries: CustomSeriesModel,
+        ecModel: GlobalModel,
+        api: ExtensionAPI,
+        payload: Payload
+    ): void {
         const oldData = this._data;
         const data = customSeries.getData();
         const group = this.group;
@@ -182,17 +417,27 @@ ChartView.extend({
         }
 
         this._data = data;
-    },
+    }
 
-    incrementalPrepareRender: function (customSeries, ecModel, api) {
+    incrementalPrepareRender(
+        customSeries: CustomSeriesModel,
+        ecModel: GlobalModel,
+        api: ExtensionAPI
+    ): void {
         this.group.removeAll();
         this._data = null;
-    },
+    }
 
-    incrementalRender: function (params, customSeries, ecModel, api, payload) {
+    incrementalRender(
+        params: StageHandlerProgressParams,
+        customSeries: CustomSeriesModel,
+        ecModel: GlobalModel,
+        api: ExtensionAPI,
+        payload: Payload
+    ): void {
         const data = customSeries.getData();
         const renderItem = makeRenderItem(customSeries, data, ecModel, api);
-        function setIncrementalAndHoverLayer(el) {
+        function setIncrementalAndHoverLayer(el: Displayable) {
             if (!el.isGroup) {
                 el.incremental = true;
                 el.useHoverLayer = true;
@@ -202,17 +447,9 @@ ChartView.extend({
             const el = createOrUpdate(null, idx, renderItem(idx, payload), customSeries, this.group, data);
             el.traverse(setIncrementalAndHoverLayer);
         }
-    },
-
-    /**
-     * @override
-     */
-    dispose: zrUtil.noop,
+    }
 
-    /**
-     * @override
-     */
-    filterForExposedEvent: function (
+    filterForExposedEvent(
         eventType: string, query: EventQueryItem, targetEl: Element, packedEvent: ECEvent
     ): boolean {
         const elementName = query.element;
@@ -230,17 +467,19 @@ ChartView.extend({
 
         return false;
     }
-});
+}
+
+ChartView.registerClass(CustomSeriesView);
 
 
-function createEl(elOption) {
+function createEl(elOption: CustomElementOption): Element {
     const graphicType = elOption.type;
     let el;
 
     // Those graphic elements are not shapes. They should not be
     // overwritten by users, so do them first.
     if (graphicType === 'path') {
-        const shape = elOption.shape;
+        const shape = (elOption as CustomSVGPathOption).shape;
         // Using pathRect brings convenience to users sacle svg path.
         const pathRect = (shape.width != null && shape.height != null)
             ? {
@@ -248,20 +487,20 @@ function createEl(elOption) {
                 y: shape.y || 0,
                 width: shape.width,
                 height: shape.height
-            }
+            } as RectLike
             : null;
         const pathData = getPathData(shape);
         // Path is also used for icon, so layout 'center' by default.
         el = graphicUtil.makePath(pathData, null, pathRect, shape.layout || 'center');
-        el.__customPathData = pathData;
+        inner(el).customPathData = pathData;
     }
     else if (graphicType === 'image') {
         el = new graphicUtil.Image({});
-        el.__customImagePath = elOption.style.image;
+        inner(el).customImagePath = (elOption as CustomImageOption).style.image;
     }
     else if (graphicType === 'text') {
         el = new graphicUtil.Text({});
-        el.__customText = elOption.style.text;
+        inner(el).customText = (elOption.style as TextStyleProps).text;
     }
     else if (graphicType === 'group') {
         el = new graphicUtil.Group();
@@ -279,96 +518,305 @@ function createEl(elOption) {
         el = new Clz();
     }
 
-    el.__customGraphicType = graphicType;
+    inner(el).customGraphicType = graphicType;
     el.name = elOption.name;
 
+    // Compat ec4: the default z2 lift is 1. If changing the number,
+    // some cases probably be broken: hierarchy layout along z, like circle packing,
+    // where emphasis only intending to modify color/border rather than lift z2.
+    (el as ECElement).z2EmphasisLift = 1;
+
     return el;
 }
 
-function updateEl(el, dataIndex, elOption, animatableModel, data, isInit, isRoot) {
-    const transitionProps = {};
-    const elOptionStyle = elOption.style || {};
-
-    elOption.shape && (transitionProps.shape = zrUtil.clone(elOption.shape));
-    elOption.position && (transitionProps.position = elOption.position.slice());
-    elOption.scale && (transitionProps.scale = elOption.scale.slice());
-    elOption.origin && (transitionProps.origin = elOption.origin.slice());
-    elOption.rotation && (transitionProps.rotation = elOption.rotation);
+/**
+ * [STRATEGY] Merge properties or erase all properties:
+ *
+ * Based on the fact that the existing zr element probably be reused, we discuss whether
+ * merge or erase all properties to the exsiting elements.
+ * + "Merge" means that if a certain props is not specified, do not assign to the existing element.
+ * + "Erase all" means that assign all of the available props whatever it specified by users.
+ *
+ * "Merge" might bring some unexpected state retaining for users and "erase all" seams to be
+ * more safe. But "erase all" force users to specify all of the props each time, which
+ * theoretically disables the chance of performance optimization (e.g., just generete shape
+ * and style at the first time rather than always do that). And "force user set all of the props"
+ * might bring trouble to specify which props need to perform "transition animation".
+ * So we still use "merge" rather than "erase all". If users need "erase all", they can
+ * simple always set all of the props each time.
+ * Some "object-like" config like `textConfig`, `textContent`, `style` which are not needed for
+ * every elment, so we replace them only when user specify them. And the that is a total replace.
+ *
+ * [STRATEGY] `hasOwnProperty` or `== null`:
+ *
+ * Ditinguishing "own property" probably bring little trouble to user when make el options.
+ * So we  trade a {xx: null} or {xx: undefined} as "not specified" if possible rather than
+ * "set them to null/undefined". In most cases, props can not be cleared. Some typicall
+ * clearable props like `style`/`textConfig`/`textContent` we enable `false` to means
+ * "clear". In some othere special cases that the prop is able to set as null/undefined,
+ * but not suitable to use `false`, `hasOwnProperty` is checked.
+ */
+function updateElNormal(
+    el: Element,
+    dataIndex: number,
+    elOption: CustomElementOption,
+    styleOpt: StyleOption,
+    attachedTxInfo: AttachedTxInfo,
+    seriesModel: CustomSeriesModel,
+    isInit: boolean,
+    isTextContent: boolean
+): void {
+    const transitionProps = {} as ElementProps;
+    const elDisplayable = el.isGroup ? null : el as Displayable;
+
+    (elOption as CustomZRPathOption).shape && (
+        (transitionProps as PathProps).shape = zrUtil.clone((elOption as CustomZRPathOption).shape)
+    );
+    setLagecyProp(elOption, transitionProps, 'position', 'x', 'y');
+    setLagecyProp(elOption, transitionProps, 'scale', 'scaleX', 'scaleY');
+    setLagecyProp(elOption, transitionProps, 'origin', 'originX', 'originY');
+    setTransProp(elOption, transitionProps, 'x');
+    setTransProp(elOption, transitionProps, 'y');
+    setTransProp(elOption, transitionProps, 'scaleX');
+    setTransProp(elOption, transitionProps, 'scaleY');
+    setTransProp(elOption, transitionProps, 'originX');
+    setTransProp(elOption, transitionProps, 'originY');
+    setTransProp(elOption, transitionProps, 'rotation');
+
+    const txCfgOpt = attachedTxInfo && attachedTxInfo.normal.cfg;
+    if (txCfgOpt) {
+        // PENDING: whether use user object directly rather than clone?
+        // TODO:5.0 textConfig transition animation?
+        el.setTextConfig(txCfgOpt);
+    }
 
-    if (el.type === 'image' && elOption.style) {
-        const targetStyle = transitionProps.style = {};
-        zrUtil.each(['x', 'y', 'width', 'height'], function (prop) {
-            prepareStyleTransition(prop, targetStyle, elOptionStyle, el.style, isInit);
-        });
+    if (el.type === 'image' && styleOpt) {
+        const targetStyle = (transitionProps as Displayable).style = {};
+        const imgStyle = (el as graphicUtil.Image).style;
+        prepareStyleTransition('x', targetStyle, styleOpt, imgStyle, isInit);
+        prepareStyleTransition('y', targetStyle, styleOpt, imgStyle, isInit);
+        prepareStyleTransition('width', targetStyle, styleOpt, imgStyle, isInit);
+        prepareStyleTransition('height', targetStyle, styleOpt, imgStyle, isInit);
     }
 
-    if (el.type === 'text' && elOption.style) {
-        const targetStyle = transitionProps.style = {};
-        zrUtil.each(['x', 'y'], function (prop) {
-            prepareStyleTransition(prop, targetStyle, elOptionStyle, el.style, isInit);
-        });
-        // Compatible with previous: both support
-        // textFill and fill, textStroke and stroke in 'text' element.
-        !elOptionStyle.hasOwnProperty('textFill') && elOptionStyle.fill && (
-            elOptionStyle.textFill = elOptionStyle.fill
+    if (el.type === 'text' && styleOpt) {
+        const textOptionStyle = styleOpt as TextStyleProps;
+        const targetStyle = (transitionProps as Displayable).style = {};
+        const textStyle = (el as graphicUtil.Text).style;
+        prepareStyleTransition('x', targetStyle, textOptionStyle, textStyle, isInit);
+        prepareStyleTransition('y', targetStyle, textOptionStyle, textStyle, isInit);
+        // Compatible with ec4: if `textFill` or `textStroke` exists use them.
+        zrUtil.hasOwn(textOptionStyle, 'textFill') && (
+            textOptionStyle.fill = (textOptionStyle as any).textFill
         );
-        !elOptionStyle.hasOwnProperty('textStroke') && elOptionStyle.stroke && (
-            elOptionStyle.textStroke = elOptionStyle.stroke
+        zrUtil.hasOwn(textOptionStyle, 'textStroke') && (
+            textOptionStyle.stroke = (textOptionStyle as any).textStroke
         );
     }
 
-    if (el.type !== 'group') {
-        el.useStyle(elOptionStyle);
+    if (elDisplayable) {
+        // PENDING: here the input style object is used directly.
+        // Good for performance but bad for compatibility control.
+        styleOpt && elDisplayable.useStyle(styleOpt);
 
         // Init animation.
         if (isInit) {
-            el.style.opacity = 0;
-            let targetOpacity = elOptionStyle.opacity;
-            targetOpacity == null && (targetOpacity = 1);
-            graphicUtil.initProps(el, {style: {opacity: targetOpacity}}, animatableModel, dataIndex);
+            elDisplayable.style.opacity = 0;
+            const targetOpacity = (styleOpt && styleOpt.opacity != null) ? styleOpt.opacity : 1;
+            graphicUtil.initProps(elDisplayable, {style: {opacity: targetOpacity}}, seriesModel, dataIndex);
         }
+
+        zrUtil.hasOwn(elOption, 'invisible') && (elDisplayable.invisible = elOption.invisible);
     }
 
     if (isInit) {
         el.attr(transitionProps);
     }
     else {
-        graphicUtil.updateProps(el, transitionProps, animatableModel, dataIndex);
+        graphicUtil.updateProps(el, transitionProps, seriesModel, dataIndex);
     }
 
     // Merge by default.
-    // z2 must not be null/undefined, otherwise sort error may occur.
-    elOption.hasOwnProperty('z2') && el.attr('z2', elOption.z2 || 0);
-    elOption.hasOwnProperty('silent') && el.attr('silent', elOption.silent);
-    elOption.hasOwnProperty('invisible') && el.attr('invisible', elOption.invisible);
-    elOption.hasOwnProperty('ignore') && el.attr('ignore', elOption.ignore);
-    // `elOption.info` enables user to mount some info on
-    // elements and use them in event handlers.
-    // Update them only when user specified, otherwise, remain.
-    elOption.hasOwnProperty('info') && el.attr('info', elOption.info);
-
-    // If `elOption.styleEmphasis` is `false`, remove hover style. The
-    // logic is ensured by `graphicUtil.setElementHoverStyle`.
-    const styleEmphasis = elOption.styleEmphasis;
-    // hoverStyle should always be set here, because if the hover style
-    // may already be changed, where the inner cache should be reset.
-    graphicUtil.enableElementHoverEmphasis(el, styleEmphasis);
+    zrUtil.hasOwn(elOption, 'silent') && (el.silent = elOption.silent);
+    zrUtil.hasOwn(elOption, 'ignore') && (el.ignore = elOption.ignore);
+
+    if (!isTextContent) {
+        // `elOption.info` enables user to mount some info on
+        // elements and use them in event handlers.
+        // Update them only when user specified, otherwise, remain.
+        zrUtil.hasOwn(elOption, 'info') && (inner(el).info = elOption.info);
+    }
+
+    el.markRedraw();
+}
+
+function updateElOnState(
+    state: DisplayStateNonNormal,
+    el: Element,
+    elStateOpt: CustomElementOptionOnState,
+    styleOpt: StyleOption,
+    attachedTxInfo: AttachedTxInfo,
+    isRoot: boolean,
+    isTextContent: boolean
+): void {
+    const elDisplayable = el.isGroup ? null : el as Displayable;
+    const txCfgOpt = attachedTxInfo && attachedTxInfo[state].cfg;
+
+    // PENDING:5.0 support customize scale change and transition animation?
+
+    if (elDisplayable) {
+        // By default support auto lift color when hover whether `emphasis` specified.
+        const stateObj = elDisplayable.ensureState(state);
+        if (styleOpt === false) {
+            const existingEmphasisState = elDisplayable.getState(state);
+            if (existingEmphasisState) {
+                existingEmphasisState.style = null;
+            }
+        }
+        else {
+            // style is needed to enable defaut emphasis.
+            stateObj.style = styleOpt || {};
+        }
+        // If `elOption.styleEmphasis` or `elOption.emphasis.style` is `false`,
+        // remove hover style.
+        // If `elOption.textConfig` or `elOption.emphasis.textConfig` is null/undefined, it does not
+        // make sense. So for simplicity, we do not ditinguish `hasOwnProperty` and null/undefined.
+        if (txCfgOpt) {
+            stateObj.textConfig = txCfgOpt;
+        }
+
+        graphicUtil.enableElementHoverEmphasis(elDisplayable);
+    }
+
     if (isRoot) {
-        graphicUtil.setAsHighDownDispatcher(el, styleEmphasis !== false);
+        graphicUtil.setAsHighDownDispatcher(el, styleOpt !== false);
+    }
+}
+
+function updateZ(
+    el: Element,
+    elOption: CustomElementOption,
+    seriesModel: CustomSeriesModel,
+    attachedTxInfo: AttachedTxInfo
+): void {
+    // Group not support textContent and not support z yet.
+    if (el.isGroup) {
+        return;
+    }
+
+    const elDisplayable = el as Displayable;
+    const currentZ = seriesModel.currentZ;
+    const currentZLevel = seriesModel.currentZLevel;
+    // Always erase.
+    elDisplayable.z = currentZ;
+    elDisplayable.zlevel = currentZLevel;
+    // z2 must not be null/undefined, otherwise sort error may occur.
+    const optZ2 = elOption.z2;
+    optZ2 != null && (elDisplayable.z2 = optZ2 || 0);
+
+    const textContent = elDisplayable.getTextContent();
+    if (textContent) {
+        textContent.z = currentZ;
+        textContent.zlevel = currentZLevel;
+    }
+
+    updateZForEachState(elDisplayable, textContent, elOption, attachedTxInfo, NORMAL);
+    updateZForEachState(elDisplayable, textContent, elOption, attachedTxInfo, EMPHASIS);
+}
+
+function updateZForEachState(
+    elDisplayable: Displayable,
+    textContent: Displayable,
+    elOption: CustomDisplayableOption,
+    attachedTxInfo: AttachedTxInfo,
+    state: DisplayState
+): void {
+    const isNormal = state === NORMAL;
+    const elStateOpt = isNormal ? elOption : retrieveStateOption(elOption, state as DisplayStateNonNormal);
+    const optZ2 = elStateOpt ? elStateOpt.z2 : null;
+    let stateObj;
+    if (optZ2 != null) {
+        // Do not `ensureState` until required.
+        stateObj = isNormal ? elDisplayable : elDisplayable.ensureState(state);
+        stateObj.z2 = optZ2 || 0;
+    }
+
+    const txConOpt = attachedTxInfo[state].conOpt;
+    if (textContent) {
+        const innerEl = inner(elDisplayable);
+        const txConZ2Set = innerEl.txConZ2Set || 0;
+        const txOptZ2 = txConOpt ? txConOpt.z2 : null;
+        const z2SetMask = 1 << Z2_SPECIFIED_BIT[state];
+
+        // Set textContent z2 as hostEl.z2 + 1 only if
+        // textContent z2 is not specified.
+        if (txOptZ2 != null) {
+            // Do not `ensureState` until required.
+            (isNormal ? textContent : textContent.ensureState(state)).z2 = txOptZ2;
+            innerEl.txConZ2Set = txConZ2Set | z2SetMask;
+        }
+        // If stateObj exists, that means stateObj.z2 has been updated, where the textContent z2
+        // should be followed, no matter textContent or textContent.emphasis is specified in elOption.
+        else if (stateObj && (txConZ2Set & z2SetMask) === 0) {
+            (isNormal ? textContent : textContent.ensureState(state)).z2 = stateObj.z2 + 1;
+        }
     }
 }
 
-function prepareStyleTransition(prop, targetStyle, elOptionStyle, oldElStyle, isInit) {
+function setLagecyProp(
+    elOption: CustomElementOption,
+    transitionProps: Partial<Pick<Transformable, TransformProps>>,
+    legacyName: 'position' | 'scale' | 'origin',
+    xName: TransformPropsX,
+    yName: TransformPropsY
+): void {
+    const legacyArr = (elOption as any)[legacyName];
+    legacyArr && (transitionProps[xName] = legacyArr[0], transitionProps[yName] = legacyArr[1]);
+}
+function setTransProp(
+    elOption: CustomElementOption,
+    transitionProps: Partial<Pick<Transformable, TransformProps>>,
+    name: TransformProps
+): void {
+    elOption[name] != null && (transitionProps[name] = elOption[name]);
+}
+
+function prepareStyleTransition(
+    prop: 'x' | 'y',
+    targetStyle: CustomTextOption['style'],
+    elOptionStyle: CustomTextOption['style'],
+    oldElStyle: graphicUtil.Text['style'],
+    isInit: boolean
+): void;
+function prepareStyleTransition(
+    prop: 'x' | 'y' | 'width' | 'height',
+    targetStyle: CustomImageOption['style'],
+    elOptionStyle: CustomImageOption['style'],
+    oldElStyle: graphicUtil.Image['style'],
+    isInit: boolean
+): void;
+function prepareStyleTransition(
+    prop: string,
+    targetStyle: any,
+    elOptionStyle: any,
+    oldElStyle: any,
+    isInit: boolean
+): void {
     if (elOptionStyle[prop] != null && !isInit) {
         targetStyle[prop] = elOptionStyle[prop];
         elOptionStyle[prop] = oldElStyle[prop];
     }
 }
 
-function makeRenderItem(customSeries, data, ecModel, api) {
+function makeRenderItem(
+    customSeries: CustomSeriesModel,
+    data: List<CustomSeriesModel>,
+    ecModel: GlobalModel,
+    api: ExtensionAPI
+) {
     const renderItem = customSeries.get('renderItem');
     const coordSys = customSeries.coordinateSystem;
-    let prepareResult = {};
+    let prepareResult = {} as ReturnType<PrepareCustomInfo>;
 
     if (coordSys) {
         if (__DEV__) {
@@ -379,8 +827,9 @@ function makeRenderItem(customSeries, data, ecModel, api) {
             );
         }
 
+        // `coordSys.prepareCustoms` is used for external coord sys like bmap.
         prepareResult = coordSys.prepareCustoms
-            ? coordSys.prepareCustoms()
+            ? coordSys.prepareCustoms(coordSys)
             : prepareCustoms[coordSys.type](coordSys);
     }
 
@@ -396,9 +845,9 @@ function makeRenderItem(customSeries, data, ecModel, api) {
         barLayout: barLayout,
         currentSeriesIndices: currentSeriesIndices,
         font: font
-    }, prepareResult.api || {});
+    }, prepareResult.api || {}) as CustomSeriesRenderItemAPI;
 
-    const userParams = {
+    const userParams: CustomSeriesRenderItemParams = {
         // The life cycle of context: current round of rendering.
         // The global life cycle is probably not necessary, because
         // user can store global status by themselves.
@@ -411,17 +860,54 @@ function makeRenderItem(customSeries, data, ecModel, api) {
         encode: wrapEncodeDef(customSeries.getData())
     };
 
+    // If someday intending to refactor them to a class, should consider do not
+    // break change: currently these attribute member are encapsulated in a closure
+    // so that do not need to force user to call these method with a scope.
+
     // Do not support call `api` asynchronously without dataIndexInside input.
-    let currDataIndexInside;
-    let currDirty = true;
-    let currItemModel;
-    let currLabelNormalModel;
-    let currLabelEmphasisModel;
-    let currVisualColor;
-
-    return function (dataIndexInside, payload) {
+    let currDataIndexInside: number;
+    let currItemModel: Model<CustomSeriesOption>;
+    let currItemStyleModels: Partial<Record<DisplayState, Model<CustomSeriesOption['itemStyle']>>> = {};
+    let currLabelModels: Partial<Record<DisplayState, Model<CustomSeriesOption['label']>>> = {};
+
+    const seriesItemStyleModels = {
+        normal: customSeries.getModel(PATH_ITEM_STYLE.normal),
+        emphasis: customSeries.getModel(PATH_ITEM_STYLE.emphasis)
+    } as Record<DisplayState, Model<CustomSeriesOption['label']>>;
+    const seriesLabelModels = {
+        normal: customSeries.getModel(PATH_LABEL.normal),
+        emphasis: customSeries.getModel(PATH_LABEL.emphasis)
+    } as Record<DisplayState, Model<CustomSeriesOption['label']>>;
+
+    function getItemModel(dataIndexInside: number): Model<CustomSeriesOption> {
+        return dataIndexInside === currDataIndexInside
+            ? (currItemModel || (currItemModel = data.getItemModel(dataIndexInside)))
+            : data.getItemModel(dataIndexInside);
+    }
+    function getItemStyleModel(dataIndexInside: number, state: DisplayState) {
+        return !data.hasItemOption
+            ? seriesItemStyleModels[state]
+            : dataIndexInside === currDataIndexInside
+            ? (currItemStyleModels[state] || (
+                currItemStyleModels[state] = getItemModel(dataIndexInside).getModel(PATH_ITEM_STYLE[state])
+            ))
+            : getItemModel(dataIndexInside).getModel(PATH_ITEM_STYLE[state]);
+    }
+    function getLabelModel(dataIndexInside: number, state: DisplayState) {
+        return !data.hasItemOption
+            ? seriesLabelModels[state]
+            : dataIndexInside === currDataIndexInside
+            ? (currLabelModels[state] || (
+                currLabelModels[state] = getItemModel(dataIndexInside).getModel(PATH_LABEL[state])
+            ))
+            : getItemModel(dataIndexInside).getModel(PATH_LABEL[state]);
+    }
+
+    return function (dataIndexInside: number, payload: Payload): CustomElementOption {
         currDataIndexInside = dataIndexInside;
-        currDirty = true;
+        currItemModel = null;
+        currItemStyleModels = {};
+        currLabelModels = {};
 
         return renderItem && renderItem(
             zrUtil.defaults({
@@ -434,158 +920,172 @@ function makeRenderItem(customSeries, data, ecModel, api) {
         );
     };
 
-    // Do not update cache until api called.
-    function updateCache(dataIndexInside) {
-        dataIndexInside == null && (dataIndexInside = currDataIndexInside);
-        if (currDirty) {
-            currItemModel = data.getItemModel(dataIndexInside);
-            currLabelNormalModel = currItemModel.getModel(LABEL_NORMAL);
-            currLabelEmphasisModel = currItemModel.getModel(LABEL_EMPHASIS);
-            currVisualColor = data.getItemVisual(dataIndexInside, 'color');
-
-            currDirty = false;
-        }
-    }
-
     /**
      * @public
-     * @param {number|string} dim
-     * @param {number} [dataIndexInside=currDataIndexInside]
-     * @return {number|string} value
+     * @param dim by default 0.
+     * @param dataIndexInside by default `currDataIndexInside`.
      */
-    function value(dim, dataIndexInside) {
+    function value(dim?: DimensionLoose, dataIndexInside?: number): ParsedValue {
         dataIndexInside == null && (dataIndexInside = currDataIndexInside);
         return data.get(data.getDimension(dim || 0), dataIndexInside);
     }
 
     /**
+     * @deprecated The orgininal intention of `api.style` is enable to set itemStyle
+     * like other series. But it not necessary and not easy to give a strict definition
+     * of what it return. And since echarts5 it needs to be make compat work. So
+     * deprecates it since echarts5.
+     *
      * By default, `visual` is applied to style (to support visualMap).
      * `visual.color` is applied at `fill`. If user want apply visual.color on `stroke`,
      * it can be implemented as:
      * `api.style({stroke: api.visual('color'), fill: null})`;
+     *
+     * [Compat]: since ec5, RectText has been separated from its hosts el.
+     * so `api.style()` will only return the style from `itemStyle` but not handle `label`
+     * any more. But `series.label` config is never published in doc.
+     * We still compat it in `api.style()`. But not encourage to use it and will still not
+     * to pulish it to doc.
      * @public
-     * @param {Object} [extra]
-     * @param {number} [dataIndexInside=currDataIndexInside]
+     * @param dataIndexInside by default `currDataIndexInside`.
      */
-    function style(extra, dataIndexInside) {
+    function style(extra?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps {
+        if (__DEV__) {
+            warnDeprecated('api.style', 'Please write literal style directly instead.');
+        }
+
         dataIndexInside == null && (dataIndexInside = currDataIndexInside);
-        updateCache(dataIndexInside);
 
-        const itemStyle = currItemModel.getModel(ITEM_STYLE_NORMAL_PATH).getItemStyle();
+        const style = data.getItemVisual(dataIndexInside, 'style');
+        const visualColor = style && style.fill;
+        const opacity = style && style.opacity;
 
-        currVisualColor != null && (itemStyle.fill = currVisualColor);
-        const opacity = data.getItemVisual(dataIndexInside, 'opacity');
+        let itemStyle = getItemStyleModel(dataIndexInside, NORMAL).getItemStyle();
+        visualColor != null && (itemStyle.fill = visualColor);
         opacity != null && (itemStyle.opacity = opacity);
 
-        const labelModel = extra
-            ? applyExtraBefore(extra, currLabelNormalModel)
-            : currLabelNormalModel;
-
-        const textStyle = graphicUtil.createTextStyle(labelModel, null, {
-            autoColor: currVisualColor,
-            isRectText: true
-        });
-
-        // TODO
-        zrUtil.extend(itemStyle, textStyle);
-
-        itemStyle.text = labelModel.getShallow('show')
+        const opt = {autoColor: zrUtil.isString(visualColor) ? visualColor : '#000'};
+        const labelModel = getLabelModel(dataIndexInside, NORMAL);
+        // Now that the feture of "auto adjust text fill/stroke" has been migrated to zrender
+        // since ec5, we should set `isAttached` as `false` here and make compat in
+        // `convertToEC4StyleForCustomSerise`.
+        const textStyle = graphicUtil.createTextStyle(labelModel, null, opt, false, true);
+        textStyle.text = labelModel.getShallow('show')
             ? zrUtil.retrieve2(
-                customSeries.getFormattedLabel(dataIndexInside, 'normal'),
+                customSeries.getFormattedLabel(dataIndexInside, NORMAL),
                 getDefaultLabel(data, dataIndexInside)
             )
             : null;
+        const textConfig = graphicUtil.createTextConfig(textStyle, labelModel, opt, false);
+
+        preFetchFromExtra(extra, itemStyle);
+        itemStyle = convertToEC4StyleForCustomSerise(itemStyle, textStyle, textConfig);
 
         extra && applyExtraAfter(itemStyle, extra);
+        (itemStyle as LegacyStyleProps).legacy = true;
 
         return itemStyle;
     }
 
     /**
+     * @deprecated The reason see `api.style()`
      * @public
-     * @param {Object} [extra]
-     * @param {number} [dataIndexInside=currDataIndexInside]
+     * @param dataIndexInside by default `currDataIndexInside`.
      */
-    function styleEmphasis(extra, dataIndexInside) {
-        dataIndexInside == null && (dataIndexInside = currDataIndexInside);
-        updateCache(dataIndexInside);
-
-        const itemStyle = currItemModel.getModel(ITEM_STYLE_EMPHASIS_PATH).getItemStyle();
-
-        const labelModel = extra
-            ? applyExtraBefore(extra, currLabelEmphasisModel)
-            : currLabelEmphasisModel;
-
-        const textStyle = graphicUtil.createTextStyle(labelModel, null, {
-            isRectText: true
-        }, true);
-        zrUtil.extend(itemStyle, textStyle);
+    function styleEmphasis(extra?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps {
+        if (__DEV__) {
+            warnDeprecated('api.styleEmphasis', 'Please write literal style directly instead.');
+        }
 
+        dataIndexInside == null && (dataIndexInside = currDataIndexInside);
 
-        itemStyle.text = labelModel.getShallow('show')
+        let itemStyle = getItemStyleModel(dataIndexInside, EMPHASIS).getItemStyle();
+        const labelModel = getLabelModel(dataIndexInside, EMPHASIS);
+        const textStyle = graphicUtil.createTextStyle(labelModel, null, null, true, true);
+        textStyle.text = labelModel.getShallow('show')
             ? zrUtil.retrieve3(
-                customSeries.getFormattedLabel(dataIndexInside, 'emphasis'),
-                customSeries.getFormattedLabel(dataIndexInside, 'normal'),
+                customSeries.getFormattedLabel(dataIndexInside, EMPHASIS),
+                customSeries.getFormattedLabel(dataIndexInside, NORMAL),
                 getDefaultLabel(data, dataIndexInside)
             )
             : null;
+        const textConfig = graphicUtil.createTextConfig(textStyle, labelModel, null, true);
+
+        preFetchFromExtra(extra, itemStyle);
+        itemStyle = convertToEC4StyleForCustomSerise(itemStyle, textStyle, textConfig);
 
         extra && applyExtraAfter(itemStyle, extra);
+        (itemStyle as LegacyStyleProps).legacy = true;
 
         return itemStyle;
     }
 
+    function preFetchFromExtra(extra: ZRStyleProps, itemStyle: ItemStyleProps): void {
+        // A trick to retrieve those props firstly, which are used to
+        // apply auto inside fill/stroke in `convertToEC4StyleForCustomSerise`.
+        // (It's not reasonable but only for a degree of compat)
+        if (extra) {
+            (extra as any).textFill && ((itemStyle as any).textFill = (extra as any).textFill);
+            (extra as any).textPosition && ((itemStyle as any).textPosition = (extra as any).textPosition);
+        }
+    }
+
     /**
      * @public
-     * @param {string} visualType
-     * @param {number} [dataIndexInside=currDataIndexInside]
+     * @param dataIndexInside by default `currDataIndexInside`.
      */
-    function visual(visualType, dataIndexInside) {
+    function visual(
+        visualType: keyof DefaultDataVisual,
+        dataIndexInside?: number
+    ): ReturnType<List['getItemVisual']> {
         dataIndexInside == null && (dataIndexInside = currDataIndexInside);
-        return data.getItemVisual(dataIndexInside, visualType);
+
+        if (zrUtil.hasOwn(STYLE_VISUAL_TYPE, visualType)) {
+            const style = data.getItemVisual(dataIndexInside, 'style');
+            return style
+                ? style[STYLE_VISUAL_TYPE[visualType as keyof typeof STYLE_VISUAL_TYPE]] as any
+                : null;
+        }
+        // Only support these visuals. Other visual might be inner tricky
+        // for performance (like `style`), do not expose to users.
+        if (zrUtil.hasOwn(VISUAL_PROPS, visualType)) {
+            return data.getItemVisual(dataIndexInside, visualType);
+        }
     }
 
     /**
      * @public
-     * @param {number} opt.count Positive interger.
-     * @param {number} [opt.barWidth]
-     * @param {number} [opt.barMaxWidth]
-     * @param {number} [opt.barMinWidth]
-     * @param {number} [opt.barGap]
-     * @param {number} [opt.barCategoryGap]
-     * @return {Object} {width, offset, offsetCenter} is not support, return undefined.
+     * @return If not support, return undefined.
      */
-    function barLayout(opt) {
-        if (coordSys.getBaseAxis) {
-            const baseAxis = coordSys.getBaseAxis();
-            return getLayoutOnAxis(zrUtil.defaults({axis: baseAxis}, opt), api);
+    function barLayout(
+        opt: Omit<Parameters<typeof getLayoutOnAxis>[0], 'axis'>
+    ): ReturnType<typeof getLayoutOnAxis> {
+        if (coordSys.type === 'cartesian2d') {
+            const baseAxis = coordSys.getBaseAxis() as Axis2D;
+            return getLayoutOnAxis(zrUtil.defaults({axis: baseAxis}, opt));
         }
     }
 
     /**
      * @public
-     * @return {Array.<number>}
      */
-    function currentSeriesIndices() {
+    function currentSeriesIndices(): ReturnType<GlobalModel['getCurrentSeriesIndices']> {
         return ecModel.getCurrentSeriesIndices();
     }
 
     /**
      * @public
-     * @param {Object} opt
-     * @param {string} [opt.fontStyle]
-     * @param {number} [opt.fontWeight]
-     * @param {number} [opt.fontSize]
-     * @param {string} [opt.fontFamily]
-     * @return {string} font string
+     * @return font string
      */
-    function font(opt) {
+    function font(
+        opt: Parameters<typeof graphicUtil.getFont>[0]
+    ): ReturnType<typeof graphicUtil.getFont> {
         return graphicUtil.getFont(opt, ecModel);
     }
 }
 
-function wrapEncodeDef(data) {
-    const encodeDef = {};
+function wrapEncodeDef(data: List<CustomSeriesModel>): Dictionary<number[]> {
+    const encodeDef = {} as Dictionary<number[]>;
     zrUtil.each(data.dimensions, function (dimName, dataDimIndex) {
         const dimInfo = data.getDimensionInfo(dimName);
         if (!dimInfo.isExtraCoord) {
@@ -597,14 +1097,29 @@ function wrapEncodeDef(data) {
     return encodeDef;
 }
 
-function createOrUpdate(el, dataIndex, elOption, animatableModel, group, data) {
-    el = doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data, true);
+function createOrUpdate(
+    el: Element,
+    dataIndex: number,
+    elOption: CustomElementOption,
+    seriesModel: CustomSeriesModel,
+    group: ViewRootGroup,
+    data: List<CustomSeriesModel>
+): Element {
+    el = doCreateOrUpdate(el, dataIndex, elOption, seriesModel, group, data, true);
     el && data.setItemGraphicEl(dataIndex, el);
 
     return el;
 }
 
-function doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data, isRoot) {
+function doCreateOrUpdate(
+    el: Element,
+    dataIndex: number,
+    elOption: CustomElementOption,
+    seriesModel: CustomSeriesModel,
+    group: ViewRootGroup,
+    data: List<CustomSeriesModel>,
+    isRoot: boolean
+): Element {
 
     // [Rule]
     // By default, follow merge mode.
@@ -616,45 +1131,75 @@ function doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data,
     //     regard "return;" as "show nothing element whatever", so make a exception to meet the
     //     most cases.)
 
-    const simplyRemove = !elOption; // `null`/`undefined`/`false`
-    elOption = elOption || {};
+    // If `elOption` is `null`/`undefined`/`false` (when `renderItem` returns nothing).
+    if (!elOption) {
+        el && group.remove(el);
+        return;
+    }
+
+    elOption = elOption || {} as CustomElementOption;
     const elOptionType = elOption.type;
-    const elOptionShape = elOption.shape;
+    const elOptionShape = (elOption as CustomZRPathOption).shape;
     const elOptionStyle = elOption.style;
 
-    if (el && (
-        simplyRemove
-        // || elOption.$merge === false
-        // If `elOptionType` is `null`, follow the merge principle.
-        || (elOptionType != null
-            && elOptionType !== el.__customGraphicType
-        )
-        || (elOptionType === 'path'
-            && hasOwnPathData(elOptionShape) && getPathData(elOptionShape) !== el.__customPathData
-        )
-        || (elOptionType === 'image'
-            && hasOwn(elOptionStyle, 'image') && elOptionStyle.image !== el.__customImagePath
-        )
-        // FIXME test and remove this restriction?
-        || (elOptionType === 'text'
-            && hasOwn(elOptionShape, 'text') && elOptionStyle.text !== el.__customText
-        )
-    )) {
-        group.remove(el);
-        el = null;
+    if (el) {
+        const elInner = inner(el);
+        if (
+            // || elOption.$merge === false
+            // If `elOptionType` is `null`, follow the merge principle.
+            (elOptionType != null
+                && elOptionType !== elInner.customGraphicType
+            )
+            || (elOptionType === 'path'
+                && hasOwnPathData(elOptionShape)
+                && getPathData(elOptionShape) !== elInner.customPathData
+            )
+            || (elOptionType === 'image'
+                && zrUtil.hasOwn(elOptionStyle, 'image')
+                && (elOptionStyle as CustomImageOption['style']).image !== elInner.customImagePath
+            )
+            // FIXME test and remove this restriction?
+            || (elOptionType === 'text'
+                && zrUtil.hasOwn(elOptionStyle, 'text')
+                && (elOptionStyle as TextStyleProps).text !== elInner.customText
+            )
+        ) {
+            group.remove(el);
+            el = null;
+        }
     }
 
-    // `elOption.type` is undefined when `renderItem` returns nothing.
-    if (simplyRemove) {
-        return;
+    const isInit = !el;
+
+    if (!el) {
+        el = createEl(elOption);
+    }
+    else {
+        // If in some case the performance issue arised, consider
+        // do not clearState but update cached normal state directly.
+        el.clearStates();
     }
 
-    const isInit = !el;
-    !el && (el = createEl(elOption));
-    updateEl(el, dataIndex, elOption, animatableModel, data, isInit, isRoot);
+    attachedTxInfoTmp.normal.cfg = attachedTxInfoTmp.normal.conOpt =
+        attachedTxInfoTmp.emphasis.cfg = attachedTxInfoTmp.emphasis.conOpt = null;
+    attachedTxInfoTmp.isLegacy = false;
+
+    doCreateOrUpdateAttachedTx(
+        el, dataIndex, elOption, seriesModel, isInit, attachedTxInfoTmp
+    );
+
+    const stateOptEmphasis = retrieveStateOption(elOption, EMPHASIS);
+    const styleOptEmphasis = retrieveStyleOptionOnState(elOption, stateOptEmphasis, EMPHASIS);
+
+    updateElNormal(el, dataIndex, elOption, elOption.style, attachedTxInfoTmp, seriesModel, isInit, false);
+    updateElOnState(EMPHASIS, el, stateOptEmphasis, styleOptEmphasis, attachedTxInfoTmp, isRoot, false);
+
+    updateZ(el, elOption, seriesModel, attachedTxInfoTmp);
 
     if (elOptionType === 'group') {
-        mergeChildren(el, dataIndex, elOption, animatableModel, data);
+        mergeChildren(
+            el as graphicUtil.Group, dataIndex, elOption as CustomGroupOption, seriesModel, data
+        );
     }
 
     // Always add whatever already added to ensure sequence.
@@ -663,6 +1208,137 @@ function doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data,
     return el;
 }
 
+function doCreateOrUpdateAttachedTx(
+    el: Element,
+    dataIndex: number,
+    elOption: CustomElementOption,
+    seriesModel: CustomSeriesModel,
+    isInit: boolean,
+    attachedTxInfo: AttachedTxInfo
+): void {
+    // group do not support textContent temporarily untill necessary.
+    if (el.isGroup) {
+        return;
+    }
+
+    // Normal must be called before emphasis, for `isLegacy` detection.
+    processTxInfo(elOption, null, attachedTxInfo);
+    processTxInfo(elOption, EMPHASIS, attachedTxInfo);
+
+    // If `elOption.textConfig` or `elOption.textContent` is null/undefined, it does not make sence.
+    // So for simplicity, if "elOption hasOwnProperty of them but be null/undefined", we do not
+    // trade them as set to null to el.
+    // Especially:
+    // `elOption.textContent: false` means remove textContent.
+    // `elOption.textContent.emphasis.style: false` means remove the style from emphasis state.
+    let txConOptNormal = attachedTxInfo.normal.conOpt as CustomElementOption | false;
+    const txConOptEmphasis = attachedTxInfo.emphasis.conOpt as CustomElementOptionOnState;
+
+    if (txConOptEmphasis != null) {
+        // If textContent has emphasis state, el should auto has emphasis
+        // state, otherwise it can not be triggered.
+        el.ensureState(EMPHASIS);
+    }
+
+    if (txConOptNormal != null || txConOptEmphasis != null) {
+        let textContent = el.getTextContent();
+        if (txConOptNormal === false) {
+            textContent && el.removeTextContent();
+        }
+        else {
+            txConOptNormal = attachedTxInfo.normal.conOpt = txConOptNormal || {type: 'text'};
+            if (!textContent) {
+                textContent = createEl(txConOptNormal) as graphicUtil.Text;
+                el.setTextContent(textContent);
+            }
+            else {
+                // If in some case the performance issue arised, consider
+                // do not clearState but update cached normal state directly.
+                textContent.clearStates();
+            }
+            const txConStlOptNormal = txConOptNormal && txConOptNormal.style;
+
+            updateElNormal(
+                textContent, dataIndex, txConOptNormal, txConStlOptNormal, null, seriesModel, isInit, true
+            );
+            const txConStlOptEmphasis = retrieveStyleOptionOnState(txConOptNormal, txConOptEmphasis, EMPHASIS);
+            updateElOnState(EMPHASIS, textContent, txConOptEmphasis, txConStlOptEmphasis, null, false, true);
+
+            textContent.markRedraw();
+        }
+    }
+}
+
+function processTxInfo(
+    elOption: CustomElementOption,
+    state: DisplayStateNonNormal,
+    attachedTxInfo: AttachedTxInfo
+): void {
+    const stateOpt = !state ? elOption : retrieveStateOption(elOption, state);
+    const styleOpt = !state ? elOption.style : retrieveStyleOptionOnState(elOption, stateOpt, EMPHASIS);
+
+    const elType = elOption.type;
+    let txCfg = stateOpt ? stateOpt.textConfig : null;
+    const txConOptNormal = elOption.textContent;
+    let txConOpt: CustomElementOption | CustomElementOptionOnState =
+        !txConOptNormal ? null : !state ? txConOptNormal : retrieveStateOption(txConOptNormal, state);
+
+    if (styleOpt && (
+        // Because emphasis style has little info to detect legacy,
+        // if normal is legacy, emphasis is trade as legacy.
+        attachedTxInfo.isLegacy
+        || isEC4CompatibleStyle(styleOpt, elType, !!txCfg, !!txConOpt)
+    )) {
+        attachedTxInfo.isLegacy = true;
+        const convertResult = convertFromEC4CompatibleStyle(styleOpt, elType, !state);
+        // Explicitly specified `textConfig` and `textContent` has higher priority than
+        // the ones generated by legacy style. Otherwise if users use them and `api.style`
+        // at the same time, they not both work and hardly to known why.
+        if (!txCfg && convertResult.textConfig) {
+            txCfg = convertResult.textConfig;
+        }
+        if (!txConOpt && convertResult.textContent) {
+            txConOpt = convertResult.textContent;
+        }
+    }
+
+    if (!state && txConOpt) {
+        const txConOptNormal = txConOpt as CustomElementOption;
+        // `textContent: {type: 'text'}`, the "type" is easy to be missing. So we tolerate it.
+        !txConOptNormal.type && (txConOptNormal.type = 'text');
+        if (__DEV__) {
+            // Do not tolerate incorret type for forward compat.
+            txConOptNormal.type !== 'text' && zrUtil.assert(
+                txConOptNormal.type === 'text',
+                'textContent.type must be "text"'
+            );
+        }
+    }
+
+    const info = !state ? attachedTxInfo.normal : attachedTxInfo[state];
+    info.cfg = txCfg;
+    info.conOpt = txConOpt;
+}
+
+function retrieveStateOption(
+    elOption: CustomElementOption, state: DisplayStateNonNormal
+): CustomElementOptionOnState {
+    return !state ? elOption : elOption ? elOption[state] : null;
+}
+
+function retrieveStyleOptionOnState(
+    stateOptionNormal: CustomElementOption,
+    stateOption: CustomElementOptionOnState,
+    state: DisplayStateNonNormal
+): StyleOption {
+    let style = stateOption && stateOption.style;
+    if (style == null && state === EMPHASIS && stateOptionNormal) {
+        style = stateOptionNormal.styleEmphasis;
+    }
+    return style;
+}
+
+
 // Usage:
 // (1) By default, `elOption.$mergeChildren` is `'byIndex'`, which indicates that
 //     the existing children will not be removed, and enables the feature that
@@ -679,7 +1355,14 @@ function doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data,
 // child (otherwise the total indicies of the children array have to be modified).
 // User can remove a single child by set its `ignore` as `true` or replace
 // it by another element, where its `$merge` can be set as `true` if necessary.
-function mergeChildren(el, dataIndex, elOption, animatableModel, data) {
+function mergeChildren(
+    el: graphicUtil.Group,
+    dataIndex: number,
+    elOption: CustomGroupOption,
+    seriesModel: CustomSeriesModel,
+    data: List<CustomSeriesModel>
+): void {
+
     const newChildren = elOption.children;
     const newLen = newChildren ? newChildren.length : 0;
     const mergeChildren = elOption.$mergeChildren;
@@ -697,7 +1380,7 @@ function mergeChildren(el, dataIndex, elOption, animatableModel, data) {
             oldChildren: el.children() || [],
             newChildren: newChildren || [],
             dataIndex: dataIndex,
-            animatableModel: animatableModel,
+            seriesModel: seriesModel,
             group: el,
             data: data
         });
@@ -714,9 +1397,10 @@ function mergeChildren(el, dataIndex, elOption, animatableModel, data) {
             el.childAt(index),
             dataIndex,
             newChildren[index],
-            animatableModel,
+            seriesModel,
             el,
-            data
+            data,
+            false
         );
     }
     if (__DEV__) {
@@ -727,7 +1411,15 @@ function mergeChildren(el, dataIndex, elOption, animatableModel, data) {
     }
 }
 
-function diffGroupChildren(context) {
+type DiffGroupContext = {
+    oldChildren: Element[],
+    newChildren: CustomElementOption[],
+    dataIndex: number,
+    seriesModel: CustomSeriesModel,
+    group: graphicUtil.Group,
+    data: List<CustomSeriesModel>
+};
+function diffGroupChildren(context: DiffGroupContext) {
     (new DataDiffer(
         context.oldChildren,
         context.newChildren,
@@ -741,12 +1433,16 @@ function diffGroupChildren(context) {
         .execute();
 }
 
-function getKey(item, idx) {
+function getKey(item: Element, idx: number): string {
     const name = item && item.name;
     return name != null ? name : GROUP_DIFF_PREFIX + idx;
 }
 
-function processAddUpdate(newIndex, oldIndex) {
+function processAddUpdate(
+    this: DataDiffer<DiffGroupContext>,
+    newIndex: number,
+    oldIndex?: number
+): void {
     const context = this.context;
     const childOption = newIndex != null ? context.newChildren[newIndex] : null;
     const child = oldIndex != null ? context.oldChildren[oldIndex] : null;
@@ -755,50 +1451,36 @@ function processAddUpdate(newIndex, oldIndex) {
         child,
         context.dataIndex,
         childOption,
-        context.animatableModel,
+        context.seriesModel,
         context.group,
-        context.data
+        context.data,
+        false
     );
 }
 
-// `graphic#applyDefaultTextStyle` will cache
-// textFill, textStroke, textStrokeWidth.
-// We have to do this trick.
-function applyExtraBefore(extra, model) {
-    const dummyModel = new Model({}, model);
-    zrUtil.each(CACHED_LABEL_STYLE_PROPERTIES, function (stylePropName, modelPropName) {
-        if (extra.hasOwnProperty(stylePropName)) {
-            dummyModel.option[modelPropName] = extra[stylePropName];
-        }
-    });
-    return dummyModel;
-}
-
-function applyExtraAfter(itemStyle, extra) {
+function applyExtraAfter(itemStyle: ZRStyleProps, extra: ZRStyleProps): void {
     for (const key in extra) {
-        if (extra.hasOwnProperty(key)
-            || !CACHED_LABEL_STYLE_PROPERTIES.hasOwnProperty(key)
-        ) {
-            itemStyle[key] = extra[key];
+        if (zrUtil.hasOwn(extra, key)) {
+            (itemStyle as any)[key] = (extra as any)[key];
         }
     }
 }
 
-function processRemove(oldIndex) {
+function processRemove(this: DataDiffer<DiffGroupContext>, oldIndex: number): void {
     const context = this.context;
     const child = context.oldChildren[oldIndex];
     child && context.group.remove(child);
 }
 
-function getPathData(shape) {
+/**
+ * @return SVG Path data.
+ */
+function getPathData(shape: CustomSVGPathOption['shape']): string {
     // "d" follows the SVG convention.
     return shape && (shape.pathData || shape.d);
 }
 
-function hasOwnPathData(shape) {
-    return shape && (shape.hasOwnProperty('pathData') || shape.hasOwnProperty('d'));
+function hasOwnPathData(shape: CustomSVGPathOption['shape']): boolean {
+    return shape && (zrUtil.hasOwn(shape, 'pathData') || zrUtil.hasOwn(shape, 'd'));
 }
 
-function hasOwn(host, prop) {
-    return host && host.hasOwnProperty(prop);
-}
diff --git a/src/coord/CoordinateSystem.ts b/src/coord/CoordinateSystem.ts
index 2af741a..84fd2f5 100644
--- a/src/coord/CoordinateSystem.ts
+++ b/src/coord/CoordinateSystem.ts
@@ -26,6 +26,7 @@ import { BoundingRect } from '../util/graphic';
 import { MatrixArray } from 'zrender/src/core/matrix';
 import ComponentModel from '../model/Component';
 import { RectLike } from 'zrender/src/core/BoundingRect';
+import { PrepareCustomInfo } from '../chart/custom';
 
 
 export interface CoordinateSystemCreator {
@@ -151,6 +152,8 @@ export interface CoordinateSystem {
     // Currently only Cartesian2D implements it.
     // But if other coordinate systems implement it, should follow this signature.
     getAxesByScale?: (scaleType: string) => Axis[];
+
+    prepareCustoms?: PrepareCustomInfo;
 }
 
 /**
diff --git a/src/echarts.ts b/src/echarts.ts
index d60e366..08fb940 100644
--- a/src/echarts.ts
+++ b/src/echarts.ts
@@ -1729,6 +1729,9 @@ class ECharts extends Eventful {
         };
 
         updateZ = function (model: ComponentModel, view: ComponentView | ChartView): void {
+            if (model.preventAutoZ) {
+                return;
+            }
             const z = model.get('z');
             const zlevel = model.get('zlevel');
             // Set z and zlevel
@@ -1743,6 +1746,7 @@ class ECharts extends Eventful {
                         textContent.z = el.z;
                         textContent.zlevel = el.zlevel;
                         // lift z2 of text content
+                        // TODO if el.emphasis.z2 is spcefied, what about textContent.
                         textContent.z2 = el.z2 + 1;
                     }
                 }
diff --git a/src/model/Component.ts b/src/model/Component.ts
index 0d2b0c0..d1a35e9 100644
--- a/src/model/Component.ts
+++ b/src/model/Component.ts
@@ -123,6 +123,11 @@ class ComponentModel<Opt extends ComponentOption = ComponentOption> extends Mode
      */
     static layoutMode: ComponentLayoutMode | ComponentLayoutMode['type'];
 
+    /**
+     * Prevent from auto set z, zlevel, z2 by the framework.
+     */
+    preventAutoZ: boolean;
+
     // Injectable properties:
     __viewId: string;
 
diff --git a/src/model/mixin/itemStyle.ts b/src/model/mixin/itemStyle.ts
index c0c88e0..129c67f 100644
--- a/src/model/mixin/itemStyle.ts
+++ b/src/model/mixin/itemStyle.ts
@@ -44,7 +44,7 @@ type ItemStyleKeys = 'fill'
     | 'shadowOffsetY'
     | 'shadowColor';
 
-type ItemStyleProps = Pick<PathStyleProps, ItemStyleKeys>;
+export type ItemStyleProps = Pick<PathStyleProps, ItemStyleKeys>;
 
 class ItemStyleMixin {
 
diff --git a/src/util/graphic.ts b/src/util/graphic.ts
index 2875480..cadfa38 100644
--- a/src/util/graphic.ts
+++ b/src/util/graphic.ts
@@ -58,7 +58,8 @@ import {
     ColorString,
     DataModel,
     ECEventData,
-    ZRStyleProps
+    ZRStyleProps,
+    TextCommonOption
 } from './types';
 import GlobalModel from '../model/Global';
 import { makeInner } from './model';
@@ -81,13 +82,6 @@ const EMPTY_OBJ = {};
 
 export const Z2_EMPHASIS_LIFT = 10;
 
-// key: label model property nane, value: style property name.
-export const CACHED_LABEL_STYLE_PROPERTIES = {
-    color: 'textFill',
-    textBorderColor: 'textStroke',
-    textBorderWidth: 'textStrokeWidth'
-};
-
 const EMPHASIS = 'emphasis';
 const NORMAL = 'normal';
 
@@ -126,8 +120,6 @@ type TextCommonParams = {
 
     forceRich?: boolean
 
-    getTextPosition?: (textStyleModel: Model, isEmphasis?: boolean) => string | string[] | number[]
-
     defaultOutsidePosition?: LabelOption['position']
 
     textStyle?: ZRStyleProps
@@ -390,12 +382,14 @@ function singleEnterEmphasis(el: Element) {
         if (!hasFillOrStroke(emphasisStyle.stroke)) {
             disp.style.stroke = liftColor(currentStroke);
         }
-        disp.z2 += Z2_EMPHASIS_LIFT;
+        const z2EmphasisLift = (disp as ECElement).z2EmphasisLift;
+        disp.z2 += z2EmphasisLift != null ? z2EmphasisLift : Z2_EMPHASIS_LIFT;
     }
 
     const textContent = el.getTextContent();
     if (textContent) {
-        textContent.z2 += Z2_EMPHASIS_LIFT;
+        const z2EmphasisLift = (textContent as ECElement).z2EmphasisLift;
+        textContent.z2 += z2EmphasisLift != null ? z2EmphasisLift : Z2_EMPHASIS_LIFT;
     }
     // TODO hover layer
 }
@@ -770,6 +764,7 @@ export function createTextConfig(
     opt: TextCommonParams,
     isEmphasis: boolean
 ) {
+    opt = opt || {};
     const textConfig: ElementTextConfig = {};
     let labelPosition;
     let labelRotate = textStyleModel.getShallow('rotate');
@@ -778,16 +773,11 @@ export function createTextConfig(
     );
     const labelOffset = textStyleModel.getShallow('offset');
 
-    if (opt.getTextPosition) {
-        labelPosition = opt.getTextPosition(textStyleModel, isEmphasis);
-    }
-    else {
-        labelPosition = textStyleModel.getShallow('position')
-            || (isEmphasis ? null : 'inside');
-        // 'outside' is not a valid zr textPostion value, but used
-        // in bar series, and magric type should be considered.
-        labelPosition === 'outside' && (labelPosition = opt.defaultOutsidePosition || 'top');
-    }
+    labelPosition = textStyleModel.getShallow('position')
+        || (isEmphasis ? null : 'inside');
+    // 'outside' is not a valid zr textPostion value, but used
+    // in bar series, and magric type should be considered.
+    labelPosition === 'outside' && (labelPosition = opt.defaultOutsidePosition || 'top');
 
     if (labelPosition != null) {
         textConfig.position = labelPosition;
@@ -1031,7 +1021,10 @@ function setTokenTextStyle(
     }
 }
 
-export function getFont(opt: LabelOption, ecModel: GlobalModel) {
+export function getFont(
+    opt: Pick<TextCommonOption, 'fontStyle' | 'fontWeight' | 'fontSize' | 'fontFamily'>,
+    ecModel: GlobalModel
+) {
     const gTextStyleModel = ecModel && ecModel.getModel('textStyle');
     return trim([
         // FIXME in node-canvas fontWeight is before fontStyle
diff --git a/src/util/styleCompat.ts b/src/util/styleCompat.ts
new file mode 100644
index 0000000..b7e1150
--- /dev/null
+++ b/src/util/styleCompat.ts
@@ -0,0 +1,256 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+*
+*   http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied.  See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+import { Dictionary, ZRStyleProps } from './types';
+import { ElementTextConfig } from 'zrender/src/Element';
+import { TextStyleProps, TextStylePropsPart, TextProps } from 'zrender/src/graphic/Text';
+import { each, hasOwn } from 'zrender/src/core/util';
+import { __DEV__ } from '../config';
+import { ItemStyleProps } from '../model/mixin/itemStyle';
+
+export interface LegacyStyleProps {
+    legacy?: boolean
+}
+
+const deprecatedLogs = {} as Dictionary<boolean>;
+
+/**
+ * Whether need to call `convertEC4CompatibleStyle`.
+ */
+export function isEC4CompatibleStyle(
+    style: ZRStyleProps & LegacyStyleProps,
+    elType: string,
+    hasOwnTextContentOption: boolean,
+    hasOwnTextConfig: boolean
+): boolean {
+    // Since echarts5, `RectText` is separated from its host element and style.text
+    // does not exist any more. The compat work brings some extra burden on performance.
+    // So we provide:
+    // `legacy: true` force make compat.
+    // `legacy: false`, force do not compat.
+    // `legacy` not set: auto detect wheter legacy.
+    //     But in this case we do not compat (difficult to detect and rare case):
+    //     Becuse custom series and graphic component support "merge", users may firstly
+    //     only set `textStrokeWidth` style or secondly only set `text`.
+    return style && (
+        style.legacy
+        || (
+            style.legacy !== false
+            && !hasOwnTextContentOption
+            && !hasOwnTextConfig
+            && elType !== 'tspan'
+            // Difficult to detect whether legacy for a "text" el.
+            && (elType === 'text' || hasOwn(style, 'text'))
+        )
+    );
+}
+
+/**
+ * `EC4CompatibleStyle` is style that might be in echarts4 format or echarts5 format.
+ * @param hostStyle The properties might be modified.
+ * @return If be text el, `textContentStyle` and `textConfig` will not be retured.
+ *         Otherwise a `textContentStyle` and `textConfig` will be created, whose props area
+ *         retried from the `hostStyle`.
+ */
+export function convertFromEC4CompatibleStyle(hostStyle: ZRStyleProps, elType: string, isNormal: boolean): {
+    textContent: TextProps & {type: string},
+    textConfig: ElementTextConfig
+} {
+    const srcStyle = hostStyle as Dictionary<any>;
+    let textConfig: ElementTextConfig;
+    let textContent: TextProps & {type: string};
+
+    let textContentStyle: TextStyleProps;
+    if (elType === 'text') {
+        textContentStyle = srcStyle;
+    }
+    else {
+        textContentStyle = {};
+        hasOwn(srcStyle, 'text') && (textContentStyle.text = srcStyle.text);
+        hasOwn(srcStyle, 'rich') && (textContentStyle.rich = srcStyle.rich);
+        hasOwn(srcStyle, 'textFill') && (textContentStyle.fill = srcStyle.textFill);
+        hasOwn(srcStyle, 'textStroke') && (textContentStyle.stroke = srcStyle.textStroke);
+
+        textContent = {
+            type: 'text',
+            style: textContentStyle,
+            // ec4 do not support rectText trigger.
+            // And when text postion is different in normal and emphasis
+            // => hover text trigger emphasis;
+            // => text position changed, leave mouse pointer immediately;
+            // That might cause state incorrect.
+            silent: true
+        };
+        textConfig = {};
+        const hasOwnPos = hasOwn(srcStyle, 'textPosition');
+        if (isNormal) {
+            textConfig.position = hasOwnPos ? srcStyle.textPosition : 'inside';
+        }
+        else {
+            hasOwnPos && (textConfig.position = srcStyle.textPosition);
+        }
+        hasOwn(srcStyle, 'textPosition') && (textConfig.position = srcStyle.textPosition);
+        hasOwn(srcStyle, 'textOffset') && (textConfig.offset = srcStyle.textOffset);
+        hasOwn(srcStyle, 'textRotation') && (textConfig.rotation = srcStyle.textRotation);
+        hasOwn(srcStyle, 'textDistance') && (textConfig.distance = srcStyle.textDistance);
+    }
+
+    convertEC4CompatibleRichItem(textContentStyle, hostStyle);
+
+    each(textContentStyle.rich, function (richItem) {
+        convertEC4CompatibleRichItem(richItem as TextStyleProps, richItem);
+    });
+
+    return {
+        textConfig: textConfig,
+        textContent: textContent
+    };
+}
+
+/**
+ * The result will be set to `out`.
+ */
+function convertEC4CompatibleRichItem(out: TextStylePropsPart, richItem: Dictionary<any>): void {
+    if (!richItem) {
+        return;
+    }
+    // (1) For simplicity, make textXXX properties (deprecated since ec5) has
+    // higher priority. For example, consider in ec4 `borderColor: 5, textBorderColor: 10`
+    // on a rect means `borderColor: 4` on the rect and `borderColor: 10` on an attached
+    // richText in ec5.
+    // (2) `out === richItem` if and only if `out` is text el or rich item.
+    // So we can overwite existing props in `out` since textXXX has higher priority.
+    richItem.font = richItem.textFont || richItem.font;
+    hasOwn(richItem, 'textStrokeWidth') && (out.lineWidth = richItem.textStrokeWidth);
+    hasOwn(richItem, 'textAlign') && (out.align = richItem.textAlign);
+    hasOwn(richItem, 'textVerticalAlign') && (out.verticalAlign = richItem.textVerticalAlign);
+    hasOwn(richItem, 'textLineHeight') && (out.lineHeight = richItem.textLineHeight);
+    hasOwn(richItem, 'textWidth') && (out.width = richItem.textWidth);
+    hasOwn(richItem, 'textHeight') && (out.height = richItem.textHeight);
+    hasOwn(richItem, 'textBackgroundColor') && (out.backgroundColor = richItem.textBackgroundColor);
+    hasOwn(richItem, 'textPadding') && (out.padding = richItem.textPadding);
+    hasOwn(richItem, 'textBorderColor') && (out.borderColor = richItem.textBorderColor);
+    hasOwn(richItem, 'textBorderWidth') && (out.borderWidth = richItem.textBorderWidth);
+    hasOwn(richItem, 'textBorderRadius') && (out.borderRadius = richItem.textBorderRadius);
+    hasOwn(richItem, 'textBoxShadowColor') && (out.shadowColor = richItem.textBoxShadowColor);
+    hasOwn(richItem, 'textBoxShadowBlur') && (out.shadowBlur = richItem.textBoxShadowBlur);
+    hasOwn(richItem, 'textBoxShadowOffsetX') && (out.shadowOffsetX = richItem.textBoxShadowOffsetX);
+    hasOwn(richItem, 'textBoxShadowOffsetY') && (out.shadowOffsetY = richItem.textBoxShadowOffsetY);
+}
+
+/**
+ * Convert to pure echarts4 format style.
+ * `itemStyle` will be modified, added with ec4 style properties from
+ * `textStyle` and `textConfig`.
+ *
+ * [Caveat]: For simplicity, `insideRollback` in ec4 does not compat, where
+ * `styleEmphasis: {textFill: 'red'}` will remove the normal auto added stroke.
+ */
+export function convertToEC4StyleForCustomSerise(
+    itemStl: ItemStyleProps,
+    txStl: TextStyleProps,
+    txCfg: ElementTextConfig
+): ZRStyleProps {
+
+    const out = itemStl as Dictionary<unknown>;
+
+    // See `custom.ts`, a trick to set extra `textPosition` firstly.
+    out.textPosition = out.textPosition || txCfg.position || 'inside';
+    txCfg.offset != null && (out.textOffset = txCfg.offset);
+    txCfg.rotation != null && (out.textRotation = txCfg.rotation);
+    txCfg.distance != null && (out.textDistance = txCfg.distance);
+
+    const isInside = (out.textPosition as string).indexOf('inside') >= 0;
+    const hostFill = itemStl.fill || '#000';
+
+    convertToEC4RichItem(out, txStl);
+
+    const textFillNotSet = out.textFill == null;
+    if (isInside) {
+        if (textFillNotSet) {
+            out.textFill = txCfg.insideFill || '#fff';
+            !out.textStroke && txCfg.insideStroke && (out.textStroke = txCfg.insideStroke);
+            !out.textStroke && (out.textStroke = hostFill);
+            out.textStrokeWidth == null && (out.textStrokeWidth = 2);
+        }
+    }
+    else {
+        if (textFillNotSet) {
+            out.textFill = txCfg.outsideFill || hostFill;
+        }
+        !out.textStroke && txCfg.outsideStroke && (out.textStroke = txCfg.outsideStroke);
+    }
+
+    out.text = txStl.text;
+    out.rich = txStl.rich;
+
+    each(txStl.rich, function (richItem) {
+        convertToEC4RichItem(richItem as Dictionary<unknown>, richItem);
+    });
+
+    return out;
+}
+
+function convertToEC4RichItem(out: Dictionary<unknown>, richItem: TextStylePropsPart) {
+    if (!richItem) {
+        return;
+    }
+
+    hasOwn(richItem, 'fill') && (out.textFill = richItem.fill);
+    hasOwn(richItem, 'stroke') && (out.textStroke = richItem.fill);
+
+    hasOwn(richItem, 'lineWidth') && (out.textStrokeWidth = richItem.lineWidth);
+    hasOwn(richItem, 'font') && (out.textStrokeWidth = richItem.font);
+    hasOwn(richItem, 'fontStyle') && (out.fontStyle = richItem.fontStyle);
+    hasOwn(richItem, 'fontWeight') && (out.fontWeight = richItem.fontWeight);
+    hasOwn(richItem, 'fontSize') && (out.fontSize = richItem.fontSize);
+    hasOwn(richItem, 'fontFamily') && (out.fontFamily = richItem.fontFamily);
+
+    hasOwn(richItem, 'align') && (out.textAlign = richItem.align);
+    hasOwn(richItem, 'verticalAlign') && (out.textVerticalAlign = richItem.verticalAlign);
+    hasOwn(richItem, 'lineHeight') && (out.textLineHeight = richItem.lineHeight);
+    hasOwn(richItem, 'width') && (out.textWidth = richItem.width);
+    hasOwn(richItem, 'height') && (out.textHeight = richItem.height);
+
+    hasOwn(richItem, 'backgroundColor') && (out.textBackgroundColor = richItem.backgroundColor);
+    hasOwn(richItem, 'padding') && (out.textPadding = richItem.padding);
+    hasOwn(richItem, 'borderColor') && (out.textBorderColor = richItem.borderColor);
+    hasOwn(richItem, 'borderWidth') && (out.textBorderWidth = richItem.borderWidth);
+    hasOwn(richItem, 'borderRadius') && (out.textBorderRadius = richItem.borderRadius);
+
+    hasOwn(richItem, 'shadowColor') && (out.textBoxShadowColor = richItem.shadowColor);
+    hasOwn(richItem, 'shadowBlur') && (out.textBoxShadowBlur = richItem.shadowBlur);
+    hasOwn(richItem, 'shadowOffsetX') && (out.textBoxShadowOffsetX = richItem.shadowOffsetX);
+    hasOwn(richItem, 'shadowOffsetY') && (out.textBoxShadowOffsetY = richItem.shadowOffsetY);
+
+    hasOwn(richItem, 'textShadowColor') && (out.textShadowColor = richItem.textShadowColor);
+    hasOwn(richItem, 'textShadowBlur') && (out.textShadowBlur = richItem.textShadowBlur);
+    hasOwn(richItem, 'textShadowOffsetX') && (out.textShadowOffsetX = richItem.textShadowOffsetX);
+    hasOwn(richItem, 'textShadowOffsetY') && (out.textShadowOffsetY = richItem.textShadowOffsetY);
+}
+
+export function warnDeprecated(deprecated: string, insteadApproach: string): void {
+    if (__DEV__) {
+        const key = deprecated + '^_^' + insteadApproach;
+        if (!deprecatedLogs[key]) {
+            console.warn(`DEPRECATED: "${deprecated}" has been deprecated. ${insteadApproach}`);
+            deprecatedLogs[key] = true;
+        }
+    }
+}
diff --git a/src/util/types.ts b/src/util/types.ts
index 07f773e..b74d08c 100644
--- a/src/util/types.ts
+++ b/src/util/types.ts
@@ -106,6 +106,7 @@ export interface ECElement extends Element {
     };
     highDownSilentOnTouch?: boolean;
     onStateChange?: (fromState: 'normal' | 'emphasis', toState: 'normal' | 'emphasis') => void;
+    z2EmphasisLift?: number;
 }
 
 export interface DataHost {
@@ -422,6 +423,7 @@ export type ModelOption = any;
 export type ThemeOption = Dictionary<any>;
 
 export type DisplayState = 'normal' | 'emphasis';
+export type DisplayStateNonNormal = 'emphasis';
 export type DisplayStateHostOption = {
     emphasis?: Dictionary<any>,
     [key: string]: any
diff --git a/test/circle-packing-with-d3.html b/test/circle-packing-with-d3.compat.html
similarity index 100%
copy from test/circle-packing-with-d3.html
copy to test/circle-packing-with-d3.compat.html
diff --git a/test/circle-packing-with-d3.html b/test/circle-packing-with-d3.html
index 486b6e3..06a77ec 100644
--- a/test/circle-packing-with-d3.html
+++ b/test/circle-packing-with-d3.html
@@ -45,6 +45,7 @@ text {
 <svg width="960" height="960"><g transform="translate(1,1)"></g></svg>
 <script src="https://d3js.org/d3.v4.min.js"></script>
 <script src="../dist/echarts.js"></script>
+<script src="./lib/testHelper.js"></script>
 <script>
 
 var stratify = d3.stratify()
@@ -105,6 +106,8 @@ d3.csv("data/flare.csv", function(error, rawData) {
             nodeName = nodePath.slice(nodePath.lastIndexOf('.') + 1).split(/(?=[A-Z][^A-Z])/g).join('\n');
             nodeName = echarts.format.truncateText(nodeName, itemLayout.r, textFont, '.');
         }
+        var z2 = api.value(1) * 2;
+        // console.log(api.style());
 
         return {
             type: 'circle',
@@ -113,19 +116,35 @@ d3.csv("data/flare.csv", function(error, rawData) {
                 cy: itemLayout.y,
                 r: itemLayout.r
             },
-            z2: api.value(1) * 2,
-            style: api.style({
-                text: nodeName,
-                textFont: textFont,
-                textPosition: 'inside'
-            }),
-            styleEmphasis: api.style({
+            z2: z2,
+            textContent: {
+                type: 'text',
+                style: {
+                    text: nodeName,
+                    // fill: 'blue'
+                },
+                emphasis: {
+                    style: {
+                        fontSize: 16,
+                        // fill: 'red'
+                    }
+                }
+            },
+            textConfig: {
+                position: 'inside'
+            },
+            style: {
+                fill: api.visual('color'),
                 text: nodeName,
-                textPosition: 'inside',
-                textFont: textFont,
-                stroke: 'rgba(0,0,0,0.5)',
-                lineWidth: 3
-            })
+                font: textFont,
+            },
+            emphasis: {
+                style: {
+                    font: textFont,
+                    stroke: 'rgba(0,0,0,0.5)',
+                    lineWidth: 3
+                }
+            }
         };
     }
 
@@ -165,6 +184,13 @@ d3.csv("data/flare.csv", function(error, rawData) {
 
     chart.setOption(option);
 
+    // testHelper.printElements(chart, {
+    //     attr: ['z', 'z2', 'style.text', 'style.fill', 'style.stroke'],
+    //     filter: function (el) {
+    //         return el.style && el.style.text;
+    //     }
+    // });
+
 });
 
 </script>
diff --git a/test/custom-feature.html b/test/custom-feature.html
index 7ec7696..03457ed 100644
--- a/test/custom-feature.html
+++ b/test/custom-feature.html
@@ -85,6 +85,7 @@ under the License.
                                         style: {
                                             fill: 'red',
                                             text: 'dataIndex: ' + params.dataIndex,
+                                            textFill: '#000',
                                             textStroke: '#fff',
                                             textStrokeWidth: 1
                                         }
diff --git a/test/custom-text-content.html b/test/custom-text-content.html
new file mode 100644
index 0000000..d999acd
--- /dev/null
+++ b/test/custom-text-content.html
@@ -0,0 +1,1193 @@
+<!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="lib/canteen.js"></script>
+        <link rel="stylesheet" href="lib/reset.css" />
+    </head>
+    <body>
+        <style>
+        </style>
+
+
+        <div id="Legacy_compat_test"></div>
+        <div id="insideFill_Stroke_auto_test"></div>
+        <div id="textConfig_other_feature_test"></div>
+        <div id="insideFill_Stroke_hover_test"></div>
+        <div id="z_order_test"></div>
+        <div id="hover_style_disable"></div>
+        <div id="hover_style_remove"></div>
+
+
+        <!-- Utils ----------------------------------------------- -->
+        <script>
+            function createMaker(opt) {
+                opt = opt || {};
+                var xStart = opt.xCurr || 80;
+                var xCurr = xStart;
+                var yStart = opt.yCurr || 20;
+                var yCurr = yStart;
+                var xStep = opt.xStep || 100;
+                var yStep = opt.yStep || 65;
+                var colCount = 0;
+                var yMax = 0;
+                var maxCol = opt.maxCol || Infinity;
+                var children = [];
+
+                function createRectShape(width, height) {
+                    width = width || 20;
+                    height = height || 30;
+                    return {x: -width / 2, y: 0, width: width, height: height};
+                }
+
+                function makeGraphic(text, creators) {
+                    var y = yCurr;
+                    children.push({
+                        type: 'text', x: xCurr, y: y,
+                        style: {text: text, fill: '#900', align: 'center', fontSize: 10}
+                    });
+                    y += yStep;
+                    for (var i = 0; i < creators.length; i++) {
+                        children.push(creators[i](xCurr, y));
+                        y += yStep;
+                    }
+                    xCurr += xStep;
+
+                    yMax = Math.max(y, yMax);
+
+                    colCount++;
+                    if (colCount >= maxCol) {
+                        colCount = 0;
+                        xCurr = xStart;
+                        yCurr = yMax + yStep * 0.7;
+                    }
+                }
+
+                return {
+                    makeGraphic: makeGraphic,
+                    createRectShape: createRectShape,
+                    children: children
+                };
+            }
+
+            // opt: {globalColor, backgroundColor}
+            function createOption(renderItem, opt) {
+                return {
+                    animation: false,
+                    backgroundColor: opt && opt.backgroundColor,
+                    color: opt && opt.globalColor,
+                    xAxis: {axisLine: {lineStyle: {color: 'rgba(0,0,0,0.2)'}}, axisLabel: {show: false}, splitLine: {show: false}},
+                    yAxis: {axisLine: {lineStyle: {color: 'rgba(0,0,0,0.2)'}}, axisLabel: {show: false}, splitLine: {show: false}},
+                    grid: {left: 30},
+                    series: {
+                        type: 'custom',
+                        renderItem: renderItem,
+                        data: [[1, 1]]
+                    }
+                };
+            }
+        </script>
+        <!-- --------------------------------------------------- -->
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+
+            function renderItem(params, api) {
+                var maker = createMaker({xStep: 120, yStep: 60, maxCol: 5});
+                var makeGraphic = maker.makeGraphic;
+                var createRectShape = maker.createRectShape;
+
+                makeGraphic([
+                    'normal: green rect', 'inside orange'
+                ].join('\n'), [
+                    function (x, y) {
+                        return {
+                            type: 'rect', position: [x, y], shape: createRectShape(),
+                            style: {
+                                // textPosition not set but by default 'inside'
+                                text: 'legacy1', fill: 'green', textFill: 'orange'
+                            }
+                        };
+                    },
+                    function (x, y) {
+                        return {
+                            type: 'rect', x: x, y: y, shape: createRectShape(),
+                            style: {fill: 'green'},
+                            textContent: {style: {text: 'ec5_api', fill: 'orange'}},
+                            textConfig: {position: 'inside'}
+                        };
+                    }
+                ]);
+
+                makeGraphic([
+                    'normal: green rect', 'inside orange', 'text is number 0', 'should be displayed'
+                ].join('\n'), [
+                    function (x, y) {
+                        return {
+                            type: 'rect', position: [x, y], shape: createRectShape(),
+                            style: {
+                                // text is number 0, can be displayed
+                                text: 0, fill: 'green', textFill: 'orange'
+                            }
+                        };
+                    },
+                    function (x, y) {
+                        return {
+                            type: 'rect', x: x, y: y, shape: createRectShape(),
+                            style: {fill: 'green'},
+                            textContent: {style: {text: 0, fill: 'orange'}},
+                            textConfig: {position: 'inside'}
+                        };
+                    }
+                ]);
+
+                makeGraphic([
+                    'normal: green rect', 'inside white/bordered', 'hover: red rect', 'inside white/bordered'
+                ].join('\n'), [
+                    function (x, y) {
+                        return {
+                            type: 'rect', position: [x, y], shape: createRectShape(),
+                            style: api.style({text: 'legacy0'}),
+                            styleEmphasis: {fill: 'red'}
+                        };
+                    },
+                    function (x, y) {
+                        return {
+                            type: 'rect', position: [x, y], shape: createRectShape(),
+                            style: {
+                                text: 'legacy1', fill: 'green', textPosition: 'inside',
+                                textFill: '#fff', textStroke: 'green', textStrokeWidth: 2
+                            },
+                            styleEmphasis: {fill: 'red'}
+                        };
+                    },
+                    function (x, y) {
+                        return {
+                            type: 'rect', x: x, y: y, shape: createRectShape(),
+                            style: {fill: 'green'},
+                            textContent: {style: {text: 'ec5_api'}},
+                            textConfig: {position: 'inside'},
+                            emphasis: {style: {fill: 'red'}}
+                        };
+                    }
+                ]);
+
+                makeGraphic([
+                    'normal: green rect', 'bottom green', 'hover: red rect', 'bottom green'
+                ].join('\n'), [
+                    function (x, y) {
+                        return {
+                            type: 'rect', position: [x, y], shape: createRectShape(),
+                            style: api.style({text: 'legacy0', textPosition: 'bottom'}),
+                            styleEmphasis: {fill: 'red'}
+                        };
+                    },
+                    function (x, y) {
+                        return {
+                            type: 'rect', x: x, y: y, shape: createRectShape(),
+                            style: {fill: 'green'},
+                            textContent: {style: {text: 'ec5_api'}},
+                            textConfig: {position: 'bottom', outsideFill: 'green'},
+                            emphasis: {style: {fill: 'red'}}
+                        };
+                    }
+                ]);
+
+                makeGraphic([
+                    'green rect', 'normal: inside white/bordered', 'hover: inside red'
+                ].join('\n'), [
+                    function (x, y) {
+                        return {
+                            type: 'rect', position: [x, y], shape: createRectShape(),
+                            style: api.style({text: 'legacy0'}),
+                            styleEmphasis: {textFill: 'red'}
+                        };
+                    },
+                    function (x, y) {
+                        return {
+                            type: 'rect', position: [x, y], shape: createRectShape(),
+                            style: {
+                                text: 'legacy1', fill: 'green', textPosition: 'inside',
+                                textFill: '#fff', textStroke: 'green', textStrokeWidth: 2
+                            },
+                            styleEmphasis: {textFill: 'red'}
+                        };
+                    },
+                    function (x, y) {
+                        return {
+                            type: 'rect', x: x, y: y, shape: createRectShape(),
+                            style: {fill: 'green'},
+                            textContent: {
+                                style: {text: 'ec5_api'},
+                                emphasis: {style: {fill: 'red', stroke: 'green', lineWidth: 2}}
+                            },
+                            textConfig: {position: 'inside'}
+                        };
+                    }
+                ]);
+
+                makeGraphic([
+                    'green rect', 'normal: bottom red', 'hover: bottom blue'
+                ].join('\n'), [
+                    function (x, y) {
+                        return {
+                            type: 'rect', position: [x, y], shape: createRectShape(),
+                            style: api.style({text: 'legacy0', textPosition: 'bottom', textFill: 'red'}),
+                            styleEmphasis: {textFill: 'blue'}
+                        };
+                    },
+                    function (x, y) {
+                        return {
+                            type: 'rect', x: x, y: y, shape: createRectShape(),
+                            style: {fill: 'green'},
+                            textContent: {
+                                style: {text: 'ec5_api', fill: 'red'},
+                                emphasis: {style: {fill: 'blue'}}
+                            },
+                            textConfig: {position: 'bottom'}
+                        };
+                    }
+                ]);
+
+                makeGraphic(['green rect', 'normal: inside white/borded', 'hover: bottom'].join('\n'), [
+                    function (x, y) {
+                        return {
+                            type: 'rect', position: [x, y], shape: createRectShape(),
+                            style: api.style({text: 'legacy0'}),
+                            // Hover not compat to ec4 (to complicated)
+                            styleEmphasis: {textPosition: 'bottom'}
+                        };
+                    }
+                ]);
+
+                makeGraphic(['green rect', 'normal: inside red', 'hover: bottom red'].join('\n'), [
+                    function (x, y) {
+                        return {
+                            type: 'rect', position: [x, y], shape: createRectShape(),
+                            style: api.style({text: 'legacy0', textFill: 'red'}),
+                            styleEmphasis: {textPosition: 'bottom'}
+                        };
+                    }
+                ]);
+
+                makeGraphic(['green rect', 'normal: inside white', 'hover: bottom red'].join('\n'), [
+                    function (x, y) {
+                        return {
+                            type: 'rect', position: [x, y], shape: createRectShape(),
+                            style: api.style({text: 'legacy0', textFill: 'white'}),
+                            styleEmphasis: {textFill: 'red', textPosition: 'bottom'}
+                        };
+                    }
+                ]);
+
+                makeGraphic(['green rect', 'normal: inside white/bordered', 'hover: auto lift'].join('\n'), [
+                    function (x, y) {
+                        return {
+                            type: 'rect', position: [x, y], shape: createRectShape(),
+                            style: api.style({text: 'legacy0'})
+                        };
+                    },
+                    function (x, y) {
+                        return {
+                            type: 'rect', x: x, y: y, shape: createRectShape(),
+                            style: {fill: 'green'},
+                            textContent: {style: {text: 'ec5_api'}},
+                            textConfig: {position: 'inside'}
+                        };
+                    }
+                ]);
+
+                makeGraphic(['green rect', 'normal: bottom green', 'hover: auto lift'].join('\n'), [
+                    function (x, y) {
+                        return {
+                            type: 'rect', position: [x, y], shape: createRectShape(),
+                            style: api.style({text: 'legacy0', textPosition: 'bottom'})
+                        };
+                    },
+                    function (x, y) {
+                        return {
+                            type: 'rect', x: x, y: y, shape: createRectShape(),
+                            style: {fill: 'green'},
+                            textContent: {style: {text: 'ec5_api', fill: 'green'}},
+                            textConfig: {position: 'bottom'}
+                        };
+                    }
+                ]);
+
+                return {
+                    type: 'group',
+                    children: maker.children
+                };
+            }
+
+            var chart = testHelper.create(echarts, 'Legacy_compat_test', {
+                title: [
+                    'Legacy compat test',
+                    'Each column rects are the same effect implemented by',
+                    'legacy API and corresponding new API.',
+                    'So **each column** must be **the same result**',
+                    '(except text string and some ec5 enhancement).',
+                    '**Hover** also needs to be tested'
+                ],
+                height: 550,
+                option: createOption(renderItem, {globalColor: ['green']})
+            });
+        });
+        </script>
+
+
+
+
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            function renderItem() {
+                var maker = createMaker({maxCol: 5});
+                var makeGraphic = maker.makeGraphic;
+                var createRectShape = maker.createRectShape;
+
+                // opt for example:
+                // opt.type
+                // opt.shape
+                // opt.textConfig: {},
+                // opt.textContent: {}, if rich: textContent.style: {rich: {i: xxx}}
+                // opt.emphasis: {}
+                // opt.extraTitle: string[]
+                function makeTest(opt) {
+                    var isRich = opt.textContent.style.rich;
+                    var type = opt.type || 'rect';
+                    var shape = opt.shape || createRectShape(40, 120);
+                    var title = ['green rect', (isRich ? 'rich text' : 'plain text')];
+                    opt.extraTitle && (title = title.concat(opt.extraTitle));
+                    makeGraphic(title.join('\n'), [
+                        function (x, y) {
+                            var result = {
+                                type: type, x: x, y: y, shape: shape, style: {fill: 'green'},
+                                textConfig: opt.textConfig,
+                                textContent: opt.textContent
+                            }
+                            if (opt.emphasis) {
+                                result.emphasis = opt.emphasis;
+                            }
+                            return result;
+                        }
+                    ]);
+                }
+
+                makeTest({
+                    textConfig: {position: 'insideTop', rotation: -0.5 * Math.PI},
+                    textContent: {style: {
+                        text: '90 rotated text align with:\nv: top h: middle\ndefault distance',
+                        align: 'left', verticalAlign: 'middle'
+                    }}
+                });
+
+                makeTest({
+                    textConfig: {position: 'insideTop', rotation: -0.5 * Math.PI, distance: 0},
+                    textContent: {style: {
+                        text: '90 rotated text align with:\n v: top h: middle\nno distance',
+                        align: 'left', verticalAlign: 'middle'
+                    }}
+                });
+
+                makeTest({
+                    textConfig: {position: 'insideTop', rotation: -0.5 * Math.PI, distance: 0, offset: [0, -25]},
+                    textContent: {style: {
+                        text: '90 rotated text outside right rect\nalign top',
+                        fill: '#700',
+                        align: 'left', verticalAlign: 'bottom'
+                    }}
+                });
+
+                makeTest({
+                    textConfig: {position: 'insideTop', rotation: -0.5 * Math.PI},
+                    textContent: {style: {
+                        text: '90 rotated text align with:\n{i|v: top h: middle}\n{a|align right}',
+                        align: 'left', verticalAlign: 'middle',
+                        rich: {
+                            i: {fontSize: 20, fill: 'orange'},
+                            a: {align: 'right'}
+                        }
+                    }}
+                });
+
+                makeTest({
+                    extraTitle: ['emphasis:\nalign/verticalAlign rollback'],
+                    textConfig: {position: 'left'},
+                    textContent: {
+                        style: {
+                            text: '--------\nalign\nright\n----',
+                            fill: '#700'
+                        }
+                    },
+                    emphasis: {
+                        textConfig: {position: null}
+                    }
+                });
+
+                makeTest({
+                    type: 'circle',
+                    shape: {cx: 0, cy: 20, r: 30},
+                    textConfig: {
+                        position: 'right',
+                    },
+                    textContent: {
+                        rotation: -0.3 * Math.PI,
+                        originX: -35,
+                        style: {
+                            text: 'Rotate, outside, origin is center',
+                            fontSize: 20,
+                            fill: '#700',
+                            align: 'left',
+                            verticalAlign: 'middle'
+                        }
+                    }
+                });
+
+                makeTest({
+                    type: 'circle',
+                    shape: {cx: 0, cy: 20, r: 30},
+                    textConfig: {
+                        position: 'inside', offset: [35, 0]
+                    },
+                    textContent: {
+                        rotation: -0.3 * Math.PI,
+                        style: {
+                            text: 'Rotate, outside, origin is center',
+                            fontSize: 20,
+                            fill: '#700',
+                            align: 'left',
+                            verticalAlign: 'middle'
+                        }
+                    }
+                });
+
+                return {
+                    type: 'group',
+                    children: maker.children
+                };
+            }
+
+            var chart = testHelper.create(echarts, 'textConfig_other_feature_test', {
+                title: [
+                    'textConfig other feature test'
+                ],
+                option: createOption(renderItem),
+                // recordCanvas: true,
+                height: 800
+            });
+        });
+        </script>
+
+
+
+
+
+
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+            function renderItem() {
+                var maker = createMaker({maxCol: 6});
+                var makeGraphic = maker.makeGraphic;
+                var createRectShape = maker.createRectShape;
+
+                // opt for example:
+                // opt.textConfig: {},
+                // opt.textContentStyle: {}, if rich: textContentStyle: {rich: {i: xxx}}
+                // opt.expect: [ {fill: 'white', stroke: 'green'},  {fill: 'white', stroke: 'green'} ]
+                function makeTest(opt) {
+                    var expect0 = opt.expect[0];
+                    var expect1 = opt.expect[1];
+                    var isRich = opt.textContentStyle.rich;
+                    makeGraphic(['green rect', (isRich ? 'rich text' : 'plain text')].join('\n'), [
+                        function (x, y) {
+                            var text = [
+                                isRich ? '{i|inside}' : 'inside',
+                                'fill: ' + expect0.fill,
+                                'stroke: ' + expect0.stroke
+                            ].join('\n');
+                            return {
+                                type: 'rect', x: x, y: y, shape: createRectShape(), style: {fill: 'green'},
+                                textConfig: echarts.util.extend({position: 'inside'}, opt.textConfig),
+                                textContent: {
+                                    style: echarts.util.extend({text: text}, opt.textContentStyle)
+                                }
+                            }
+                        },
+                        function (x, y) {
+                            var text = [
+                                isRich ? '{i|bottom}' : 'bottom',
+                                'fill: ' + expect1.fill,
+                                'stroke: ' + expect1.stroke
+                            ].join('\n');
+                            return {
+                                type: 'rect', x: x, y: y, shape: createRectShape(), style: {fill: 'green'},
+                                textConfig: echarts.util.extend({position: 'bottom'}, opt.textConfig),
+                                textContent: {
+                                    style: echarts.util.extend({text: text}, opt.textContentStyle)
+                                }
+                            }
+                        }
+                    ]);
+                }
+
+                makeTest({
+                    textConfig: {},
+                    textContentStyle: {},
+                    expect: [
+                        {fill: 'white', stroke: 'green'},
+                        {fill: 'black', stroke: 'none'}
+                    ]
+                });
+
+                makeTest({
+                    textConfig: {},
+                    textContentStyle: {backgroundColor: '#aaa'},
+                    expect: [
+                        {fill: 'white', stroke: 'none'},
+                        {fill: 'black', stroke: 'none'}
+                    ]
+                });
+
+                makeTest({
+                    textConfig: {},
+                    textContentStyle: {borderColor: '#aaa', borderWidth: 2, padding: 5},
+                    expect: [
+                        {fill: 'white', stroke: 'none'},
+                        {fill: 'black', stroke: 'none'}
+                    ]
+                });
+
+                makeTest({
+                    textConfig: {},
+                    textContentStyle: {rich: {i: {fontSize: 30}}},
+                    expect: [
+                        {fill: 'white', stroke: 'green'},
+                        {fill: 'black', stroke: 'none'}
+                    ]
+                });
+
+                makeTest({
+                    textConfig: {},
+                    textContentStyle: {rich: {i: {backgroundColor: '#aaa'}}},
+                    expect: [
+                        {fill: 'white', stroke: 'none'},
+                        {fill: 'black', stroke: 'none'}
+                    ]
+                });
+
+                makeTest({
+                    textConfig: {},
+                    textContentStyle: {rich: {i: {borderColor: '#aaa', borderWidth: 2, fontSize: 20}}},
+                    expect: [
+                        {fill: 'white', stroke: 'none'},
+                        {fill: 'black', stroke: 'green'}
+                    ]
+                });
+
+                makeTest({
+                    textConfig: {},
+                    textContentStyle: {fill: 'orange'},
+                    expect: [
+                        {fill: 'orange', stroke: 'none'},
+                        {fill: 'orange', stroke: 'none'}
+                    ]
+                });
+
+                makeTest({
+                    textConfig: {},
+                    textContentStyle: {fill: 'orange', stroke: 'blue', lineWidth: 2},
+                    expect: [
+                        {fill: 'orange', stroke: 'blue'},
+                        {fill: 'orange', stroke: 'blue'}
+                    ]
+                });
+
+                makeTest({
+                    textConfig: {},
+                    textContentStyle: {stroke: 'blue', lineWidth: 2},
+                    expect: [
+                        {fill: 'white', stroke: 'blue'},
+                        {fill: 'black', stroke: 'blue'}
+                    ]
+                });
+
+                makeTest({
+                    textConfig: {insideFill: 'yellow'},
+                    textContentStyle: {},
+                    expect: [
+                        {fill: 'yellow', stroke: 'green'},
+                        {fill: 'black', stroke: 'none'}
+                    ]
+                });
+
+                makeTest({
+                    textConfig: {insideFill: 'yellow'},
+                    textContentStyle: {fill: 'orange'},
+                    expect: [
+                        {fill: 'orange', stroke: 'none'},
+                        {fill: 'orange', stroke: 'none'}
+                    ]
+                });
+
+                makeTest({
+                    textConfig: {insideFill: 'yellow', insideStroke: 'blue'},
+                    textContentStyle: {},
+                    expect: [
+                        {fill: 'yellow', stroke: 'blue'},
+                        {fill: 'black', stroke: 'none'}
+                    ]
+                });
+
+                makeTest({
+                    textConfig: {insideFill: 'yellow', insideStroke: 'blue'},
+                    textContentStyle: {fill: 'pink'},
+                    expect: [
+                        {fill: 'pink', stroke: 'blue'},
+                        {fill: 'pink', stroke: 'none'}
+                    ]
+                });
+
+                makeTest({
+                    textConfig: {insideStroke: 'blue'},
+                    textContentStyle: {},
+                    expect: [
+                        {fill: 'white', stroke: 'blue'},
+                        {fill: 'black', stroke: 'none'}
+                    ]
+                });
+
+                makeTest({
+                    textConfig: {outsideFill: 'blue', outsideStroke: 'red'},
+                    textContentStyle: {},
+                    expect: [
+                        {fill: 'white', stroke: 'green'},
+                        {fill: 'blue', stroke: 'red'}
+                    ]
+                });
+                makeTest({
+                    textConfig: {outsideFill: 'blue', outsideStroke: 'red'},
+                    textContentStyle: {fill: 'pink'},
+                    expect: [
+                        {fill: 'pink', stroke: 'none'},
+                        {fill: 'pink', stroke: 'red'}
+                    ]
+                });
+
+                return {
+                    type: 'group',
+                    children: maker.children
+                };
+            }
+
+            var chart = testHelper.create(echarts, 'insideFill_Stroke_auto_test', {
+                title: [
+                    'insideFill/insideStroke outsideFill/outsideStroke auto rule test'
+                ],
+                option: createOption(renderItem, {backgroundColor: '#ddd'}),
+                height: 800
+            });
+        });
+        </script>
+
+
+
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+
+            function renderItem(params, api) {
+                var maker = createMaker({xCurr: 150, xStep: 150, yStep: 90});
+                var makeGraphic = maker.makeGraphic;
+                var createRectShape = maker.createRectShape;
+
+                makeGraphic(['insideFill/Stroke not set', 'normal: white/bordered'].join('\n'), [
+                    function (x, y) {
+                        return {
+                            type: 'rect', x: x, y: y, shape: createRectShape(),
+                            style: {fill: 'green'},
+                            textContent: {
+                                type: 'text', silent: true, style: {
+                                    text: 'if hover,\ntop black'
+                                }
+                            },
+                            textConfig: {position: 'inside'},
+                            emphasis: {
+                                textConfig: {position: 'top'}
+                            }
+                        };
+                    },
+                    function (x, y) {
+                        return {
+                            type: 'rect', x: x, y: y, shape: createRectShape(),
+                            style: {fill: 'green'},
+                            textContent: {type: 'text', silent: true, style: {text: 'if hover,\nright green'}},
+                            textConfig: {
+                                position: 'inside', outsideFill: 'green'
+                            },
+                            emphasis: {
+                                textConfig: {position: 'right'}
+                            }
+                        };
+                    },
+                    function (x, y) {
+                        return {
+                            type: 'rect', x: x, y: y, shape: createRectShape(),
+                            style: {fill: 'green'},
+                            textContent: {
+                                silent: true, style: {text: 'if hover,\nbottom\nlarge bordered red'},
+                                emphasis: {style: {fontSize: 20}}
+                            },
+                            textConfig: {
+                                position: 'inside', outsideFill: 'green', outsideStroke: 'red'
+                            },
+                            emphasis: {
+                                textConfig: {position: 'bottom'}
+                            }
+                        };
+                    },
+                    function (x, y) {
+                        return {
+                            type: 'rect', x: x, y: y, shape: createRectShape(),
+                            style: {fill: 'green'},
+                            textContent: {
+                                silent: true,
+                                style: {
+                                    text: 'rich text\nif hover,\nbottom black\n{r|normal orange\nhover red}',
+                                    rich: {r: {fontSize: 16, fill: 'orange'}},
+                                },
+                                emphasis: {
+                                    style: {
+                                        rich: {r: {fill: 'red'}}
+                                    }
+                                }
+                            },
+                            textConfig: {position: 'inside'},
+                            emphasis: {
+                                textConfig: {position: 'bottom'}
+                            }
+                        };
+                    }
+                ]);
+
+                makeGraphic(['green rect'].join('\n'), [
+                    function (x, y) {
+                        return {
+                            type: 'rect', x: x, y: y, shape: createRectShape(),
+                            style: {fill: 'green'},
+                            textContent: {silent: true, style: {
+                                text: 'normal:\ninside yellow/bordered\nhover:\ntop black'
+                            }},
+                            textConfig: {position: 'inside', insideFill: 'yellow'},
+                            emphasis: {
+                                textConfig: {position: 'top'}
+                            }
+                        };
+                    },
+                    function (x, y) {
+                        return {
+                            type: 'rect', x: x, y: y, shape: createRectShape(),
+                            style: {fill: 'green'},
+                            textContent: {
+                                silent: true,
+                                style: {text: 'normal:\ninside white/bordered\nhover:\nright green'}
+                            },
+                            textConfig: {
+                                position: 'inside', outsideFill: 'green'
+                            },
+                            emphasis: {
+                                textConfig: {position: 'right'}
+                            }
+                        };
+                    }
+                ]);
+
+                return {
+                    type: 'group',
+                    children: maker.children
+                };
+            }
+
+            var chart = testHelper.create(echarts, 'insideFill_Stroke_hover_test', {
+                title: [
+                    'insideFill/Stroke & hover test. Please **hover any of them**'
+                ],
+                option: createOption(renderItem),
+                height: 600
+            });
+        });
+        </script>
+
+
+
+
+
+
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+
+            function renderItem(params, api) {
+                return {
+                    type: 'group',
+                    children: [{
+                        type: 'circle',
+                        shape: {cx: 100, cy: 100, r: 50},
+                        style: {fill: 'red'},
+                        z2: 100,
+                        // textContent do not set z2, but auto follow the circle z2.
+                        textContent: {
+                            type: 'text',
+                            style: {
+                                text: 'normal: {red|red} is over {green|green}.\nHover me',
+                                // stroke: '#fff', lineWidth: 2,
+                                rich: {
+                                    red: {fill: 'red', stroke: '#fff', lineWidth: 2, fontSize: 20},
+                                    green: {fill: 'green', stroke: '#fff', lineWidth: 2, fontSize: 20}
+                                },
+                            },
+                            silent: true,
+                            emphasis: {
+                                style: {
+                                    text: 'emphasis: {green|green} over {red|red}\nText {below|below} green',
+                                    rich: {
+                                        below: {fontSize: 40}
+                                    }
+                                }
+                            }
+                        },
+                        textConfig: {
+                            position: 'inside'
+                        }
+                    }, {
+                        type: 'circle',
+                        shape: {cx: 100, cy: 140, r: 40},
+                        style: {fill: 'green'},
+                        // textContent do not set z2, but auto follow the circle z2.
+                        textContent: {
+                            type: 'text',
+                            style: {
+                                text: 'text should be always\n{over|over} all circles.',
+                                rich: {over: {fontSize: 30}}
+                            },
+                            silent: true
+                        },
+                        textConfig: {
+                            position: 'inside'
+                        },
+                        z2: 80,
+                        emphasis: {
+                            z2: 110
+                        }
+
+                    }, {
+                        type: 'circle',
+                        shape: {cx: 300, cy: 100, r: 50},
+                        style: {fill: 'red'},
+                        z2: 100,
+                        // textContent do not set z2, but auto follow the circle z2.
+                        textContent: {
+                            type: 'text',
+                            style: {
+                                text: 'normal: {red|red} is over {green|green}.\nHover me',
+                                // stroke: '#fff', lineWidth: 2,
+                                rich: {
+                                    red: {fill: 'red', stroke: '#fff', lineWidth: 2, fontSize: 20},
+                                    green: {fill: 'green', stroke: '#fff', lineWidth: 2, fontSize: 20}
+                                },
+                            },
+                            silent: true,
+                            emphasis: {
+                                style: {
+                                    text: 'emphasis: {red|red} over {green|green}\nText {below|over} green',
+                                    rich: {
+                                        below: {fontSize: 40}
+                                    }
+                                }
+                            }
+                        },
+                        textConfig: {
+                            position: 'inside'
+                        }
+                    }, {
+                        type: 'circle',
+                        shape: {cx: 300, cy: 140, r: 40},
+                        style: {fill: 'green'},
+                        // textContent do not set z2, but auto follow the circle z2.
+                        textContent: {
+                            type: 'text',
+                            style: {
+                                text: 'text should be always\n{over|over} all circles.',
+                                rich: {over: {fontSize: 30}}
+                            },
+                            silent: true,
+                            emphasis: {
+                                z2: 110
+                            }
+                        },
+                        textConfig: {
+                            position: 'inside'
+                        },
+                        z2: 80
+
+                    }]
+                };
+            }
+
+            var chart = testHelper.create(echarts, 'z_order_test', {
+                title: [
+                    'z order test. Please **hover any of them**.'
+                ],
+                option: createOption(renderItem)
+            });
+        });
+        </script>
+
+
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+
+            var option = {
+                xAxis: {scale: true, min: 0.75, max: 3},
+                yAxis: {},
+                series: [{
+                    type: 'custom',
+                    renderItem: function (params, api) {
+                        var shape0 = {x: 0, y: 30, width: 100, height: 50};
+                        var shape1 = {x: 30, y: 50, width: 100, height: 50};
+                        var shape2 = {x: 60, y: 70, width: 100, height: 50};
+                        var position = api.coord([api.value(0), api.value(1)]);
+                        var name = api.value(2);
+
+                        var map = {
+                            legacy: {
+                                type: 'group',
+                                x: position[0],
+                                y: position[1],
+                                children: [{
+                                    type: 'rect',
+                                    shape: shape0,
+                                    style: {fill: '#333'}
+                                }, {
+                                    type: 'rect',
+                                    shape: shape1,
+                                    style: {
+                                        fill: '#555',
+                                        text: 'has inner text',
+                                        textFill: 'white',
+                                        textPostion: 'insideTop'
+                                    },
+                                    styleEmphasis: {textFill: 'yellow'}
+                                }, {
+                                    type: 'rect',
+                                    shape: shape2,
+                                    style: {fill: '#500'},
+                                    styleEmphasis: false
+                                }]
+                            },
+                            ec5: {
+                                type: 'group',
+                                x: position[0],
+                                y: position[1],
+                                children: [{
+                                    type: 'rect',
+                                    shape: shape0,
+                                    style: {fill: '#333'},
+                                }, {
+                                    type: 'rect',
+                                    shape: shape1,
+                                    style: {fill: '#555', text: 'has inner text'},
+                                    textContent: {
+                                        style: {fill: 'white'},
+                                        emphasis: {style: {fill: 'yellow'}}
+                                    },
+                                    textConfig: {position: 'insideTop'}
+                                }, {
+                                    type: 'rect',
+                                    shape: shape2,
+                                    style: {fill: '#500'},
+                                    emphasis: {
+                                        // set false to disable lift color and z2.
+                                        style: false
+                                    }
+                                }]
+                            }
+                        };
+
+                        return map[name];
+                    },
+                    data: [[1, 1, 'legacy'], [2, 1, 'ec5']]
+                }]
+            };
+
+            var chart = testHelper.create(echarts, 'hover_style_disable', {
+                title: [
+                    'Hover style disable: hover each group of elements,',
+                    'all of the elements should lift color and z2 **except the red rect**'
+                ],
+                height: 300,
+                option: option
+            });
+        });
+        </script>
+
+
+
+
+
+
+
+
+        <script>
+        require(['echarts'], function (echarts) {
+
+            var option = {
+                xAxis: {scale: true, min: 0.75, max: 3},
+                yAxis: {},
+                series: [{
+                    type: 'custom',
+                    renderItem: function (params, api) {
+                        var shape0 = {x: 00, y: 30, width: 100, height: 50};
+                        var shape1 = {x: 30, y: 50, width: 100, height: 50};
+                        var shape2 = {x: 60, y: 70, width: 100, height: 50};
+                        var position = api.coord([api.value(0), api.value(1)]);
+                        var name = api.value(2);
+                        var useHover = !!api.value(3);
+                        var map = {
+                            legacy: {
+                                type: 'group',
+                                x: position[0],
+                                y: position[1],
+                                children: [{
+                                    type: 'rect',
+                                    shape: shape0,
+                                    style: api.style({fill: '#222', text: 'legacy'}),
+                                    styleEmphasis: useHover ? {fill: 'red', textFill: 'yellow'} : false
+                                }]
+                            },
+                            ec5: {
+                                type: 'group',
+                                x: position[0],
+                                y: position[1],
+                                children: [{
+                                    type: 'rect',
+                                    shape: shape0,
+                                    style: {fill: '#222'},
+                                    textContent: {
+                                        style: {text: 'ec5_api'},
+                                        emphasis: {style: useHover ? {fill: 'yellow'} : false}
+                                    },
+                                    textConfig: {position: 'inside'},
+                                    emphasis: {style: useHover ? {fill: 'red'} : false}
+                                }]
+                            }
+                        };
+
+                        return map[name];
+                    },
+                    data: [[1, 1, 'legacy', 1], [2, 1, 'ec5', 1]]
+                }]
+            };
+
+            var chart = testHelper.create(echarts, 'hover_style_remove', {
+                title: [
+                    'Hover style remove test: ',
+                    '(1) Hover each: become **red rect, yellow text**',
+                    '(2) Click "disable hover style", elements should more to right a bit.',
+                    '(3) Hover each again: should no hover style',
+                    '(4) Click "enable hover style", elements should more to left a bit.',
+                    '(5) Check whether resume to (1)',
+                ],
+                height: 300,
+                option: option,
+                buttons: [{
+                    text: 'disable hover style',
+                    onclick: function () {
+                        chart.setOption({
+                            xAxis: {scale: true, min: 0.75, max: 7},
+                            series: {
+                                type: 'custom',
+                                data: [[3, 1, 'legacy', 0], [6, 1, 'ec5', 0]]
+                            }
+                        })
+                    }
+                }, {
+                    text: 'enable hover style',
+                    onclick: function () {
+                        chart.setOption({
+                            xAxis: {scale: true, min: 0.75, max: 3},
+                            series: {
+                                type: 'custom',
+                                data: [[1, 1, 'legacy', 1], [2, 1, 'ec5', 1]]
+                            }
+                        })
+                    }
+                }]
+            });
+        });
+        </script>
+
+
+
+
+    </body>
+</html>
+
diff --git a/test/hoverStyle.html b/test/hoverStyle.html
index 6a5bca7..245068c 100644
--- a/test/hoverStyle.html
+++ b/test/hoverStyle.html
@@ -680,7 +680,8 @@ under the License.
                         // silent: true,
                         label: {
                             show: true,
-                            silent: true,
+                            // silent: true,
+                            position: 'top'
                         },
                         itemStyle: {
                             color: 'green',


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