You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by sh...@apache.org on 2020/09/30 09:34:47 UTC

[incubator-echarts] 01/01: Merge branch 'master' into next-merge-master

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

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

commit 2f32ac609d5d29280c22d7abe1786e90bfa7133b
Merge: 3f616bb fd8417b
Author: pissang <bm...@gmail.com>
AuthorDate: Wed Sep 30 17:33:44 2020 +0800

    Merge branch 'master' into next-merge-master
    
    # Conflicts:
    #	dist/echarts-en.common.js
    #	dist/echarts-en.common.min.js
    #	dist/echarts-en.js
    #	dist/echarts-en.js.map
    #	dist/echarts-en.min.js
    #	dist/echarts-en.simple.js
    #	dist/echarts-en.simple.min.js
    #	dist/echarts.common.js
    #	dist/echarts.common.min.js
    #	dist/echarts.js
    #	dist/echarts.js.map
    #	dist/echarts.min.js
    #	dist/echarts.simple.js
    #	dist/echarts.simple.min.js
    #	dist/extension/bmap.js
    #	dist/extension/bmap.js.map
    #	dist/extension/bmap.min.js
    #	dist/extension/dataTool.js.map
    #	extension-src/bmap/BMapCoordSys.ts
    #	extension-src/bmap/BMapView.ts
    #	extension/bmap/BMapCoordSys.js
    #	extension/bmap/BMapModel.js
    #	extension/bmap/BMapView.js
    #	package-lock.json
    #	package.json
    #	src/chart/bar/BarView.js
    #	src/chart/bar/PictorialBarSeries.js
    #	src/chart/funnel/funnelLayout.ts
    #	src/chart/graph/GraphSeries.js
    #	src/chart/graph/circularLayoutHelper.ts
    #	src/chart/graph/forceLayout.js
    #	src/chart/graph/simpleLayoutHelper.ts
    #	src/chart/helper/Line.js
    #	src/chart/helper/Symbol.js
    #	src/chart/helper/createGraphFromNodeEdge.ts
    #	src/chart/lines/LinesSeries.ts
    #	src/chart/map/MapSeries.js
    #	src/chart/radar/RadarSeries.js
    #	src/chart/radar/RadarView.js
    #	src/chart/sunburst/SunburstPiece.js
    #	src/chart/sunburst/SunburstSeries.ts
    #	src/chart/themeRiver/ThemeRiverSeries.js
    #	src/chart/tree/TreeSeries.js
    #	src/chart/tree/TreeView.js
    #	src/chart/treemap/TreemapSeries.ts
    #	src/chart/treemap/treemapLayout.ts
    #	src/chart/treemap/treemapVisual.js
    #	src/component/marker/MarkAreaView.js
    #	src/component/marker/MarkLineView.js
    #	src/component/marker/MarkPointView.js
    #	src/component/marker/MarkerModel.js
    #	src/component/timeline/SliderTimelineView.js
    #	src/component/title.ts
    #	src/component/toolbox/feature/DataView.js
    #	src/component/toolbox/feature/DataZoom.js
    #	src/component/toolbox/feature/SaveAsImage.js
    #	src/component/tooltip/TooltipContent.js
    #	src/component/tooltip/TooltipRichContent.js
    #	src/component/tooltip/TooltipView.js
    #	src/coord/axisHelper.js
    #	src/coord/geo/GeoModel.js
    #	src/data/Graph.js
    #	src/data/List.js
    #	src/data/Tree.js
    #	src/echarts.js
    #	src/layout/barPolar.ts
    #	src/processor/dataSample.ts
    #	src/stream/Scheduler.js
    #	src/visual/VisualMapping.js
    #	test/timeline-dynamic-series.html

 .asf.yaml                                    |  13 +
 .gitattributes                               |   3 +
 .huskyrc                                     |   5 +
 .lgtm.yml                                    |  12 +
 README.md                                    |   8 +-
 package.json                                 |   1 +
 src/chart/bar/BarView.ts                     |  92 +++--
 src/chart/funnel/FunnelSeries.ts             |  10 +-
 src/chart/funnel/funnelLayout.ts             | 233 ++++++++---
 src/chart/graph/GraphSeries.ts               |  11 +-
 src/chart/graph/circularLayoutHelper.ts      |  10 +-
 src/chart/graph/edgeVisual.ts                |   8 -
 src/chart/graph/forceLayout.ts               |   8 +-
 src/chart/graph/simpleLayout.ts              |   2 +-
 src/chart/graph/simpleLayoutHelper.ts        |  17 +-
 src/chart/helper/Line.ts                     |  41 +-
 src/chart/helper/multipleGraphEdgeHelper.ts  | 230 +++++++++++
 src/chart/lines/LinesSeries.ts               |   5 +-
 src/chart/radar/RadarView.ts                 |   4 +-
 src/chart/sunburst/SunburstSeries.ts         |  24 +-
 src/chart/themeRiver/ThemeRiverSeries.ts     |  67 ++-
 src/chart/tree/TreeSeries.ts                 |   2 +-
 src/chart/treemap/TreemapSeries.ts           |  16 +-
 src/chart/treemap/treemapLayout.ts           |   4 +-
 src/chart/treemap/treemapVisual.ts           |  31 +-
 src/component/axisPointer/axisTrigger.ts     |   2 +-
 src/component/legend/ScrollableLegendView.ts |   2 +-
 src/component/marker/MarkAreaView.ts         |  73 +++-
 src/component/marker/MarkLineView.ts         |   3 +
 src/component/marker/MarkPointView.ts        |   7 +-
 src/component/timeline/SliderTimelineView.ts |  17 +-
 src/component/toolbox/feature/DataView.ts    |  32 +-
 src/component/toolbox/feature/DataZoom.ts    |  18 +-
 src/component/toolbox/feature/SaveAsImage.ts |   3 +-
 src/component/visualMap/VisualMapModel.ts    |   2 +-
 src/coord/axisDefault.ts                     |   2 +-
 src/coord/axisHelper.ts                      |   6 +-
 src/coord/geo/GeoModel.ts                    |   1 -
 src/data/Graph.ts                            |   4 -
 src/data/List.ts                             |   2 +-
 src/data/Tree.ts                             |  29 +-
 src/echarts.ts                               |   2 +-
 src/layout/barPolar.ts                       |   6 +-
 src/stream/Scheduler.ts                      |   2 +-
 src/visual/commonVisualTypes.ts              |   2 +
 test/axisLabel.html                          |  11 +-
 test/bar-polar-stack.html                    | 218 ++++++++++
 test/bmap-mapOptions.html                    | 530 ++++++++++++++++++++++++
 test/bmap2.html                              | 592 +++++++++++++++++++++++++++
 test/data/aqi/aqi-beijing.json               |   1 +
 test/dataZoom-toolbox.html                   |   8 +-
 test/dataset-charts.html                     |  36 +-
 test/funnel.html                             |  11 +-
 test/geo-labelFormatter.html                 | 364 ++++++++++++++++
 test/graph-mulitple-edges.html               | 357 ++++++++++++++++
 test/lines-mergeOption.html                  | 103 +++++
 test/markArea.html                           |  21 +-
 test/markLine-symbolRotate.html              | 187 +++++++++
 test/markPoint.html                          |  11 +-
 test/pie-dataView.html                       | 122 ++++++
 test/radar.html                              |   9 +-
 test/sunburst-visualMap.html                 |  24 +-
 test/themeRiver3.html                        | 154 +++++++
 test/timeline-dynamic-series.html            |  10 +-
 test/tooltip-rich.html                       |  14 +-
 test/tooltip-textStyle.html                  | 455 ++++++++++++++++++++
 test/tooltip-windowResize.html               | 383 +++++++++++++++++
 test/tree-image2.html                        |  98 +++++
 test/treemap-simple2.html                    | 137 +++++++
 69 files changed, 4628 insertions(+), 300 deletions(-)

diff --cc package.json
index 113afe5,8616612..ac87f3e
--- a/package.json
+++ b/package.json
@@@ -40,19 -31,21 +40,20 @@@
    },
    "devDependencies": {
      "@babel/core": "7.3.4",
 -    "@babel/helper-module-transforms": "7.0.0-beta.31",
 -    "@babel/helper-simple-access": "7.0.0-beta.31",
 -    "@babel/template": "7.0.0-beta.31",
 -    "@babel/types": "7.0.0-beta.31",
 -    "assert": "1.4.1",
 +    "@babel/types": "^7.10.5",
 +    "@microsoft/api-extractor": "7.7.2",
 +    "@typescript-eslint/eslint-plugin": "^2.15.0",
 +    "@typescript-eslint/parser": "^2.18.0",
++    "husky": "^4.2.5",
      "canvas": "^2.6.0",
 +    "chalk": "^3.0.0",
 +    "chokidar": "^3.4.0",
      "commander": "2.11.0",
 -    "coordtransform": "2.0.2",
 -    "escodegen": "1.8.0",
 +    "esbuild": "^0.4.1",
      "eslint": "6.3.0",
 -    "esprima": "2.7.2",
 -    "estraverse": "4.1.1",
      "fs-extra": "0.26.7",
      "glob": "7.0.0",
 -    "husky": "^4.2.5",
 +    "globby": "11.0.0",
      "jest": "^24.9.0",
      "jest-canvas-mock": "^2.2.0",
      "jsdom": "^15.2.1",
diff --cc src/chart/bar/BarView.ts
index 2d48aaa,0000000..cd00df6
mode 100644,000000..100644
--- a/src/chart/bar/BarView.ts
+++ b/src/chart/bar/BarView.ts
@@@ -1,1088 -1,0 +1,1118 @@@
 +/*
 +* 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 Path, {PathProps} from 'zrender/src/graphic/Path';
 +import Group from 'zrender/src/graphic/Group';
 +import {extend, map, defaults, each} from 'zrender/src/core/util';
 +import type {RectLike} from 'zrender/src/core/BoundingRect';
 +import {
 +    Rect,
 +    Sector,
 +    updateProps,
 +    initProps,
 +    updateLabel,
 +    initLabel,
 +    removeElementWithFadeOut
 +} from '../../util/graphic';
 +import { getECData } from '../../util/innerStore';
 +import { enableHoverEmphasis, setStatesStylesFromModel } from '../../util/states';
 +import { setLabelStyle, getLabelStatesModels, labelInner } from '../../label/labelStyle';
 +import {throttle} from '../../util/throttle';
 +import {createClipPath} from '../helper/createClipPathFromCoordSys';
 +import Sausage from '../../util/shape/sausage';
 +import ChartView from '../../view/Chart';
 +import List, {DefaultDataVisual} from '../../data/List';
 +import GlobalModel from '../../model/Global';
 +import ExtensionAPI from '../../ExtensionAPI';
 +import {
 +    StageHandlerProgressParams,
 +    ZRElementEvent,
 +    ColorString,
 +    OrdinalSortInfo,
 +    Payload,
 +    OrdinalNumber,
 +    ParsedValue,
 +    ECElement
 +} from '../../util/types';
 +import BarSeriesModel, {BarSeriesOption, BarDataItemOption} from './BarSeries';
 +import type Axis2D from '../../coord/cartesian/Axis2D';
 +import type Cartesian2D from '../../coord/cartesian/Cartesian2D';
++import type Polar from '../../coord/polar/Polar';
 +import type Model from '../../model/Model';
 +import { isCoordinateSystemType } from '../../coord/CoordinateSystem';
 +import { getDefaultLabel, getDefaultInterpolatedLabel } from '../helper/labelHelper';
 +import OrdinalScale from '../../scale/Ordinal';
 +import SeriesModel from '../../model/Series';
 +import {AngleAxisModel, RadiusAxisModel} from '../../coord/polar/AxisModel';
 +import CartesianAxisModel from '../../coord/cartesian/AxisModel';
 +import {LayoutRect} from '../../util/layout';
 +import {EventCallback} from 'zrender/src/core/Eventful';
 +
 +const BAR_BORDER_WIDTH_QUERY = ['itemStyle', 'borderWidth'] as const;
 +const BAR_BORDER_RADIUS_QUERY = ['itemStyle', 'borderRadius'] as const;
 +const _eventPos = [0, 0];
 +
 +const mathMax = Math.max;
 +const mathMin = Math.min;
 +
 +type CoordSysOfBar = BarSeriesModel['coordinateSystem'];
 +type RectShape = Rect['shape'];
 +type SectorShape = Sector['shape'];
 +
 +type SectorLayout = SectorShape;
 +type RectLayout = RectShape;
 +
 +type BarPossiblePath = Sector | Rect | Sausage;
 +
++type CartesianCoordArea = ReturnType<Cartesian2D['getArea']>;
++type PolarCoordArea = ReturnType<Polar['getArea']>;
++
 +function getClipArea(coord: CoordSysOfBar, data: List) {
-     let coordSysClipArea;
++    let coordSysClipArea = coord.getArea && coord.getArea();
 +    if (isCoordinateSystemType<Cartesian2D>(coord, 'cartesian2d')) {
-         coordSysClipArea = coord.getArea && coord.getArea();
 +        const baseAxis = coord.getBaseAxis();
 +        // When boundaryGap is false or using time axis. bar may exceed the grid.
 +        // We should not clip this part.
 +        // See test/bar2.html
 +        if (baseAxis.type !== 'category' || !baseAxis.onBand) {
 +            const expandWidth = data.getLayout('bandWidth');
 +            if (baseAxis.isHorizontal()) {
-                 coordSysClipArea.x -= expandWidth;
-                 coordSysClipArea.width += expandWidth * 2;
++                (coordSysClipArea as CartesianCoordArea).x -= expandWidth;
++                (coordSysClipArea as CartesianCoordArea).width += expandWidth * 2;
 +            }
 +            else {
-                 coordSysClipArea.y -= expandWidth;
-                 coordSysClipArea.height += expandWidth * 2;
++                (coordSysClipArea as CartesianCoordArea).y -= expandWidth;
++                (coordSysClipArea as CartesianCoordArea).height += expandWidth * 2;
 +            }
 +        }
 +    }
 +
-     return coordSysClipArea;
++    return coordSysClipArea as PolarCoordArea | CartesianCoordArea;
 +}
 +
 +class BarView extends ChartView {
 +    static type = 'bar' as const;
 +    type = BarView.type;
 +
 +    private _data: List;
 +
 +    private _isLargeDraw: boolean;
 +
 +    private _isFirstFrame: boolean; // First frame after series added
 +    private _onRendered: EventCallback<unknown, unknown>;
 +
 +    private _backgroundGroup: Group;
 +
 +    private _backgroundEls: (Rect | Sector)[];
 +
 +    private _model: BarSeriesModel;
 +
 +    constructor() {
 +        super();
 +        this._isFirstFrame = true;
 +    }
 +
 +    render(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload) {
 +        this._model = seriesModel;
 +
 +        this.removeOnRenderedListener(api);
 +
 +        this._updateDrawMode(seriesModel);
 +
 +        const coordinateSystemType = seriesModel.get('coordinateSystem');
 +
 +        if (coordinateSystemType === 'cartesian2d'
 +            || coordinateSystemType === 'polar'
 +        ) {
 +            this._isLargeDraw
 +                ? this._renderLarge(seriesModel, ecModel, api)
 +                : this._renderNormal(seriesModel, ecModel, api, payload);
 +        }
 +        else if (__DEV__) {
 +            console.warn('Only cartesian2d and polar supported for bar.');
 +        }
 +    }
 +
 +    incrementalPrepareRender(seriesModel: BarSeriesModel): void {
 +        this._clear();
 +        this._updateDrawMode(seriesModel);
 +        // incremental also need to clip, otherwise might be overlow.
 +        // But must not set clip in each frame, otherwise all of the children will be marked redraw.
 +        this._updateLargeClip(seriesModel);
 +    }
 +
 +    incrementalRender(params: StageHandlerProgressParams, seriesModel: BarSeriesModel): void {
 +        // Do not support progressive in normal mode.
 +        this._incrementalRenderLarge(params, seriesModel);
 +    }
 +
 +    private _updateDrawMode(seriesModel: BarSeriesModel): void {
 +        const isLargeDraw = seriesModel.pipelineContext.large;
 +        if (this._isLargeDraw == null || isLargeDraw !== this._isLargeDraw) {
 +            this._isLargeDraw = isLargeDraw;
 +            this._clear();
 +        }
 +    }
 +
 +    private _renderNormal(
 +        seriesModel: BarSeriesModel,
 +        ecModel: GlobalModel,
 +        api: ExtensionAPI,
 +        payload: Payload
 +    ): void {
 +        const group = this.group;
 +        const data = seriesModel.getData();
 +        const oldData = this._data;
 +
 +        const coord = seriesModel.coordinateSystem;
 +        const baseAxis = coord.getBaseAxis();
 +        let isHorizontalOrRadial: boolean;
 +
 +        if (coord.type === 'cartesian2d') {
 +            isHorizontalOrRadial = (baseAxis as Axis2D).isHorizontal();
 +        }
 +        else if (coord.type === 'polar') {
 +            isHorizontalOrRadial = baseAxis.dim === 'angle';
 +        }
 +
 +        const animationModel = seriesModel.isAnimationEnabled() ? seriesModel : null;
 +
 +        const axis2DModel = (baseAxis as Axis2D).model;
 +        const realtimeSort = seriesModel.get('realtimeSort');
 +
 +        // If no data in the first frame, wait for data to initSort
 +        if (realtimeSort && data.count()) {
 +            if (this._isFirstFrame) {
 +                this._initSort(data, isHorizontalOrRadial, baseAxis as Axis2D, api);
 +                this._isFirstFrame = false;
 +                return;
 +            }
 +            else {
 +                this._onRendered = () => {
 +                    const orderMap = (idx: number) => {
 +                        const el = (data.getItemGraphicEl(idx) as Rect);
 +                        if (el) {
 +                            const shape = el.shape;
 +                            // If data is NaN, shape.xxx may be NaN, so use || 0 here in case
 +                            return (isHorizontalOrRadial ? shape.y + shape.height : shape.x + shape.width) || 0;
 +                        }
 +                        else {
 +                            return 0;
 +                        }
 +                    };
 +                    this._updateSort(data, orderMap, baseAxis as Axis2D, api);
 +                };
 +                api.getZr().on('rendered', this._onRendered as any);
 +            }
 +        }
 +
 +        const needsClip = seriesModel.get('clip', true) || realtimeSort;
 +        const coordSysClipArea = getClipArea(coord, data);
 +        // If there is clipPath created in large mode. Remove it.
 +        group.removeClipPath();
 +        // We don't use clipPath in normal mode because we needs a perfect animation
 +        // And don't want the label are clipped.
 +
 +        const roundCap = seriesModel.get('roundCap', true);
 +
 +        const drawBackground = seriesModel.get('showBackground', true);
 +        const backgroundModel = seriesModel.getModel('backgroundStyle');
 +        const barBorderRadius = backgroundModel.get('borderRadius') || 0;
 +
 +        const bgEls: BarView['_backgroundEls'] = [];
 +        const oldBgEls = this._backgroundEls;
 +
 +        const isInitSort = payload && payload.isInitSort;
 +        const isChangeOrder = payload && payload.type === 'changeAxisOrder';
 +
 +        const defaultTextGetter = (values: ParsedValue | ParsedValue[]) => {
 +            return getDefaultInterpolatedLabel(seriesModel.getData(), values);
 +        };
++        function createBackground(dataIndex: number) {
++            const bgLayout = getLayout[coord.type](data, dataIndex);
++            const bgEl = createBackgroundEl(coord, isHorizontalOrRadial, bgLayout);
++            bgEl.useStyle(backgroundModel.getItemStyle());
++            // Only cartesian2d support borderRadius.
++            if (coord.type === 'cartesian2d') {
++                (bgEl as Rect).setShape('r', barBorderRadius);
++            }
++            bgEls[dataIndex] = bgEl;
++            return bgEl;
++        };
 +        data.diff(oldData)
 +            .add(function (dataIndex) {
 +                const itemModel = data.getItemModel<BarDataItemOption>(dataIndex);
 +                const layout = getLayout[coord.type](data, dataIndex, itemModel);
 +
 +                if (drawBackground) {
-                     const bgLayout = getLayout[coord.type](data, dataIndex);
-                     const bgEl = createBackgroundEl(coord, isHorizontalOrRadial, bgLayout);
-                     bgEl.useStyle(backgroundModel.getItemStyle());
-                     // Only cartesian2d support borderRadius.
-                     if (coord.type === 'cartesian2d') {
-                         (bgEl as Rect).setShape('r', barBorderRadius);
-                     }
-                     bgEls[dataIndex] = bgEl;
++                    createBackground(dataIndex);
 +                }
 +
 +                // If dataZoom in filteMode: 'empty', the baseValue can be set as NaN in "axisProxy".
 +                if (!data.hasValue(dataIndex)) {
 +                    return;
 +                }
 +
 +                let isClipped = false;
 +                if (needsClip) {
 +                    // Clip will modify the layout params.
 +                    // And return a boolean to determine if the shape are fully clipped.
 +                    isClipped = clip[coord.type](coordSysClipArea, layout);
 +                }
 +
 +                const el = elementCreator[coord.type](
 +                    seriesModel,
 +                    data,
 +                    dataIndex,
 +                    layout,
 +                    isHorizontalOrRadial,
 +                    animationModel,
 +                    baseAxis.model,
 +                    false,
 +                    roundCap
 +                );
 +
 +                updateStyle(
 +                    el, data, dataIndex, itemModel, layout,
 +                    seriesModel, isHorizontalOrRadial, coord.type === 'polar'
 +                );
 +
 +                initLabel(
 +                    el, data, dataIndex, itemModel.getModel('label'), seriesModel, animationModel, defaultTextGetter
 +                );
 +                if (isInitSort) {
 +                    (el as Rect).attr({ shape: layout });
 +                }
 +                else if (realtimeSort) {
 +                    (el as unknown as ECElement).disableLabelAnimation = true;
 +
 +                    updateRealtimeAnimation(
 +                        seriesModel,
 +                        axis2DModel,
 +                        animationModel,
 +                        el as Rect,
 +                        layout as LayoutRect,
 +                        dataIndex,
 +                        isHorizontalOrRadial,
 +                        false,
 +                        false
 +                    );
 +                }
 +                else {
 +                    initProps(el, {shape: layout} as any, seriesModel, dataIndex);
 +                }
 +
 +                data.setItemGraphicEl(dataIndex, el);
 +
 +                group.add(el);
 +                el.ignore = isClipped;
 +            })
 +            .update(function (newIndex, oldIndex) {
 +                const itemModel = data.getItemModel<BarDataItemOption>(newIndex);
 +                const layout = getLayout[coord.type](data, newIndex, itemModel);
 +
 +                if (drawBackground) {
-                     const bgEl = oldBgEls[oldIndex];
-                     bgEl.useStyle(backgroundModel.getItemStyle());
-                     // Only cartesian2d support borderRadius.
-                     if (coord.type === 'cartesian2d') {
-                         (bgEl as Rect).setShape('r', barBorderRadius);
++                    let bgEl: Rect | Sector;
++                    if (oldBgEls.length === 0) {
++                        bgEl = createBackground(oldIndex);
++                    }
++                    else {
++                        bgEl = oldBgEls[oldIndex];
++                        bgEl.useStyle(backgroundModel.getItemStyle());
++                        // Only cartesian2d support borderRadius.
++                        if (coord.type === 'cartesian2d') {
++                            (bgEl as Rect).setShape('r', barBorderRadius);
++                        }
++                        bgEls[newIndex] = bgEl;
 +                    }
-                     bgEls[newIndex] = bgEl;
- 
-                     const bgLayout = getLayout[coord.type](data, newIndex);
-                     const shape = createBackgroundShape(isHorizontalOrRadial, bgLayout, coord);
-                     updateProps(
-                         bgEl as Path, {shape: shape as RectShape}, animationModel, newIndex
-                     );
 +                }
 +
 +                let el = oldData.getItemGraphicEl(oldIndex) as BarPossiblePath;
 +                if (!data.hasValue(newIndex)) {
 +                    group.remove(el);
 +                    el = null;
 +                }
 +
 +                let isClipped = false;
 +                if (needsClip) {
 +                    isClipped = clip[coord.type](coordSysClipArea, layout);
 +                    if (isClipped) {
 +                        group.remove(el);
 +                    }
 +                }
 +
 +                if (!el) {
 +                    el = elementCreator[coord.type](
 +                        seriesModel,
 +                        data,
 +                        newIndex,
 +                        layout,
 +                        isHorizontalOrRadial,
 +                        animationModel,
 +                        baseAxis.model,
 +                        !!el,
 +                        roundCap
 +                    );
 +                }
 +
 +                // Not change anything if only order changed.
 +                // Especially not change label.
 +                if (!isChangeOrder) {
 +                    updateStyle(
 +                        el, data, newIndex, itemModel, layout,
 +                        seriesModel, isHorizontalOrRadial, coord.type === 'polar'
 +                    );
 +                    updateLabel(
 +                        el, data, newIndex, itemModel.getModel('label'), seriesModel, animationModel, defaultTextGetter
 +                    );
 +                }
 +
 +                if (isInitSort) {
 +                    (el as Rect).attr({ shape: layout });
 +                }
 +                else if (realtimeSort) {
 +                    (el as unknown as ECElement).disableLabelAnimation = true;
 +
 +                    updateRealtimeAnimation(
 +                        seriesModel,
 +                        axis2DModel,
 +                        animationModel,
 +                        el as Rect,
 +                        layout as LayoutRect,
 +                        newIndex,
 +                        isHorizontalOrRadial,
 +                        true,
 +                        isChangeOrder
 +                    );
 +                }
 +                else {
 +                    updateProps(el, {
 +                        shape: layout
 +                    } as any, seriesModel, newIndex, null);
 +                }
 +
 +                data.setItemGraphicEl(newIndex, el);
 +                el.ignore = isClipped;
 +                group.add(el);
 +            })
 +            .remove(function (dataIndex) {
 +                const el = oldData.getItemGraphicEl(dataIndex) as Path;
 +                el && removeElementWithFadeOut(el, seriesModel, dataIndex);
 +            })
 +            .execute();
 +
 +        const bgGroup = this._backgroundGroup || (this._backgroundGroup = new Group());
 +        bgGroup.removeAll();
 +
 +        for (let i = 0; i < bgEls.length; ++i) {
 +            bgGroup.add(bgEls[i]);
 +        }
 +        group.add(bgGroup);
 +        this._backgroundEls = bgEls;
 +
 +        this._data = data;
 +    }
 +
 +    private _renderLarge(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI): void {
 +        this._clear();
 +        createLarge(seriesModel, this.group);
 +        this._updateLargeClip(seriesModel);
 +    }
 +
 +    private _incrementalRenderLarge(params: StageHandlerProgressParams, seriesModel: BarSeriesModel): void {
 +        this._removeBackground();
 +        createLarge(seriesModel, this.group, true);
 +    }
 +
 +    private _updateLargeClip(seriesModel: BarSeriesModel): void {
 +        // Use clipPath in large mode.
 +        const clipPath = seriesModel.get('clip', true)
 +            ? createClipPath(seriesModel.coordinateSystem, false, seriesModel)
 +            : null;
 +        if (clipPath) {
 +            this.group.setClipPath(clipPath);
 +        }
 +        else {
 +            this.group.removeClipPath();
 +        }
 +    }
 +
 +    _dataSort(
 +        data: List<BarSeriesModel, DefaultDataVisual>,
 +        idxMap: ((idx: number) => number)
 +    ): OrdinalSortInfo[] {
 +        type SortValueInfo = {
 +            mappedValue: number,
 +            ordinalNumber: OrdinalNumber,
 +            beforeSortIndex: number
 +        };
 +        const info: SortValueInfo[] = [];
 +        data.each(idx => {
 +            info.push({
 +                mappedValue: idxMap(idx),
 +                ordinalNumber: idx,
 +                beforeSortIndex: null
 +            });
 +        });
 +
 +        info.sort((a, b) => {
 +            return b.mappedValue - a.mappedValue;
 +        });
 +
 +        // Update beforeSortIndex
 +        for (let i = 0; i < info.length; ++i) {
 +            info[info[i].ordinalNumber].beforeSortIndex = i;
 +        }
 +
 +        return map(info, item => {
 +            return {
 +                ordinalNumber: item.ordinalNumber,
 +                beforeSortIndex: item.beforeSortIndex
 +            };
 +        });
 +    }
 +
 +    _isDataOrderChanged(
 +        data: List<BarSeriesModel, DefaultDataVisual>,
 +        orderMap: ((idx: number) => number),
 +        oldOrder: OrdinalSortInfo[]
 +    ): boolean {
 +        const oldCount = oldOrder ? oldOrder.length : 0;
 +        if (oldCount !== data.count()) {
 +            return true;
 +        }
 +
 +        let lastValue = Number.MAX_VALUE;
 +        for (let i = 0; i < oldOrder.length; ++i) {
 +            const value = orderMap(oldOrder[i].ordinalNumber);
 +            if (value > lastValue) {
 +                return true;
 +            }
 +            lastValue = value;
 +        }
 +        return false;
 +    }
 +
 +    _updateSort(
 +        data: List<BarSeriesModel, DefaultDataVisual>,
 +        orderMap: ((idx: number) => number),
 +        baseAxis: Axis2D,
 +        api: ExtensionAPI
 +    ) {
 +        const oldOrder = (baseAxis.scale as OrdinalScale).getCategorySortInfo();
 +        const isOrderChanged = this._isDataOrderChanged(data, orderMap, oldOrder);
 +        if (isOrderChanged) {
 +            const newOrder = this._dataSort(data, orderMap);
 +            const extent = baseAxis.scale.getExtent();
 +            for (let i = extent[0]; i < extent[1]; ++i) {
 +                /**
 +                 * Consider the case when A and B changed order, whose representing
 +                 * bars are both out of sight, we don't wish to trigger reorder action
 +                 * as long as the order in the view doesn't change.
 +                 */
 +                if (!oldOrder[i] || oldOrder[i].ordinalNumber !== newOrder[i].ordinalNumber) {
 +                    this.removeOnRenderedListener(api);
 +
 +                    const action = {
 +                        type: 'changeAxisOrder',
 +                        componentType: baseAxis.dim + 'Axis',
 +                        axisId: baseAxis.index,
 +                        sortInfo: newOrder
 +                    } as Payload;
 +                    api.dispatchAction(action);
 +                    break;
 +                }
 +            }
 +        }
 +    }
 +
 +    _initSort(
 +        data: List<BarSeriesModel, DefaultDataVisual>,
 +        isHorizontal: boolean,
 +        baseAxis: Axis2D,
 +        api: ExtensionAPI
 +    ) {
 +        const action = {
 +            type: 'changeAxisOrder',
 +            componentType: baseAxis.dim + 'Axis',
 +            isInitSort: true,
 +            axisId: baseAxis.index,
 +            sortInfo: this._dataSort(
 +                data,
 +                idx => parseFloat(data.get(isHorizontal ? 'y' : 'x', idx) as string) || 0
 +            )
 +        } as Payload;
 +        api.dispatchAction(action);
 +    }
 +
 +    remove(ecModel: GlobalModel, api: ExtensionAPI) {
 +        this._clear(this._model);
 +        this.removeOnRenderedListener(api);
 +    }
 +
 +    dispose(ecModel: GlobalModel, api: ExtensionAPI) {
 +        this.removeOnRenderedListener(api);
 +    }
 +
 +    removeOnRenderedListener(api: ExtensionAPI) {
 +        if (this._onRendered) {
 +            api.getZr().off('rendered', this._onRendered);
 +            this._onRendered = null;
 +        }
 +    }
 +
 +    private _clear(model?: SeriesModel): void {
 +        const group = this.group;
 +        const data = this._data;
 +        if (model && model.isAnimationEnabled() && data && !this._isLargeDraw) {
 +            this._removeBackground();
 +            this._backgroundEls = [];
 +
 +            data.eachItemGraphicEl(function (el: Path) {
 +                removeElementWithFadeOut(el, model, getECData(el).dataIndex);
 +            });
 +        }
 +        else {
 +            group.removeAll();
 +        }
 +        this._data = null;
 +        this._isFirstFrame = true;
 +    }
 +
 +    private _removeBackground(): void {
 +        this.group.remove(this._backgroundGroup);
 +        this._backgroundGroup = null;
 +    }
 +}
 +
 +interface Clipper {
-     (coordSysBoundingRect: RectLike, layout: RectLayout | SectorLayout): boolean
++    (coordSysBoundingRect: PolarCoordArea | CartesianCoordArea, layout: RectLayout | SectorLayout): boolean
 +}
 +const clip: {
 +    [key in 'cartesian2d' | 'polar']: Clipper
 +} = {
-     cartesian2d(coordSysBoundingRect: RectLike, layout: Rect['shape']) {
++    cartesian2d(coordSysBoundingRect: CartesianCoordArea, layout: Rect['shape']) {
 +        const signWidth = layout.width < 0 ? -1 : 1;
 +        const signHeight = layout.height < 0 ? -1 : 1;
 +        // Needs positive width and height
 +        if (signWidth < 0) {
 +            layout.x += layout.width;
 +            layout.width = -layout.width;
 +        }
 +        if (signHeight < 0) {
 +            layout.y += layout.height;
 +            layout.height = -layout.height;
 +        }
 +
 +        const x = mathMax(layout.x, coordSysBoundingRect.x);
 +        const x2 = mathMin(layout.x + layout.width, coordSysBoundingRect.x + coordSysBoundingRect.width);
 +        const y = mathMax(layout.y, coordSysBoundingRect.y);
 +        const y2 = mathMin(layout.y + layout.height, coordSysBoundingRect.y + coordSysBoundingRect.height);
 +
 +        layout.x = x;
 +        layout.y = y;
 +        layout.width = x2 - x;
 +        layout.height = y2 - y;
 +
 +        const clipped = layout.width < 0 || layout.height < 0;
 +
 +        // Reverse back
 +        if (signWidth < 0) {
 +            layout.x += layout.width;
 +            layout.width = -layout.width;
 +        }
 +        if (signHeight < 0) {
 +            layout.y += layout.height;
 +            layout.height = -layout.height;
 +        }
 +
 +        return clipped;
 +    },
 +
-     polar() {
-         return false;
++    polar(coordSysClipArea: PolarCoordArea, layout: Sector['shape']) {
++        const signR = layout.r0 <= layout.r ? 1 : -1;
++        // Make sure r is larger than r0
++        if (signR < 0) {
++            const tmp = layout.r;
++            layout.r = layout.r0;
++            layout.r0 = tmp;
++        }
++
++        const r = mathMin(layout.r, coordSysClipArea.r);
++        const r0 = mathMax(layout.r0, coordSysClipArea.r0);
++
++        layout.r = r;
++        layout.r0 = r0;
++
++        const clipped = r - r0 < 0;
++
++        // Reverse back
++        if (signR < 0) {
++            const tmp = layout.r;
++            layout.r = layout.r0;
++            layout.r0 = tmp;
++        }
++
++        return clipped;
 +    }
 +};
 +
 +interface ElementCreator {
 +    (
 +        seriesModel: BarSeriesModel, data: List, newIndex: number,
 +        layout: RectLayout | SectorLayout, isHorizontalOrRadial: boolean,
 +        animationModel: BarSeriesModel,
 +        axisModel: CartesianAxisModel | AngleAxisModel | RadiusAxisModel,
 +        isUpdate: boolean,
 +        roundCap?: boolean
 +    ): BarPossiblePath
 +}
 +
 +const elementCreator: {
 +    [key in 'polar' | 'cartesian2d']: ElementCreator
 +} = {
 +
 +    cartesian2d(
 +        seriesModel, data, newIndex, layout: RectLayout, isHorizontal,
 +        animationModel, axisModel, isUpdate, roundCap
 +    ) {
 +        const rect = new Rect({
 +            shape: extend({}, layout),
 +            z2: 1
 +        });
 +        (rect as any).__dataIndex = newIndex;
 +
 +        rect.name = 'item';
 +
 +        if (animationModel) {
 +            const rectShape = rect.shape;
 +            const animateProperty = isHorizontal ? 'height' : 'width' as 'width' | 'height';
 +            rectShape[animateProperty] = 0;
 +        }
 +        return rect;
 +    },
 +
 +    polar(
 +        seriesModel, data, newIndex, layout: SectorLayout, isRadial: boolean,
 +        animationModel, axisModel, isUpdate, roundCap
 +    ) {
 +        // Keep the same logic with bar in catesion: use end value to control
 +        // direction. Notice that if clockwise is true (by default), the sector
 +        // will always draw clockwisely, no matter whether endAngle is greater
 +        // or less than startAngle.
 +        const clockwise = layout.startAngle < layout.endAngle;
 +
 +        const ShapeClass = (!isRadial && roundCap) ? Sausage : Sector;
 +
 +        const sector = new ShapeClass({
 +            shape: defaults({clockwise: clockwise}, layout),
 +            z2: 1
 +        });
 +
 +        sector.name = 'item';
 +
 +        // Animation
 +        if (animationModel) {
 +            const sectorShape = sector.shape;
 +            const animateProperty = isRadial ? 'r' : 'endAngle' as 'r' | 'endAngle';
 +            const animateTarget = {} as SectorShape;
 +            sectorShape[animateProperty] = isRadial ? 0 : layout.startAngle;
 +            animateTarget[animateProperty] = layout[animateProperty];
 +            (isUpdate ? updateProps : initProps)(sector, {
 +                shape: animateTarget
 +                // __value: typeof dataValue === 'string' ? parseInt(dataValue, 10) : dataValue
 +            }, animationModel);
 +        }
 +
 +        return sector;
 +    }
 +};
 +
 +function updateRealtimeAnimation(
 +    seriesModel: BarSeriesModel,
 +    axisModel: CartesianAxisModel,
 +    animationModel: BarSeriesModel,
 +    el: Rect,
 +    layout: LayoutRect,
 +    newIndex: number,
 +    isHorizontal: boolean,
 +    isUpdate: boolean,
 +    isChangeOrder: boolean
 +) {
 +    // Animation
 +    if (animationModel || axisModel) {
 +        let seriesTarget;
 +        let axisTarget;
 +        if (isHorizontal) {
 +            axisTarget = {
 +                x: layout.x,
 +                width: layout.width
 +            };
 +            seriesTarget = {
 +                y: layout.y,
 +                height: layout.height
 +            };
 +        }
 +        else {
 +            axisTarget = {
 +                y: layout.y,
 +                height: layout.height
 +            };
 +            seriesTarget = {
 +                x: layout.x,
 +                width: layout.width
 +            };
 +        }
 +
 +        if (!isChangeOrder) {
 +            // Keep the original growth animation if only axis order changed.
 +            // Not start a new animation.
 +            (isUpdate ? updateProps : initProps)(el, {
 +                shape: seriesTarget
 +            }, seriesModel, newIndex, null);
 +        }
 +
 +        (isUpdate ? updateProps : initProps)(el, {
 +            shape: axisTarget
 +        }, axisModel, newIndex);
 +    }
 +}
 +
 +interface GetLayout {
 +    (data: List, dataIndex: number, itemModel?: Model<BarDataItemOption>): RectLayout | SectorLayout
 +}
 +const getLayout: {
 +    [key in 'cartesian2d' | 'polar']: GetLayout
 +} = {
 +    // itemModel is only used to get borderWidth, which is not needed
 +    // when calculating bar background layout.
 +    cartesian2d(data, dataIndex, itemModel?): RectLayout {
 +        const layout = data.getItemLayout(dataIndex) as RectLayout;
 +        const fixedLineWidth = itemModel ? getLineWidth(itemModel, layout) : 0;
 +
 +        // fix layout with lineWidth
 +        const signX = layout.width > 0 ? 1 : -1;
 +        const signY = layout.height > 0 ? 1 : -1;
 +        return {
 +            x: layout.x + signX * fixedLineWidth / 2,
 +            y: layout.y + signY * fixedLineWidth / 2,
 +            width: layout.width - signX * fixedLineWidth,
 +            height: layout.height - signY * fixedLineWidth
 +        };
 +    },
 +
 +    polar(data, dataIndex, itemModel?): SectorLayout {
 +        const layout = data.getItemLayout(dataIndex);
 +        return {
 +            cx: layout.cx,
 +            cy: layout.cy,
 +            r0: layout.r0,
 +            r: layout.r,
 +            startAngle: layout.startAngle,
 +            endAngle: layout.endAngle
 +        } as SectorLayout;
 +    }
 +};
 +
 +function isZeroOnPolar(layout: SectorLayout) {
 +    return layout.startAngle != null
 +        && layout.endAngle != null
 +        && layout.startAngle === layout.endAngle;
 +}
 +
 +function updateStyle(
 +    el: BarPossiblePath,
 +    data: List, dataIndex: number,
 +    itemModel: Model<BarDataItemOption>,
 +    layout: RectLayout | SectorLayout,
 +    seriesModel: BarSeriesModel,
 +    isHorizontal: boolean,
 +    isPolar: boolean
 +) {
 +    const style = data.getItemVisual(dataIndex, 'style');
 +
 +    if (!isPolar) {
 +        (el as Rect).setShape('r', itemModel.get(BAR_BORDER_RADIUS_QUERY) || 0);
 +    }
 +
 +    el.useStyle(style);
 +
 +    el.ignore = isZeroOnPolar(layout as SectorLayout);
 +
 +    const cursorStyle = itemModel.getShallow('cursor');
 +    cursorStyle && (el as Path).attr('cursor', cursorStyle);
 +
 +    if (!isPolar) {
 +        const labelPositionOutside = isHorizontal
 +            ? ((layout as RectLayout).height > 0 ? 'bottom' as const : 'top' as const)
 +            : ((layout as RectLayout).width > 0 ? 'left' as const : 'right' as const);
 +
 +        setLabelStyle(
 +            el, getLabelStatesModels(itemModel),
 +            {
 +                labelFetcher: seriesModel,
 +                labelDataIndex: dataIndex,
 +                defaultText: getDefaultLabel(seriesModel.getData(), dataIndex),
 +                inheritColor: style.fill as ColorString,
 +                defaultOutsidePosition: labelPositionOutside
 +            }
 +        );
 +
 +        const label = el.getTextContent();
 +        if (label) {
 +            const obj = labelInner(label);
 +            obj.prevValue = obj.value;
 +            obj.value = seriesModel.getRawValue(dataIndex) as ParsedValue | ParsedValue[];
 +        }
 +    }
 +
 +    const emphasisModel = itemModel.getModel(['emphasis']);
 +    enableHoverEmphasis(el, emphasisModel.get('focus'), emphasisModel.get('blurScope'));
 +    setStatesStylesFromModel(el, itemModel);
 +
 +    if (isZeroOnPolar(layout as SectorLayout)) {
 +        each(el.states, (state) => {
 +            if (state.style) {
 +                state.style.fill = state.style.stroke = 'none';
 +            }
 +        });
 +    }
 +}
 +
 +// In case width or height are too small.
 +function getLineWidth(
 +    itemModel: Model<BarDataItemOption>,
 +    rawLayout: RectLayout
 +) {
 +    const lineWidth = itemModel.get(BAR_BORDER_WIDTH_QUERY) || 0;
 +    // width or height may be NaN for empty data
 +    const width = isNaN(rawLayout.width) ? Number.MAX_VALUE : Math.abs(rawLayout.width);
 +    const height = isNaN(rawLayout.height) ? Number.MAX_VALUE : Math.abs(rawLayout.height);
 +    return Math.min(lineWidth, width, height);
 +}
 +
 +class LagePathShape {
 +    points: ArrayLike<number>;
 +}
 +interface LargePathProps extends PathProps {
 +    shape?: LagePathShape
 +}
 +class LargePath extends Path<LargePathProps> {
 +    type = 'largeBar';
 +
 +    shape: LagePathShape;
 +;
 +    __startPoint: number[];
 +    __baseDimIdx: number;
 +    __largeDataIndices: ArrayLike<number>;
 +    __barWidth: number;
 +
 +    constructor(opts?: LargePathProps) {
 +        super(opts);
 +    }
 +
 +    getDefaultShape() {
 +        return new LagePathShape();
 +    }
 +
 +    buildPath(ctx: CanvasRenderingContext2D, shape: LagePathShape) {
 +        // Drawing lines is more efficient than drawing
 +        // a whole line or drawing rects.
 +        const points = shape.points;
 +        const startPoint = this.__startPoint;
 +        const baseDimIdx = this.__baseDimIdx;
 +
 +        for (let i = 0; i < points.length; i += 2) {
 +            startPoint[baseDimIdx] = points[i + baseDimIdx];
 +            ctx.moveTo(startPoint[0], startPoint[1]);
 +            ctx.lineTo(points[i], points[i + 1]);
 +        }
 +    }
 +}
 +
 +function createLarge(
 +    seriesModel: BarSeriesModel,
 +    group: Group,
 +    incremental?: boolean
 +) {
 +    // TODO support polar
 +    const data = seriesModel.getData();
 +    const startPoint = [];
 +    const baseDimIdx = data.getLayout('valueAxisHorizontal') ? 1 : 0;
 +    startPoint[1 - baseDimIdx] = data.getLayout('valueAxisStart');
 +
 +    const largeDataIndices = data.getLayout('largeDataIndices');
 +    const barWidth = data.getLayout('barWidth');
 +
 +    const backgroundModel = seriesModel.getModel('backgroundStyle');
 +    const drawBackground = seriesModel.get('showBackground', true);
 +
 +    if (drawBackground) {
 +        const points = data.getLayout('largeBackgroundPoints');
 +        const backgroundStartPoint: number[] = [];
 +        backgroundStartPoint[1 - baseDimIdx] = data.getLayout('backgroundStart');
 +
 +        const bgEl = new LargePath({
 +            shape: {points: points},
 +            incremental: !!incremental,
 +            silent: true,
 +            z2: 0
 +        });
 +        bgEl.__startPoint = backgroundStartPoint;
 +        bgEl.__baseDimIdx = baseDimIdx;
 +        bgEl.__largeDataIndices = largeDataIndices;
 +        bgEl.__barWidth = barWidth;
 +        setLargeBackgroundStyle(bgEl, backgroundModel, data);
 +        group.add(bgEl);
 +    }
 +
 +    const el = new LargePath({
 +        shape: {points: data.getLayout('largePoints')},
 +        incremental: !!incremental
 +    });
 +    el.__startPoint = startPoint;
 +    el.__baseDimIdx = baseDimIdx;
 +    el.__largeDataIndices = largeDataIndices;
 +    el.__barWidth = barWidth;
 +    group.add(el);
 +    setLargeStyle(el, seriesModel, data);
 +
 +    // Enable tooltip and user mouse/touch event handlers.
 +    getECData(el).seriesIndex = seriesModel.seriesIndex;
 +
 +    if (!seriesModel.get('silent')) {
 +        el.on('mousedown', largePathUpdateDataIndex);
 +        el.on('mousemove', largePathUpdateDataIndex);
 +    }
 +}
 +
 +// Use throttle to avoid frequently traverse to find dataIndex.
 +const largePathUpdateDataIndex = throttle(function (this: LargePath, event: ZRElementEvent) {
 +    const largePath = this;
 +    const dataIndex = largePathFindDataIndex(largePath, event.offsetX, event.offsetY);
 +    getECData(largePath).dataIndex = dataIndex >= 0 ? dataIndex : null;
 +}, 30, false);
 +
 +function largePathFindDataIndex(largePath: LargePath, x: number, y: number) {
 +    const baseDimIdx = largePath.__baseDimIdx;
 +    const valueDimIdx = 1 - baseDimIdx;
 +    const points = largePath.shape.points;
 +    const largeDataIndices = largePath.__largeDataIndices;
 +    const barWidthHalf = Math.abs(largePath.__barWidth / 2);
 +    const startValueVal = largePath.__startPoint[valueDimIdx];
 +
 +    _eventPos[0] = x;
 +    _eventPos[1] = y;
 +    const pointerBaseVal = _eventPos[baseDimIdx];
 +    const pointerValueVal = _eventPos[1 - baseDimIdx];
 +    const baseLowerBound = pointerBaseVal - barWidthHalf;
 +    const baseUpperBound = pointerBaseVal + barWidthHalf;
 +
 +    for (let i = 0, len = points.length / 2; i < len; i++) {
 +        const ii = i * 2;
 +        const barBaseVal = points[ii + baseDimIdx];
 +        const barValueVal = points[ii + valueDimIdx];
 +        if (
 +            barBaseVal >= baseLowerBound && barBaseVal <= baseUpperBound
 +            && (
 +                startValueVal <= barValueVal
 +                    ? (pointerValueVal >= startValueVal && pointerValueVal <= barValueVal)
 +                    : (pointerValueVal >= barValueVal && pointerValueVal <= startValueVal)
 +            )
 +        ) {
 +            return largeDataIndices[i];
 +        }
 +    }
 +
 +    return -1;
 +}
 +
 +function setLargeStyle(
 +    el: LargePath,
 +    seriesModel: BarSeriesModel,
 +    data: List
 +) {
 +    const globalStyle = data.getVisual('style');
 +
 +    el.useStyle(extend({}, globalStyle));
 +    // Use stroke instead of fill.
 +    el.style.fill = null;
 +    el.style.stroke = globalStyle.fill;
 +    el.style.lineWidth = data.getLayout('barWidth');
 +}
 +
 +function setLargeBackgroundStyle(
 +    el: LargePath,
 +    backgroundModel: Model<BarSeriesOption['backgroundStyle']>,
 +    data: List
 +) {
 +    const borderColor = backgroundModel.get('borderColor') || backgroundModel.get('color');
 +    const itemStyle = backgroundModel.getItemStyle();
 +
 +    el.useStyle(itemStyle);
 +    el.style.fill = null;
 +    el.style.stroke = borderColor;
 +    el.style.lineWidth = data.getLayout('barWidth') as number;
 +}
 +
 +function createBackgroundShape(
 +    isHorizontalOrRadial: boolean,
 +    layout: SectorLayout | RectLayout,
 +    coord: CoordSysOfBar
 +): SectorShape | RectShape {
 +    if (isCoordinateSystemType<Cartesian2D>(coord, 'cartesian2d')) {
 +        const rectShape = layout as RectShape;
 +        const coordLayout = coord.getArea();
 +        return {
 +            x: isHorizontalOrRadial ? rectShape.x : coordLayout.x,
 +            y: isHorizontalOrRadial ? coordLayout.y : rectShape.y,
 +            width: isHorizontalOrRadial ? rectShape.width : coordLayout.width,
 +            height: isHorizontalOrRadial ? coordLayout.height : rectShape.height
 +        } as RectShape;
 +    }
 +    else {
 +        const coordLayout = coord.getArea();
 +        const sectorShape = layout as SectorShape;
 +        return {
 +            cx: coordLayout.cx,
 +            cy: coordLayout.cy,
 +            r0: isHorizontalOrRadial ? coordLayout.r0 : sectorShape.r0,
 +            r: isHorizontalOrRadial ? coordLayout.r : sectorShape.r,
 +            startAngle: isHorizontalOrRadial ? sectorShape.startAngle : 0,
 +            endAngle: isHorizontalOrRadial ? sectorShape.endAngle : Math.PI * 2
 +        } as SectorShape;
 +    }
 +}
 +
 +function createBackgroundEl(
 +    coord: CoordSysOfBar,
 +    isHorizontalOrRadial: boolean,
 +    layout: SectorLayout | RectLayout
 +): Rect | Sector {
 +    const ElementClz = coord.type === 'polar' ? Sector : Rect;
 +    return new ElementClz({
 +        shape: createBackgroundShape(isHorizontalOrRadial, layout, coord) as any,
 +        silent: true,
 +        z2: 0
 +    });
 +}
 +
 +ChartView.registerClass(BarView);
 +
 +export default BarView;
diff --cc src/chart/funnel/FunnelSeries.ts
index bb358b2,0000000..c1f2f62
mode 100644,000000..100644
--- a/src/chart/funnel/FunnelSeries.ts
+++ b/src/chart/funnel/FunnelSeries.ts
@@@ -1,188 -1,0 +1,194 @@@
 +/*
 +* 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 * as zrUtil from 'zrender/src/core/util';
 +import createListSimply from '../helper/createListSimply';
 +import {defaultEmphasis} from '../../util/model';
 +import {makeSeriesEncodeForNameBased} from '../../data/helper/sourceHelper';
 +import LegendVisualProvider from '../../visual/LegendVisualProvider';
 +import SeriesModel from '../../model/Series';
 +import {
 +    SeriesOption,
 +    BoxLayoutOptionMixin,
 +    HorizontalAlign,
 +    LabelOption,
 +    LabelLineOption,
 +    ItemStyleOption,
 +    OptionDataValueNumeric,
 +    StatesOptionMixin,
-     OptionDataItemObject
++    OptionDataItemObject,
++    LayoutOrient,
++    VerticalAlign
 +} from '../../util/types';
 +import GlobalModel from '../../model/Global';
 +import List from '../../data/List';
 +import ComponentModel from '../../model/Component';
 +
 +
 +type FunnelLabelOption = Omit<LabelOption, 'position'> & {
 +    position?: LabelOption['position']
 +        | 'outer' | 'inner' | 'center' | 'rightTop' | 'rightBottom' | 'leftTop' | 'leftBottom'
 +};
 +
 +export interface FunnelStateOption {
 +    itemStyle?: ItemStyleOption
 +    label?: FunnelLabelOption
 +    labelLine?: LabelLineOption
 +}
 +
 +export interface FunnelDataItemOption
 +    extends FunnelStateOption, StatesOptionMixin<FunnelStateOption>,
 +    OptionDataItemObject<OptionDataValueNumeric> {
 +
 +    itemStyle?: ItemStyleOption & {
++        width?: number | string
 +        height?: number | string
 +    }
 +}
 +
 +export interface FunnelSeriesOption extends SeriesOption<FunnelStateOption>, FunnelStateOption,
 +    BoxLayoutOptionMixin {
 +    type?: 'funnel'
 +
 +    min?: number
 +    max?: number
 +
 +    /**
 +     * Absolute number or percent string
 +     */
 +    minSize?: number | string
 +    maxSize?: number | string
 +
 +    sort?: 'ascending' | 'descending' | 'none'
 +
++    orient?: LayoutOrient
++
 +    gap?: number
 +
-     funnelAlign?: HorizontalAlign
++    funnelAlign?: HorizontalAlign | VerticalAlign
 +
 +    data?: (OptionDataValueNumeric | OptionDataValueNumeric[] | FunnelDataItemOption)[]
 +}
 +
 +class FunnelSeriesModel extends SeriesModel<FunnelSeriesOption> {
 +    static type = 'series.funnel' as const;
 +    type = FunnelSeriesModel.type;
 +
 +    useColorPaletteOnData = true;
 +
 +    init(option: FunnelSeriesOption) {
 +        super.init.apply(this, arguments as any);
 +
 +        // Enable legend selection for each data item
 +        // Use a function instead of direct access because data reference may changed
 +        this.legendVisualProvider = new LegendVisualProvider(
 +            zrUtil.bind(this.getData, this), zrUtil.bind(this.getRawData, this)
 +        );
 +        // Extend labelLine emphasis
 +        this._defaultLabelLine(option);
 +    }
 +
 +    getInitialData(this: FunnelSeriesModel, option: FunnelSeriesOption, ecModel: GlobalModel): List {
 +        return createListSimply(this, {
 +            coordDimensions: ['value'],
 +            encodeDefaulter: zrUtil.curry(makeSeriesEncodeForNameBased, this)
 +        });
 +    }
 +
 +    _defaultLabelLine(option: FunnelSeriesOption) {
 +        // Extend labelLine emphasis
 +        defaultEmphasis(option, 'labelLine', ['show']);
 +
 +        const labelLineNormalOpt = option.labelLine;
 +        const labelLineEmphasisOpt = option.emphasis.labelLine;
 +        // Not show label line if `label.normal.show = false`
 +        labelLineNormalOpt.show = labelLineNormalOpt.show
 +            && option.label.show;
 +        labelLineEmphasisOpt.show = labelLineEmphasisOpt.show
 +            && option.emphasis.label.show;
 +    }
 +
 +    // Overwrite
 +    getDataParams(dataIndex: number) {
 +        const data = this.getData();
 +        const params = super.getDataParams(dataIndex);
 +        const valueDim = data.mapDimension('value');
 +        const sum = data.getSum(valueDim);
 +        // Percent is 0 if sum is 0
 +        params.percent = !sum ? 0 : +(data.get(valueDim, dataIndex) as number / sum * 100).toFixed(2);
 +
 +        params.$vars.push('percent');
 +        return params;
 +    }
 +
 +    static defaultOption: FunnelSeriesOption = {
 +        zlevel: 0,                  // 一级层叠
 +        z: 2,                       // 二级层叠
 +        legendHoverLink: true,
 +        left: 80,
 +        top: 60,
 +        right: 80,
 +        bottom: 60,
 +        // width: {totalWidth} - left - right,
 +        // height: {totalHeight} - top - bottom,
 +
 +        // 默认取数据最小最大值
 +        // min: 0,
 +        // max: 100,
 +        minSize: '0%',
 +        maxSize: '100%',
 +        sort: 'descending', // 'ascending', 'descending'
++        orient: 'vertical',
 +        gap: 0,
 +        funnelAlign: 'center',
 +        label: {
 +            show: true,
 +            position: 'outer'
 +            // formatter: 标签文本格式器,同Tooltip.formatter,不支持异步回调
 +        },
 +        labelLine: {
 +            show: true,
 +            length: 20,
 +            lineStyle: {
 +                // color: 各异,
 +                width: 1
 +            }
 +        },
 +        itemStyle: {
 +            // color: 各异,
 +            borderColor: '#fff',
 +            borderWidth: 1
 +        },
 +        emphasis: {
 +            label: {
 +                show: true
 +            }
 +        },
 +        select: {
 +            itemStyle: {
 +                borderColor: '#212121'
 +            }
 +        }
 +    };
 +
 +}
 +
 +ComponentModel.registerClass(FunnelSeriesModel);
 +
 +export default FunnelSeriesModel;
diff --cc src/chart/funnel/funnelLayout.ts
index 9a6e533,0000000..e309fb9
mode 100644,000000..100644
--- a/src/chart/funnel/funnelLayout.ts
+++ b/src/chart/funnel/funnelLayout.ts
@@@ -1,265 -1,0 +1,390 @@@
 +/*
 +* 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 * as layout from '../../util/layout';
 +import {parsePercent, linearMap} from '../../util/number';
 +import FunnelSeriesModel, { FunnelSeriesOption, FunnelDataItemOption } from './FunnelSeries';
 +import ExtensionAPI from '../../ExtensionAPI';
 +import List from '../../data/List';
 +import GlobalModel from '../../model/Global';
 +
 +function getViewRect(seriesModel: FunnelSeriesModel, api: ExtensionAPI) {
 +    return layout.getLayoutRect(
 +        seriesModel.getBoxLayoutParams(), {
 +            width: api.getWidth(),
 +            height: api.getHeight()
 +        }
 +    );
 +}
 +
 +function getSortedIndices(data: List, sort: FunnelSeriesOption['sort']) {
 +    const valueDim = data.mapDimension('value');
 +    const valueArr = data.mapArray(valueDim, function (val: number) {
 +        return val;
 +    });
 +    const indices: number[] = [];
 +    const isAscending = sort === 'ascending';
 +    for (let i = 0, len = data.count(); i < len; i++) {
 +        indices[i] = i;
 +    }
 +
 +    // Add custom sortable function & none sortable opetion by "options.sort"
 +    if (typeof sort === 'function') {
 +        indices.sort(sort);
 +    }
 +    else if (sort !== 'none') {
 +        indices.sort(function (a, b) {
 +            return isAscending
 +                ? valueArr[a] - valueArr[b]
 +                : valueArr[b] - valueArr[a];
 +        });
 +    }
 +    return indices;
 +}
 +
 +function labelLayout(data: List) {
++    const seriesModel = data.hostModel;
++    const orient = seriesModel.get('orient');
 +    data.each(function (idx) {
 +        const itemModel = data.getItemModel<FunnelDataItemOption>(idx);
 +        const labelModel = itemModel.getModel('label');
-         const labelPosition = labelModel.get('position');
++        let labelPosition = labelModel.get('position');
 +
 +        const labelLineModel = itemModel.getModel('labelLine');
 +
 +        const layout = data.getItemLayout(idx);
 +        const points = layout.points;
 +
 +        const isLabelInside = labelPosition === 'inner'
 +            || labelPosition === 'inside' || labelPosition === 'center'
 +            || labelPosition === 'insideLeft' || labelPosition === 'insideRight';
 +
 +        let textAlign;
 +        let textX;
 +        let textY;
 +        let linePoints;
 +
 +        if (isLabelInside) {
 +            if (labelPosition === 'insideLeft') {
 +                textX = (points[0][0] + points[3][0]) / 2 + 5;
 +                textY = (points[0][1] + points[3][1]) / 2;
 +                textAlign = 'left';
 +            }
 +            else if (labelPosition === 'insideRight') {
 +                textX = (points[1][0] + points[2][0]) / 2 - 5;
 +                textY = (points[1][1] + points[2][1]) / 2;
 +                textAlign = 'right';
 +            }
 +            else {
 +                textX = (points[0][0] + points[1][0] + points[2][0] + points[3][0]) / 4;
 +                textY = (points[0][1] + points[1][1] + points[2][1] + points[3][1]) / 4;
 +                textAlign = 'center';
 +            }
 +            linePoints = [
 +                [textX, textY], [textX, textY]
 +            ];
 +        }
 +        else {
 +            let x1;
 +            let y1;
 +            let x2;
++            let y2;
 +            const labelLineLen = labelLineModel.get('length');
++            if (__DEV__) {
++                if (orient === 'vertical' && ['top', 'bottom'].indexOf(labelPosition as string) > -1) {
++                    labelPosition = 'left';
++                    console.warn('Position error: Funnel chart on vertical orient dose not support top and bottom.');
++                }
++                if (orient === 'horizontal' && ['left', 'right'].indexOf(labelPosition as string) > -1) {
++                    labelPosition = 'bottom';
++                    console.warn('Position error: Funnel chart on horizontal orient dose not support left and right.');
++                }
++            }
 +            if (labelPosition === 'left') {
 +                // Left side
 +                x1 = (points[3][0] + points[0][0]) / 2;
 +                y1 = (points[3][1] + points[0][1]) / 2;
 +                x2 = x1 - labelLineLen;
 +                textX = x2 - 5;
 +                textAlign = 'right';
 +            }
 +            else if (labelPosition === 'right') {
 +                // Right side
 +                x1 = (points[1][0] + points[2][0]) / 2;
 +                y1 = (points[1][1] + points[2][1]) / 2;
 +                x2 = x1 + labelLineLen;
 +                textX = x2 + 5;
 +                textAlign = 'left';
 +            }
++            else if (labelPosition === 'top') {
++                // Top side
++                x1 = (points[3][0] + points[0][0]) / 2;
++                y1 = (points[3][1] + points[0][1]) / 2;
++                y2 = y1 - labelLineLen;
++                textY = y2 - 5;
++                textAlign = 'center';
++            }
++            else if (labelPosition === 'bottom') {
++                // Bottom side
++                x1 = (points[1][0] + points[2][0]) / 2;
++                y1 = (points[1][1] + points[2][1]) / 2;
++                y2 = y1 + labelLineLen;
++                textY = y2 + 5;
++                textAlign = 'center';
++            }
 +            else if (labelPosition === 'rightTop') {
 +                // RightTop side
-                 x1 = points[1][0];
-                 y1 = points[1][1];
-                 x2 = x1 + labelLineLen;
-                 textX = x2 + 5;
-                 textAlign = 'top';
++                x1 = orient === 'horizontal' ? points[3][0] : points[1][0];
++                y1 = orient === 'horizontal' ? points[3][1] : points[1][1];
++                if (orient === 'horizontal') {
++                    y2 = y1 - labelLineLen;
++                    textY = y2 - 5;
++                    textAlign = 'center';
++                }
++                else {
++                    x2 = x1 + labelLineLen;
++                    textX = x2 + 5;
++                    textAlign = 'top';
++                }
 +            }
 +            else if (labelPosition === 'rightBottom') {
 +                // RightBottom side
 +                x1 = points[2][0];
 +                y1 = points[2][1];
-                 x2 = x1 + labelLineLen;
-                 textX = x2 + 5;
-                 textAlign = 'bottom';
++                if (orient === 'horizontal') {
++                    y2 = y1 + labelLineLen;
++                    textY = y2 + 5;
++                    textAlign = 'center';
++                }
++                else {
++                    x2 = x1 + labelLineLen;
++                    textX = x2 + 5;
++                    textAlign = 'bottom';
++                }
 +            }
 +            else if (labelPosition === 'leftTop') {
 +                // LeftTop side
 +                x1 = points[0][0];
-                 y1 = points[1][1];
-                 x2 = x1 - labelLineLen;
-                 textX = x2 - 5;
-                 textAlign = 'right';
++                y1 = orient === 'horizontal' ? points[0][1] : points[1][1];
++                if (orient === 'horizontal') {
++                    y2 = y1 - labelLineLen;
++                    textY = y2 - 5;
++                    textAlign = 'center';
++                }
++                else {
++                    x2 = x1 - labelLineLen;
++                    textX = x2 - 5;
++                    textAlign = 'right';
++                }
 +            }
 +            else if (labelPosition === 'leftBottom') {
 +                // LeftBottom side
-                 x1 = points[3][0];
-                 y1 = points[2][1];
-                 x2 = x1 - labelLineLen;
-                 textX = x2 - 5;
-                 textAlign = 'right';
++                x1 = orient === 'horizontal' ? points[1][0] : points[3][0];
++                y1 = orient === 'horizontal' ? points[1][1] : points[2][1];
++                if (orient === 'horizontal') {
++                    y2 = y1 + labelLineLen;
++                    textY = y2 + 5;
++                    textAlign = 'center';
++                }
++                else {
++                    x2 = x1 - labelLineLen;
++                    textX = x2 - 5;
++                    textAlign = 'right';
++                }
 +            }
 +            else {
-                 // Right side
++                // Right side or Bottom side
 +                x1 = (points[1][0] + points[2][0]) / 2;
 +                y1 = (points[1][1] + points[2][1]) / 2;
-                 x2 = x1 + labelLineLen;
-                 textX = x2 + 5;
-                 textAlign = 'left';
++                if (orient === 'horizontal') {
++                    y2 = y1 + labelLineLen;
++                    textY = y2 + 5;
++                    textAlign = 'center';
++                }
++                else {
++                    x2 = x1 + labelLineLen;
++                    textX = x2 + 5;
++                    textAlign = 'left';
++                }
++            }
++            if (orient === 'horizontal') {
++                x2 = x1;
++                textX = x2;
++            }
++            else {
++                y2 = y1;
++                textY = y2;
 +            }
-             const y2 = y1;
- 
 +            linePoints = [[x1, y1], [x2, y2]];
-             textY = y2;
 +        }
 +
 +        layout.label = {
 +            linePoints: linePoints,
 +            x: textX,
 +            y: textY,
 +            verticalAlign: 'middle',
 +            textAlign: textAlign,
 +            inside: isLabelInside
 +        };
 +    });
 +}
 +
 +export default function (ecModel: GlobalModel, api: ExtensionAPI) {
 +    ecModel.eachSeriesByType('funnel', function (seriesModel: FunnelSeriesModel) {
 +        const data = seriesModel.getData();
 +        const valueDim = data.mapDimension('value');
 +        const sort = seriesModel.get('sort');
 +        const viewRect = getViewRect(seriesModel, api);
++        const orient = seriesModel.get('orient');
++        const viewWidth = viewRect.width;
++        const viewHeight = viewRect.height;
 +        let indices = getSortedIndices(data, sort);
++        let x = viewRect.x;
++        let y = viewRect.y;
 +
-         const sizeExtent = [
-             parsePercent(seriesModel.get('minSize'), viewRect.width),
-             parsePercent(seriesModel.get('maxSize'), viewRect.width)
-         ];
++        const sizeExtent = orient === 'horizontal' ? [
++            parsePercent(seriesModel.get('minSize'), viewHeight),
++            parsePercent(seriesModel.get('maxSize'), viewHeight)
++        ] : [
++                parsePercent(seriesModel.get('minSize'), viewWidth),
++                parsePercent(seriesModel.get('maxSize'), viewWidth)
++            ];
 +        const dataExtent = data.getDataExtent(valueDim);
 +        let min = seriesModel.get('min');
 +        let max = seriesModel.get('max');
 +        if (min == null) {
 +            min = Math.min(dataExtent[0], 0);
 +        }
 +        if (max == null) {
 +            max = dataExtent[1];
 +        }
 +
 +        const funnelAlign = seriesModel.get('funnelAlign');
 +        let gap = seriesModel.get('gap');
-         let itemHeight = (viewRect.height - gap * (data.count() - 1)) / data.count();
++        const viewSize = orient === 'horizontal' ? viewWidth : viewHeight;
++        let itemSize = (viewSize - gap * (data.count() - 1)) / data.count();
 +
-         let y = viewRect.y;
- 
-         const getLinePoints = function (idx: number, offY: number) {
++        const getLinePoints = function (idx: number, offset: number) {
 +            // End point index is data.count() and we assign it 0
++            if (orient === 'horizontal') {
++                const val = data.get(valueDim, idx) as number || 0;
++                const itemHeight = linearMap(val, [min, max], sizeExtent, true);
++                let y0;
++                switch (funnelAlign) {
++                    case 'top':
++                        y0 = y;
++                        break;
++                    case 'center':
++                        y0 = y + (viewHeight - itemHeight) / 2;
++                        break;
++                    case 'bottom':
++                        y0 = y + (viewHeight - itemHeight);
++                        break;
++                }
++
++                return [
++                    [offset, y0],
++                    [offset, y0 + itemHeight]
++                ];
++            }
 +            const val = data.get(valueDim, idx) as number || 0;
 +            const itemWidth = linearMap(val, [min, max], sizeExtent, true);
 +            let x0;
 +            switch (funnelAlign) {
 +                case 'left':
-                     x0 = viewRect.x;
++                    x0 = x;
 +                    break;
 +                case 'center':
-                     x0 = viewRect.x + (viewRect.width - itemWidth) / 2;
++                    x0 = x + (viewWidth - itemWidth) / 2;
 +                    break;
 +                case 'right':
-                     x0 = viewRect.x + viewRect.width - itemWidth;
++                    x0 = x + viewWidth - itemWidth;
 +                    break;
 +            }
 +            return [
-                 [x0, offY],
-                 [x0 + itemWidth, offY]
++                [x0, offset],
++                [x0 + itemWidth, offset]
 +            ];
 +        };
 +
 +        if (sort === 'ascending') {
 +            // From bottom to top
-             itemHeight = -itemHeight;
++            itemSize = -itemSize;
 +            gap = -gap;
-             y += viewRect.height;
++            if (orient === 'horizontal') {
++                x += viewWidth;
++            }
++            else {
++                y += viewHeight;
++            }
 +            indices = indices.reverse();
 +        }
 +
 +        for (let i = 0; i < indices.length; i++) {
 +            const idx = indices[i];
 +            const nextIdx = indices[i + 1];
- 
 +            const itemModel = data.getItemModel<FunnelDataItemOption>(idx);
-             let height = itemModel.get(['itemStyle', 'height']);
-             if (height == null) {
-                 height = itemHeight;
++
++            if (orient === 'horizontal') {
++                let width = itemModel.get(['itemStyle', 'width']);
++                if (width == null) {
++                    width = itemSize;
++                }
++                else {
++                    width = parsePercent(width, viewWidth);
++                    if (sort === 'ascending') {
++                        width = -width;
++                    }
++                }
++
++                const start = getLinePoints(idx, x);
++                const end = getLinePoints(nextIdx, x + width);
++
++                x += width + gap;
++
++                data.setItemLayout(idx, {
++                    points: start.concat(end.slice().reverse())
++                });
 +            }
 +            else {
-                 height = parsePercent(height, viewRect.height);
-                 if (sort === 'ascending') {
-                     height = -height;
++                let height = itemModel.get(['itemStyle', 'height']);
++                if (height == null) {
++                    height = itemSize;
++                }
++                else {
++                    height = parsePercent(height, viewHeight);
++                    if (sort === 'ascending') {
++                        height = -height;
++                    }
 +                }
-             }
 +
-             const start = getLinePoints(idx, y);
-             const end = getLinePoints(nextIdx, y + height);
++                const start = getLinePoints(idx, y);
++                const end = getLinePoints(nextIdx, y + height);
 +
-             y += height + gap;
++                y += height + gap;
 +
-             data.setItemLayout(idx, {
-                 points: start.concat(end.slice().reverse())
-             });
++                data.setItemLayout(idx, {
++                    points: start.concat(end.slice().reverse())
++                });
++            }
 +        }
 +
 +        labelLayout(data);
 +    });
 +}
diff --cc src/chart/graph/GraphSeries.ts
index 1623c9c,0000000..59bd859
mode 100644,000000..100644
--- a/src/chart/graph/GraphSeries.ts
+++ b/src/chart/graph/GraphSeries.ts
@@@ -1,505 -1,0 +1,512 @@@
 +/*
 +* 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 List from '../../data/List';
 +import * as zrUtil from 'zrender/src/core/util';
 +import {defaultEmphasis} from '../../util/model';
 +import Model from '../../model/Model';
 +import createGraphFromNodeEdge from '../helper/createGraphFromNodeEdge';
 +import LegendVisualProvider from '../../visual/LegendVisualProvider';
 +import {
 +    SeriesOption,
 +    SeriesOnCartesianOptionMixin,
 +    SeriesOnPolarOptionMixin,
 +    SeriesOnCalendarOptionMixin,
 +    SeriesOnGeoOptionMixin,
 +    SeriesOnSingleOptionMixin,
 +    OptionDataValue,
 +    RoamOptionMixin,
 +    LabelOption,
 +    ItemStyleOption,
 +    LineStyleOption,
 +    SymbolOptionMixin,
 +    BoxLayoutOptionMixin,
 +    LabelFormatterCallback,
 +    Dictionary,
 +    LineLabelOption,
 +    StatesOptionMixin,
 +    GraphEdgeItemObject,
 +    OptionDataValueNumeric
 +} from '../../util/types';
 +import SeriesModel from '../../model/Series';
 +import Graph from '../../data/Graph';
 +import GlobalModel from '../../model/Global';
 +import { VectorArray } from 'zrender/src/core/vector';
 +import { ForceLayoutInstance } from './forceLayout';
 +import { LineDataVisual } from '../../visual/commonVisualTypes';
 +import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup';
 +import { defaultSeriesFormatTooltip } from '../../component/tooltip/seriesFormatTooltip';
++import {initCurvenessList, createEdgeMapForCurveness} from '../helper/multipleGraphEdgeHelper';
++
 +
 +type GraphDataValue = OptionDataValue | OptionDataValue[];
 +
 +interface GraphEdgeLineStyleOption extends LineStyleOption {
 +    curveness?: number
 +}
 +
 +export interface GraphNodeStateOption {
 +    itemStyle?: ItemStyleOption
 +    label?: LabelOption
 +}
 +
 +interface ExtraNodeStateOption {
 +    emphasis?: {
 +        focus?: 'adjacency'
 +        scale?: boolean
 +    }
 +}
 +
 +interface ExtraEdgeStateOption {
 +    emphasis?: {
 +        focus?: 'adjacency'
 +    }
 +}
 +
 +export interface GraphNodeItemOption extends SymbolOptionMixin, GraphNodeStateOption,
 +    GraphNodeStateOption, StatesOptionMixin<GraphNodeStateOption, ExtraNodeStateOption> {
 +
 +    id?: string
 +    name?: string
 +    value?: GraphDataValue
 +
 +    /**
 +     * Fixed x position
 +     */
 +    x?: number
 +    /**
 +     * Fixed y position
 +     */
 +    y?: number
 +
 +    /**
 +     * If this node is fixed during force layout.
 +     */
 +    fixed?: boolean
 +
 +    /**
 +     * Index or name of category
 +     */
 +    category?: number | string
 +
 +    draggable?: boolean
 +}
 +
 +export interface GraphEdgeStateOption {
 +    lineStyle?: GraphEdgeLineStyleOption
 +    label?: LineLabelOption
 +}
 +export interface GraphEdgeItemOption extends
 +        GraphEdgeStateOption,
 +        StatesOptionMixin<GraphEdgeStateOption, ExtraEdgeStateOption>,
 +        GraphEdgeItemObject<OptionDataValueNumeric> {
 +
 +    value?: number
 +
 +    /**
 +     * Symbol of both line ends
 +     */
 +    symbol?: string | string[]
 +
 +    symbolSize?: number | number[]
 +
 +    ignoreForceLayout?: boolean
 +}
 +
 +export interface GraphCategoryItemOption extends SymbolOptionMixin,
 +    GraphNodeStateOption, StatesOptionMixin<GraphNodeStateOption> {
 +    name?: string
 +
 +    value?: OptionDataValue
 +}
 +
 +export interface GraphSeriesOption extends SeriesOption,
 +    SeriesOnCartesianOptionMixin, SeriesOnPolarOptionMixin, SeriesOnCalendarOptionMixin,
 +    SeriesOnGeoOptionMixin, SeriesOnSingleOptionMixin,
 +    SymbolOptionMixin,
 +    RoamOptionMixin,
 +    BoxLayoutOptionMixin {
 +
 +    type?: 'graph'
 +
 +    coordinateSystem?: string
 +
 +    legendHoverLink?: boolean
 +
 +    layout?: 'none' | 'force' | 'circular'
 +
 +    data?: (GraphNodeItemOption | GraphDataValue)[]
 +    nodes?: (GraphNodeItemOption | GraphDataValue)[]
 +
 +    edges?: GraphEdgeItemOption[]
 +    links?: GraphEdgeItemOption[]
 +
 +    categories?: GraphCategoryItemOption[]
 +
 +    focusNodeAdjacency?: boolean
 +
 +    /**
 +     * Symbol size scale ratio in roam
 +     */
 +    nodeScaleRatio?: 0.6,
 +
 +    draggable?: boolean
 +
 +    edgeSymbol?: string | string[]
 +    edgeSymbolSize?: number | number[]
 +
 +    edgeLabel?: LineLabelOption & {
 +        formatter?: LabelFormatterCallback | string
 +    }
 +    label?: LabelOption & {
 +        formatter?: LabelFormatterCallback | string
 +    }
 +
 +    itemStyle?: ItemStyleOption
 +    lineStyle?: GraphEdgeLineStyleOption
 +
 +    emphasis?: {
 +        focus?: GraphNodeItemOption['emphasis']['focus']
 +        scale?: boolean
 +        label?: LabelOption
 +        edgeLabel?: LabelOption
 +        itemStyle?: ItemStyleOption
 +        lineStyle?: LineStyleOption
 +    }
 +
 +    blur?: {
 +        label?: LabelOption
 +        edgeLabel?: LabelOption
 +        itemStyle?: ItemStyleOption
 +        lineStyle?: LineStyleOption
 +    }
 +
 +    select?: {
 +        label?: LabelOption
 +        edgeLabel?: LabelOption
 +        itemStyle?: ItemStyleOption
 +        lineStyle?: LineStyleOption
 +    }
 +
 +    // Configuration of circular layout
 +    circular?: {
 +        rotateLabel?: boolean
 +    }
 +
 +    // Configuration of force directed layout
 +    force?: {
 +        initLayout?: 'circular' | 'none'
 +        // Node repulsion. Can be an array to represent range.
 +        repulsion?: number | number[]
 +        gravity?: number
 +        // Initial friction
 +        friction?: number
 +
 +        // Edge length. Can be an array to represent range.
 +        edgeLength?: number | number[]
 +
 +        layoutAnimation?: boolean
 +    }
 +}
 +
 +class GraphSeriesModel extends SeriesModel<GraphSeriesOption> {
 +    static readonly type = 'series.graph';
 +    readonly type = GraphSeriesModel.type;
 +
 +    private _categoriesData: List;
 +    private _categoriesModels: Model<GraphCategoryItemOption>[];
 +
 +    /**
 +     * Preserved points during layouting
 +     */
 +    preservedPoints?: Dictionary<VectorArray>;
 +
 +    forceLayout?: ForceLayoutInstance;
 +
 +    hasSymbolVisual = true;
 +
 +    init(option: GraphSeriesOption) {
 +        super.init.apply(this, arguments as any);
 +
 +        const self = this;
 +        function getCategoriesData() {
 +            return self._categoriesData;
 +        }
 +        // Provide data for legend select
 +        this.legendVisualProvider = new LegendVisualProvider(
 +            getCategoriesData, getCategoriesData
 +        );
 +
 +        this.fillDataTextStyle(option.edges || option.links);
 +
 +        this._updateCategoriesData();
 +    }
 +
 +    mergeOption(option: GraphSeriesOption) {
 +        super.mergeOption.apply(this, arguments as any);
 +
 +        this.fillDataTextStyle(option.edges || option.links);
 +
 +        this._updateCategoriesData();
 +    }
 +
 +    mergeDefaultAndTheme(option: GraphSeriesOption) {
 +        super.mergeDefaultAndTheme.apply(this, arguments as any);
 +        defaultEmphasis(option, 'edgeLabel', ['show']);
 +    }
 +
 +    getInitialData(option: GraphSeriesOption, ecModel: GlobalModel): List {
 +        const edges = option.edges || option.links || [];
 +        const nodes = option.data || option.nodes || [];
 +        const self = this;
 +
 +        if (nodes && edges) {
-             return createGraphFromNodeEdge(nodes as GraphNodeItemOption[], edges, this, true, beforeLink).data;
++            // auto curveness
++            initCurvenessList(this);
++            const graph = createGraphFromNodeEdge(nodes as GraphNodeItemOption[], edges, this, true, beforeLink);
++            zrUtil.each(graph.edges, function (edge) {
++                createEdgeMapForCurveness(edge.node1, edge.node2, this, edge.dataIndex);
++            }, this);
++            return graph.data;
 +        }
 +
 +        function beforeLink(nodeData: List, edgeData: List) {
 +            // Overwrite nodeData.getItemModel to
 +            nodeData.wrapMethod('getItemModel', function (model) {
 +                const categoriesModels = self._categoriesModels;
 +                const categoryIdx = model.getShallow('category');
 +                const categoryModel = categoriesModels[categoryIdx];
 +                if (categoryModel) {
 +                    categoryModel.parentModel = model.parentModel;
 +                    model.parentModel = categoryModel;
 +                }
 +                return model;
 +            });
 +
 +            // TODO Inherit resolveParentPath by default in Model#getModel?
 +            const oldGetModel = Model.prototype.getModel;
 +            function newGetModel(this: Model, path: any, parentModel?: Model) {
 +                const model = oldGetModel.call(this, path, parentModel);
 +                model.resolveParentPath = resolveParentPath;
 +                return model;
 +            }
 +
 +            edgeData.wrapMethod('getItemModel', function (model: Model) {
 +                model.resolveParentPath = resolveParentPath;
 +                model.getModel = newGetModel;
 +                return model;
 +            });
 +
 +            function resolveParentPath(this: Model, pathArr: readonly string[]): string[] {
 +                if (pathArr && (pathArr[0] === 'label' || pathArr[1] === 'label')) {
 +                    const newPathArr = pathArr.slice();
 +                    if (pathArr[0] === 'label') {
 +                        newPathArr[0] = 'edgeLabel';
 +                    }
 +                    else if (pathArr[1] === 'label') {
 +                        newPathArr[1] = 'edgeLabel';
 +                    }
 +                    return newPathArr;
 +                }
 +                return pathArr as string[];
 +            }
 +        }
 +    }
 +
 +    getGraph(): Graph {
 +        return this.getData().graph;
 +    }
 +
 +    getEdgeData() {
 +        return this.getGraph().edgeData as List<GraphSeriesModel, LineDataVisual>;
 +    }
 +
 +    getCategoriesData(): List {
 +        return this._categoriesData;
 +    }
 +
 +    formatTooltip(
 +        dataIndex: number,
 +        multipleSeries: boolean,
 +        dataType: string
 +    ) {
 +        if (dataType === 'edge') {
 +            const nodeData = this.getData();
 +            const params = this.getDataParams(dataIndex, dataType);
 +            const edge = nodeData.graph.getEdgeByIndex(dataIndex);
 +            const sourceName = nodeData.getName(edge.node1.dataIndex);
 +            const targetName = nodeData.getName(edge.node2.dataIndex);
 +
 +            const nameArr = [];
 +            sourceName != null && nameArr.push(sourceName);
 +            targetName != null && nameArr.push(targetName);
 +
 +            return createTooltipMarkup('nameValue', {
 +                name: nameArr.join(' > '),
 +                value: params.value,
 +                noValue: params.value == null
 +            });
 +        }
 +        // dataType === 'node' or empty
 +        const nodeMarkup = defaultSeriesFormatTooltip({
 +            series: this,
 +            dataIndex: dataIndex,
 +            multipleSeries: multipleSeries
 +        });
 +        return nodeMarkup;
 +    }
 +
 +    _updateCategoriesData() {
 +        const categories = zrUtil.map(this.option.categories || [], function (category) {
 +            // Data must has value
 +            return category.value != null ? category : zrUtil.extend({
 +                value: 0
 +            }, category);
 +        });
 +        const categoriesData = new List(['value'], this);
 +        categoriesData.initData(categories);
 +
 +        this._categoriesData = categoriesData;
 +
 +        this._categoriesModels = categoriesData.mapArray(function (idx) {
 +            return categoriesData.getItemModel(idx);
 +        });
 +    }
 +
 +    setZoom(zoom: number) {
 +        this.option.zoom = zoom;
 +    }
 +
 +    setCenter(center: number[]) {
 +        this.option.center = center;
 +    }
 +
 +    isAnimationEnabled() {
 +        return super.isAnimationEnabled()
 +            // Not enable animation when do force layout
 +            && !(this.get('layout') === 'force' && this.get(['force', 'layoutAnimation']));
 +    }
 +
 +    static defaultOption: GraphSeriesOption = {
 +        zlevel: 0,
 +        z: 2,
 +
 +        coordinateSystem: 'view',
 +
 +        // Default option for all coordinate systems
 +        // xAxisIndex: 0,
 +        // yAxisIndex: 0,
 +        // polarIndex: 0,
 +        // geoIndex: 0,
 +
 +        legendHoverLink: true,
 +
 +        layout: null,
 +
 +        focusNodeAdjacency: false,
 +
 +        // Configuration of circular layout
 +        circular: {
 +            rotateLabel: false
 +        },
 +        // Configuration of force directed layout
 +        force: {
 +            initLayout: null,
 +            // Node repulsion. Can be an array to represent range.
 +            repulsion: [0, 50],
 +            gravity: 0.1,
 +            // Initial friction
 +            friction: 0.6,
 +
 +            // Edge length. Can be an array to represent range.
 +            edgeLength: 30,
 +
 +            layoutAnimation: true
 +        },
 +
 +        left: 'center',
 +        top: 'center',
 +        // right: null,
 +        // bottom: null,
 +        // width: '80%',
 +        // height: '80%',
 +
 +        symbol: 'circle',
 +        symbolSize: 10,
 +
 +        edgeSymbol: ['none', 'none'],
 +        edgeSymbolSize: 10,
 +        edgeLabel: {
 +            position: 'middle',
 +            distance: 5
 +        },
 +
 +        draggable: false,
 +
 +        roam: false,
 +
 +        // Default on center of graph
 +        center: null,
 +
 +        zoom: 1,
 +        // Symbol size scale ratio in roam
 +        nodeScaleRatio: 0.6,
 +
 +        // cursor: null,
 +
 +        // categories: [],
 +
 +        // data: []
 +        // Or
 +        // nodes: []
 +        //
 +        // links: []
 +        // Or
 +        // edges: []
 +
 +        label: {
 +            show: false,
 +            formatter: '{b}'
 +        },
 +
 +        itemStyle: {},
 +
 +        lineStyle: {
 +            color: '#aaa',
 +            width: 1,
-             curveness: 0,
 +            opacity: 0.5
 +        },
 +        emphasis: {
 +            scale: true,
 +            label: {
 +                show: true
 +            }
 +        },
 +
 +        select: {
 +            itemStyle: {
 +                borderColor: '#212121'
 +            }
 +        }
 +    };
 +}
 +
 +SeriesModel.registerClass(GraphSeriesModel);
 +
 +export default GraphSeriesModel;
diff --cc src/chart/graph/circularLayoutHelper.ts
index 4364763,0000000..a81538c
mode 100644,000000..100644
--- a/src/chart/graph/circularLayoutHelper.ts
+++ b/src/chart/graph/circularLayoutHelper.ts
@@@ -1,167 -1,0 +1,173 @@@
 +/*
 +* 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 * as vec2 from 'zrender/src/core/vector';
 +import {getSymbolSize, getNodeGlobalScale} from './graphHelper';
 +import GraphSeriesModel, { GraphEdgeItemOption } from './GraphSeries';
 +import Graph from '../../data/Graph';
 +import List from '../../data/List';
++import * as zrUtil from 'zrender/src/core/util';
++import {getCurvenessForEdge} from '../helper/multipleGraphEdgeHelper';
 +
 +const PI = Math.PI;
 +
 +const _symbolRadiansHalf: number[] = [];
 +
 +/**
 + * `basedOn` can be:
 + * 'value':
 + *     This layout is not accurate and have same bad case. For example,
 + *     if the min value is very smaller than the max value, the nodes
 + *     with the min value probably overlap even though there is enough
 + *     space to layout them. So we only use this approach in the as the
 + *     init layout of the force layout.
 + *     FIXME
 + *     Probably we do not need this method any more but use
 + *     `basedOn: 'symbolSize'` in force layout if
 + *     delay its init operations to GraphView.
 + * 'symbolSize':
 + *     This approach work only if all of the symbol size calculated.
 + *     That is, the progressive rendering is not applied to graph.
 + *     FIXME
 + *     If progressive rendering is applied to graph some day,
 + *     probably we have to use `basedOn: 'value'`.
 + */
 +export function circularLayout(
 +    seriesModel: GraphSeriesModel,
 +    basedOn: 'value' | 'symbolSize'
 +) {
 +    const coordSys = seriesModel.coordinateSystem;
 +    if (coordSys && coordSys.type !== 'view') {
 +        return;
 +    }
 +
 +    const rect = coordSys.getBoundingRect();
 +
 +    const nodeData = seriesModel.getData();
 +    const graph = nodeData.graph;
 +
 +    const cx = rect.width / 2 + rect.x;
 +    const cy = rect.height / 2 + rect.y;
 +    const r = Math.min(rect.width, rect.height) / 2;
 +    const count = nodeData.count();
 +
 +    nodeData.setLayout({
 +        cx: cx,
 +        cy: cy
 +    });
 +
 +    if (!count) {
 +        return;
 +    }
 +
 +    _layoutNodesBasedOn[basedOn](seriesModel, graph, nodeData, r, cx, cy, count);
 +
-     graph.eachEdge(function (edge) {
-         let curveness = edge.getModel<GraphEdgeItemOption>().get(['lineStyle', 'curveness']) || 0;
++    graph.eachEdge(function (edge, index) {
++        let curveness = zrUtil.retrieve3(
++            edge.getModel<GraphEdgeItemOption>().get(['lineStyle', 'curveness']),
++            getCurvenessForEdge(edge, seriesModel, index),
++            0
++        );
 +        const p1 = vec2.clone(edge.node1.getLayout());
 +        const p2 = vec2.clone(edge.node2.getLayout());
 +        let cp1;
 +        const x12 = (p1[0] + p2[0]) / 2;
 +        const y12 = (p1[1] + p2[1]) / 2;
 +        if (+curveness) {
 +            curveness *= 3;
 +            cp1 = [
 +                cx * curveness + x12 * (1 - curveness),
 +                cy * curveness + y12 * (1 - curveness)
 +            ];
 +        }
 +        edge.setLayout([p1, p2, cp1]);
 +    });
 +}
 +
 +interface LayoutNode {
 +    (
 +        seriesModel: GraphSeriesModel,
 +        graph: Graph,
 +        nodeData: List,
 +        r: number,
 +        cx: number,
 +        cy: number,
 +        count: number
 +    ): void
 +}
 +
 +const _layoutNodesBasedOn: Record<'value' | 'symbolSize', LayoutNode> = {
 +
 +    value(seriesModel, graph, nodeData, r, cx, cy, count) {
 +        let angle = 0;
 +        const sum = nodeData.getSum('value');
 +        const unitAngle = Math.PI * 2 / (sum || count);
 +
 +        graph.eachNode(function (node) {
 +            const value = node.getValue('value') as number;
 +            const radianHalf = unitAngle * (sum ? value : 1) / 2;
 +
 +            angle += radianHalf;
 +            node.setLayout([
 +                r * Math.cos(angle) + cx,
 +                r * Math.sin(angle) + cy
 +            ]);
 +            angle += radianHalf;
 +        });
 +    },
 +
 +    symbolSize(seriesModel, graph, nodeData, r, cx, cy, count) {
 +        let sumRadian = 0;
 +        _symbolRadiansHalf.length = count;
 +
 +        const nodeScale = getNodeGlobalScale(seriesModel);
 +
 +        graph.eachNode(function (node) {
 +            let symbolSize = getSymbolSize(node);
 +
 +            // Normally this case will not happen, but we still add
 +            // some the defensive code (2px is an arbitrary value).
 +            isNaN(symbolSize) && (symbolSize = 2);
 +            symbolSize < 0 && (symbolSize = 0);
 +
 +            symbolSize *= nodeScale;
 +
 +            let symbolRadianHalf = Math.asin(symbolSize / 2 / r);
 +            // when `symbolSize / 2` is bigger than `r`.
 +            isNaN(symbolRadianHalf) && (symbolRadianHalf = PI / 2);
 +            _symbolRadiansHalf[node.dataIndex] = symbolRadianHalf;
 +            sumRadian += symbolRadianHalf * 2;
 +        });
 +
 +        const halfRemainRadian = (2 * PI - sumRadian) / count / 2;
 +
 +        let angle = 0;
 +        graph.eachNode(function (node) {
 +            const radianHalf = halfRemainRadian + _symbolRadiansHalf[node.dataIndex];
 +
 +            angle += radianHalf;
 +            node.setLayout([
 +                r * Math.cos(angle) + cx,
 +                r * Math.sin(angle) + cy
 +            ]);
 +            angle += radianHalf;
 +        });
 +    }
 +};
diff --cc src/chart/graph/edgeVisual.ts
index ba298b1,0000000..a134794
mode 100644,000000..100644
--- a/src/chart/graph/edgeVisual.ts
+++ b/src/chart/graph/edgeVisual.ts
@@@ -1,88 -1,0 +1,80 @@@
 +/*
 +* 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 GlobalModel from '../../model/Global';
 +import GraphSeriesModel, { GraphEdgeItemOption } from './GraphSeries';
- import { DefaultDataVisual } from '../../data/List';
 +import { extend } from 'zrender/src/core/util';
 +
 +function normalize(a: string | string[]): string[];
 +function normalize(a: number | number[]): number[];
 +function normalize(a: string | number | (string | number)[]): (string | number)[] {
 +    if (!(a instanceof Array)) {
 +        a = [a, a];
 +    }
 +    return a;
 +}
 +
- interface EdgeLineDataVisual extends DefaultDataVisual {
-     fromSymbol: string
-     toSymbol: string
-     fromSymbolSize: number
-     toSymbolSize: number
- }
- 
 +export default function (ecModel: GlobalModel) {
 +    ecModel.eachSeriesByType('graph', function (seriesModel: GraphSeriesModel) {
 +        const graph = seriesModel.getGraph();
 +        const edgeData = seriesModel.getEdgeData();
 +        const symbolType = normalize(seriesModel.get('edgeSymbol'));
 +        const symbolSize = normalize(seriesModel.get('edgeSymbolSize'));
 +
 +        // const colorQuery = ['lineStyle', 'color'] as const;
 +        // const opacityQuery = ['lineStyle', 'opacity'] as const;
 +
 +        edgeData.setVisual('fromSymbol', symbolType && symbolType[0]);
 +        edgeData.setVisual('toSymbol', symbolType && symbolType[1]);
 +        edgeData.setVisual('fromSymbolSize', symbolSize && symbolSize[0]);
 +        edgeData.setVisual('toSymbolSize', symbolSize && symbolSize[1]);
 +
 +        edgeData.setVisual('style', seriesModel.getModel('lineStyle').getLineStyle());
 +
 +        edgeData.each(function (idx) {
 +            const itemModel = edgeData.getItemModel<GraphEdgeItemOption>(idx);
 +            const edge = graph.getEdgeByIndex(idx);
 +            const symbolType = normalize(itemModel.getShallow('symbol', true));
 +            const symbolSize = normalize(itemModel.getShallow('symbolSize', true));
 +            // Edge visual must after node visual
 +            const style = itemModel.getModel('lineStyle').getLineStyle();
 +
 +            const existsStyle = edgeData.ensureUniqueItemVisual(idx, 'style');
 +            extend(existsStyle, style);
 +
 +            switch (existsStyle.stroke) {
 +                case 'source': {
 +                    const nodeStyle = edge.node1.getVisual('style');
 +                    existsStyle.stroke = nodeStyle && nodeStyle.fill;
 +                    break;
 +                }
 +                case 'target': {
 +                    const nodeStyle = edge.node2.getVisual('style');
 +                    existsStyle.stroke = nodeStyle && nodeStyle.fill;
 +                    break;
 +                }
 +            }
 +
 +            symbolType[0] && edge.setVisual('fromSymbol', symbolType[0]);
 +            symbolType[1] && edge.setVisual('toSymbol', symbolType[1]);
 +            symbolSize[0] && edge.setVisual('fromSymbolSize', symbolSize[0]);
 +            symbolSize[1] && edge.setVisual('toSymbolSize', symbolSize[1]);
 +        });
 +    });
 +}
diff --cc src/chart/graph/forceLayout.ts
index 1fbbed8,0000000..70a1d1d
mode 100644,000000..100644
--- a/src/chart/graph/forceLayout.ts
+++ b/src/chart/graph/forceLayout.ts
@@@ -1,161 -1,0 +1,167 @@@
 +/*
 +* 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 {forceLayout} from './forceHelper';
 +import {simpleLayout} from './simpleLayoutHelper';
 +import {circularLayout} from './circularLayoutHelper';
 +import {linearMap} from '../../util/number';
 +import * as vec2 from 'zrender/src/core/vector';
 +import * as zrUtil from 'zrender/src/core/util';
 +import GlobalModel from '../../model/Global';
 +import GraphSeriesModel, { GraphNodeItemOption, GraphEdgeItemOption } from './GraphSeries';
++import {getCurvenessForEdge} from '../helper/multipleGraphEdgeHelper';
 +
 +export interface ForceLayoutInstance {
 +    step(cb: (stopped: boolean) => void): void
 +    warmUp(): void
 +    setFixed(idx: number): void
 +    setUnfixed(idx: number): void
 +}
 +
 +
 +export default function (ecModel: GlobalModel) {
 +    ecModel.eachSeriesByType('graph', function (graphSeries: GraphSeriesModel) {
 +        const coordSys = graphSeries.coordinateSystem;
 +        if (coordSys && coordSys.type !== 'view') {
 +            return;
 +        }
 +        if (graphSeries.get('layout') === 'force') {
 +            const preservedPoints = graphSeries.preservedPoints || {};
 +            const graph = graphSeries.getGraph();
 +            const nodeData = graph.data;
 +            const edgeData = graph.edgeData;
 +            const forceModel = graphSeries.getModel('force');
 +            const initLayout = forceModel.get('initLayout');
 +            if (graphSeries.preservedPoints) {
 +                nodeData.each(function (idx) {
 +                    const id = nodeData.getId(idx);
 +                    nodeData.setItemLayout(idx, preservedPoints[id] || [NaN, NaN]);
 +                });
 +            }
 +            else if (!initLayout || initLayout === 'none') {
 +                simpleLayout(graphSeries);
 +            }
 +            else if (initLayout === 'circular') {
 +                circularLayout(graphSeries, 'value');
 +            }
 +
 +            const nodeDataExtent = nodeData.getDataExtent('value');
 +            const edgeDataExtent = edgeData.getDataExtent('value');
 +            // let edgeDataExtent = edgeData.getDataExtent('value');
 +            const repulsion = forceModel.get('repulsion');
 +            const edgeLength = forceModel.get('edgeLength');
 +            const repulsionArr = zrUtil.isArray(repulsion)
 +                ? repulsion : [repulsion, repulsion];
 +            let edgeLengthArr = zrUtil.isArray(edgeLength)
 +                ? edgeLength : [edgeLength, edgeLength];
 +
 +            // Larger value has smaller length
 +            edgeLengthArr = [edgeLengthArr[1], edgeLengthArr[0]];
 +
 +            const nodes = nodeData.mapArray('value', function (value: number, idx) {
 +                const point = nodeData.getItemLayout(idx) as number[];
 +                let rep = linearMap(value, nodeDataExtent, repulsionArr);
 +                if (isNaN(rep)) {
 +                    rep = (repulsionArr[0] + repulsionArr[1]) / 2;
 +                }
 +                return {
 +                    w: rep,
 +                    rep: rep,
 +                    fixed: nodeData.getItemModel<GraphNodeItemOption>(idx).get('fixed'),
 +                    p: (!point || isNaN(point[0]) || isNaN(point[1])) ? null : point
 +                };
 +            });
 +            const edges = edgeData.mapArray('value', function (value: number, idx) {
 +                const edge = graph.getEdgeByIndex(idx);
 +                let d = linearMap(value, edgeDataExtent, edgeLengthArr);
 +                if (isNaN(d)) {
 +                    d = (edgeLengthArr[0] + edgeLengthArr[1]) / 2;
 +                }
 +                const edgeModel = edge.getModel<GraphEdgeItemOption>();
++                const curveness = zrUtil.retrieve3(
++                    edge.getModel<GraphEdgeItemOption>().get(['lineStyle', 'curveness']),
++                    -getCurvenessForEdge(edge, graphSeries, idx, true),
++                    0
++                );
 +                return {
 +                    n1: nodes[edge.node1.dataIndex],
 +                    n2: nodes[edge.node2.dataIndex],
 +                    d: d,
-                     curveness: edgeModel.get(['lineStyle', 'curveness']) || 0,
++                    curveness,
 +                    ignoreForceLayout: edgeModel.get('ignoreForceLayout')
 +                };
 +            });
 +
 +            // let coordSys = graphSeries.coordinateSystem;
 +            const rect = coordSys.getBoundingRect();
 +            const forceInstance = forceLayout(nodes, edges, {
 +                rect: rect,
 +                gravity: forceModel.get('gravity'),
 +                friction: forceModel.get('friction')
 +            });
 +            forceInstance.beforeStep(function (nodes, edges) {
 +                for (let i = 0, l = nodes.length; i < l; i++) {
 +                    if (nodes[i].fixed) {
 +                        // Write back to layout instance
 +                        vec2.copy(
 +                            nodes[i].p,
 +                            graph.getNodeByIndex(i).getLayout() as number[]
 +                        );
 +                    }
 +                }
 +            });
 +            forceInstance.afterStep(function (nodes, edges, stopped) {
 +                for (let i = 0, l = nodes.length; i < l; i++) {
 +                    if (!nodes[i].fixed) {
 +                        graph.getNodeByIndex(i).setLayout(nodes[i].p);
 +                    }
 +                    preservedPoints[nodeData.getId(i)] = nodes[i].p;
 +                }
 +                for (let i = 0, l = edges.length; i < l; i++) {
 +                    const e = edges[i];
 +                    const edge = graph.getEdgeByIndex(i);
 +                    const p1 = e.n1.p;
 +                    const p2 = e.n2.p;
 +                    let points = edge.getLayout() as number[][];
 +                    points = points ? points.slice() : [];
 +                    points[0] = points[0] || [];
 +                    points[1] = points[1] || [];
 +                    vec2.copy(points[0], p1);
 +                    vec2.copy(points[1], p2);
 +                    if (+e.curveness) {
 +                        points[2] = [
 +                            (p1[0] + p2[0]) / 2 - (p1[1] - p2[1]) * e.curveness,
 +                            (p1[1] + p2[1]) / 2 - (p2[0] - p1[0]) * e.curveness
 +                        ];
 +                    }
 +                    edge.setLayout(points);
 +                }
 +            });
 +            graphSeries.forceLayout = forceInstance;
 +            graphSeries.preservedPoints = preservedPoints;
 +
 +            // Step to get the layout
 +            forceInstance.step();
 +        }
 +        else {
 +            // Remove prev injected forceLayout instance
 +            graphSeries.forceLayout = null;
 +        }
 +    });
 +}
diff --cc src/chart/graph/simpleLayout.ts
index 945b138,0000000..8b6d785
mode 100644,000000..100644
--- a/src/chart/graph/simpleLayout.ts
+++ b/src/chart/graph/simpleLayout.ts
@@@ -1,63 -1,0 +1,63 @@@
 +/*
 +* 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 {each} from 'zrender/src/core/util';
 +import {simpleLayout, simpleLayoutEdge} from './simpleLayoutHelper';
 +import GlobalModel from '../../model/Global';
 +import ExtensionAPI from '../../ExtensionAPI';
 +import GraphSeriesModel from './GraphSeries';
 +
 +export default function (ecModel: GlobalModel, api: ExtensionAPI) {
 +    ecModel.eachSeriesByType('graph', function (seriesModel: GraphSeriesModel) {
 +        const layout = seriesModel.get('layout');
 +        const coordSys = seriesModel.coordinateSystem;
 +        if (coordSys && coordSys.type !== 'view') {
 +            const data = seriesModel.getData();
 +
 +            let dimensions: string[] = [];
 +            each(coordSys.dimensions, function (coordDim) {
 +                dimensions = dimensions.concat(data.mapDimensionsAll(coordDim));
 +            });
 +
 +            for (let dataIndex = 0; dataIndex < data.count(); dataIndex++) {
 +                const value = [];
 +                let hasValue = false;
 +                for (let i = 0; i < dimensions.length; i++) {
 +                    const val = data.get(dimensions[i], dataIndex) as number;
 +                    if (!isNaN(val)) {
 +                        hasValue = true;
 +                    }
 +                    value.push(val);
 +                }
 +                if (hasValue) {
 +                    data.setItemLayout(dataIndex, coordSys.dataToPoint(value));
 +                }
 +                else {
 +                    // Also {Array.<number>}, not undefined to avoid if...else... statement
 +                    data.setItemLayout(dataIndex, [NaN, NaN]);
 +                }
 +            }
 +
-             simpleLayoutEdge(data.graph);
++            simpleLayoutEdge(data.graph, seriesModel);
 +        }
 +        else if (!layout || layout === 'none') {
 +            simpleLayout(seriesModel);
 +        }
 +    });
 +}
diff --cc src/chart/graph/simpleLayoutHelper.ts
index 723f153,0000000..73644ae
mode 100644,000000..100644
--- a/src/chart/graph/simpleLayoutHelper.ts
+++ b/src/chart/graph/simpleLayoutHelper.ts
@@@ -1,53 -1,0 +1,60 @@@
 +/*
 +* 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 * as vec2 from 'zrender/src/core/vector';
 +import GraphSeriesModel, { GraphNodeItemOption, GraphEdgeItemOption } from './GraphSeries';
 +import Graph from '../../data/Graph';
++import * as zrUtil from 'zrender/src/core/util';
++import {getCurvenessForEdge} from '../helper/multipleGraphEdgeHelper';
++
 +
 +export function simpleLayout(seriesModel: GraphSeriesModel) {
 +    const coordSys = seriesModel.coordinateSystem;
 +    if (coordSys && coordSys.type !== 'view') {
 +        return;
 +    }
 +    const graph = seriesModel.getGraph();
 +
 +    graph.eachNode(function (node) {
 +        const model = node.getModel<GraphNodeItemOption>();
 +        node.setLayout([+model.get('x'), +model.get('y')]);
 +    });
 +
-     simpleLayoutEdge(graph);
++    simpleLayoutEdge(graph, seriesModel);
 +}
 +
- export function simpleLayoutEdge(graph: Graph) {
-     graph.eachEdge(function (edge, idx) {
-         const curveness = edge.getModel<GraphEdgeItemOption>().get(['lineStyle', 'curveness']) || 0;
++export function simpleLayoutEdge(graph: Graph, seriesModel: GraphSeriesModel) {
++    graph.eachEdge(function (edge, index) {
++        const curveness = zrUtil.retrieve3(
++            edge.getModel<GraphEdgeItemOption>().get(['lineStyle', 'curveness']),
++            -getCurvenessForEdge(edge, seriesModel, index, true),
++            0
++        );
 +        const p1 = vec2.clone(edge.node1.getLayout());
 +        const p2 = vec2.clone(edge.node2.getLayout());
 +        const points = [p1, p2];
 +        if (+curveness) {
 +            points.push([
 +                (p1[0] + p2[0]) / 2 - (p1[1] - p2[1]) * curveness,
 +                (p1[1] + p2[1]) / 2 - (p2[0] - p1[0]) * curveness
 +            ]);
 +        }
 +        edge.setLayout(points);
 +    });
- }
++}
diff --cc src/chart/helper/Line.ts
index dbc09c0,0000000..138f8c8
mode 100644,000000..100644
--- a/src/chart/helper/Line.ts
+++ b/src/chart/helper/Line.ts
@@@ -1,463 -1,0 +1,490 @@@
 +/*
 +* 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 * as zrUtil from 'zrender/src/core/util';
 +import * as vector from 'zrender/src/core/vector';
 +import * as symbolUtil from '../../util/symbol';
 +import ECLinePath from './LinePath';
 +import * as graphic from '../../util/graphic';
 +import { enableHoverEmphasis, enterEmphasis, leaveEmphasis, SPECIAL_STATES } from '../../util/states';
- import {createTextStyle, getLabelStatesModels, setLabelStyle} from '../../label/labelStyle';
++import {getLabelStatesModels, setLabelStyle} from '../../label/labelStyle';
 +import {round} from '../../util/number';
 +import List from '../../data/List';
 +import { ZRTextAlign, ZRTextVerticalAlign, LineLabelOption, ColorString } from '../../util/types';
 +import SeriesModel from '../../model/Series';
 +import type { LineDrawSeriesScope, LineDrawModelOption } from './LineDraw';
 +
 +import { TextStyleProps } from 'zrender/src/graphic/Text';
 +import { LineDataVisual } from '../../visual/commonVisualTypes';
- import { setLabelLineStyle } from '../../label/labelGuideHelper';
 +import Model from '../../model/Model';
 +
 +const SYMBOL_CATEGORIES = ['fromSymbol', 'toSymbol'] as const;
 +
 +type ECSymbol = ReturnType<typeof createSymbol>;
 +
++type LineECSymbol = ECSymbol & {
++    __specifiedRotation: number
++}
++
 +type LineList = List<SeriesModel, LineDataVisual>;
 +
 +export interface LineLabel extends graphic.Text {
 +    lineLabelOriginalOpacity: number
 +}
 +
 +interface InnerLineLabel extends LineLabel {
 +    __align: TextStyleProps['align']
 +    __verticalAlign: TextStyleProps['verticalAlign']
 +    __position: LineLabelOption['position']
 +    __labelDistance: number[]
 +}
 +
 +function makeSymbolTypeKey(symbolCategory: 'fromSymbol' | 'toSymbol') {
 +    return '_' + symbolCategory + 'Type' as '_fromSymbolType' | '_toSymbolType';
 +}
 +
 +/**
 + * @inner
 + */
 +function createSymbol(name: 'fromSymbol' | 'toSymbol', lineData: LineList, idx: number) {
 +    const symbolType = lineData.getItemVisual(idx, name);
-     const symbolSize = lineData.getItemVisual(
-         idx,
-         name + 'Size' as 'fromSymbolSize' | 'toSymbolSize'
-     );
- 
 +    if (!symbolType || symbolType === 'none') {
 +        return;
 +    }
 +
++    const symbolSize = lineData.getItemVisual(idx, name + 'Size' as 'fromSymbolSize' | 'toSymbolSize');
++    const symbolRotate = lineData.getItemVisual(idx, name + 'Rotate' as 'fromSymbolRotate' | 'toSymbolRotate');
++
 +    const symbolSizeArr = zrUtil.isArray(symbolSize)
 +        ? symbolSize : [symbolSize, symbolSize];
 +    const symbolPath = symbolUtil.createSymbol(
 +        symbolType, -symbolSizeArr[0] / 2, -symbolSizeArr[1] / 2,
 +        symbolSizeArr[0], symbolSizeArr[1]
 +    );
 +
++    (symbolPath as LineECSymbol).__specifiedRotation = symbolRotate == null || isNaN(symbolRotate)
++        ? void 0
++        : +symbolRotate * Math.PI / 180 || 0;
++
 +    symbolPath.name = name;
 +
 +    return symbolPath;
 +}
 +
 +function createLine(points: number[][]) {
 +    const line = new ECLinePath({
 +        name: 'line',
 +        subPixelOptimize: true
 +    });
 +    setLinePoints(line.shape, points);
 +    return line;
 +}
 +
 +function setLinePoints(targetShape: ECLinePath['shape'], points: number[][]) {
 +    type CurveShape = ECLinePath['shape'] & {
 +        cpx1: number
 +        cpy1: number
 +    };
 +
 +    targetShape.x1 = points[0][0];
 +    targetShape.y1 = points[0][1];
 +    targetShape.x2 = points[1][0];
 +    targetShape.y2 = points[1][1];
 +    targetShape.percent = 1;
 +
 +    const cp1 = points[2];
 +    if (cp1) {
 +        (targetShape as CurveShape).cpx1 = cp1[0];
 +        (targetShape as CurveShape).cpy1 = cp1[1];
 +    }
 +    else {
 +        (targetShape as CurveShape).cpx1 = NaN;
 +        (targetShape as CurveShape).cpy1 = NaN;
 +    }
 +}
 +
 +class Line extends graphic.Group {
 +
 +    private _fromSymbolType: string;
 +    private _toSymbolType: string;
 +
 +    constructor(lineData: List, idx: number, seriesScope?: LineDrawSeriesScope) {
 +        super();
 +        this._createLine(lineData as LineList, idx, seriesScope);
 +    }
 +
 +    _createLine(lineData: LineList, idx: number, seriesScope?: LineDrawSeriesScope) {
 +        const seriesModel = lineData.hostModel;
 +        const linePoints = lineData.getItemLayout(idx);
 +        const line = createLine(linePoints);
 +        line.shape.percent = 0;
 +        graphic.initProps(line, {
 +            shape: {
 +                percent: 1
 +            }
 +        }, seriesModel, idx);
 +
 +        this.add(line);
 +
 +        zrUtil.each(SYMBOL_CATEGORIES, function (symbolCategory) {
 +            const symbol = createSymbol(symbolCategory, lineData, idx);
 +            // symbols must added after line to make sure
 +            // it will be updated after line#update.
 +            // Or symbol position and rotation update in line#beforeUpdate will be one frame slow
 +            this.add(symbol);
 +            this[makeSymbolTypeKey(symbolCategory)] = lineData.getItemVisual(idx, symbolCategory);
 +        }, this);
 +
 +        this._updateCommonStl(lineData, idx, seriesScope);
 +    }
 +
 +    // TODO More strict on the List type in parameters?
 +    updateData(lineData: List, idx: number, seriesScope: LineDrawSeriesScope) {
 +        const seriesModel = lineData.hostModel;
 +
 +        const line = this.childOfName('line') as ECLinePath;
 +        const linePoints = lineData.getItemLayout(idx);
 +        const target = {
 +            shape: {} as ECLinePath['shape']
 +        };
 +
 +        setLinePoints(target.shape, linePoints);
 +        graphic.updateProps(line, target, seriesModel, idx);
 +
 +        zrUtil.each(SYMBOL_CATEGORIES, function (symbolCategory) {
 +            const symbolType = (lineData as LineList).getItemVisual(idx, symbolCategory);
 +            const key = makeSymbolTypeKey(symbolCategory);
 +            // Symbol changed
 +            if (this[key] !== symbolType) {
 +                this.remove(this.childOfName(symbolCategory));
 +                const symbol = createSymbol(symbolCategory, lineData as LineList, idx);
 +                this.add(symbol);
 +            }
 +            this[key] = symbolType;
 +        }, this);
 +
 +        this._updateCommonStl(lineData, idx, seriesScope);
 +    };
 +
 +    getLinePath() {
 +        return this.childAt(0) as graphic.Line;
 +    }
 +
 +    _updateCommonStl(lineData: List, idx: number, seriesScope?: LineDrawSeriesScope) {
 +        const seriesModel = lineData.hostModel as SeriesModel;
 +
 +        const line = this.childOfName('line') as ECLinePath;
 +
 +        let emphasisLineStyle = seriesScope && seriesScope.emphasisLineStyle;
 +        let blurLineStyle = seriesScope && seriesScope.blurLineStyle;
 +        let selectLineStyle = seriesScope && seriesScope.selectLineStyle;
 +
 +        let labelStatesModels = seriesScope && seriesScope.labelStatesModels;
 +
 +        // Optimization for large dataset
 +        if (!seriesScope || lineData.hasItemOption) {
 +            const itemModel = lineData.getItemModel<LineDrawModelOption>(idx);
 +
 +            emphasisLineStyle = itemModel.getModel(['emphasis', 'lineStyle']).getLineStyle();
 +            blurLineStyle = itemModel.getModel(['blur', 'lineStyle']).getLineStyle();
 +            selectLineStyle = itemModel.getModel(['select', 'lineStyle']).getLineStyle();
 +
 +            labelStatesModels = getLabelStatesModels(itemModel);
 +        }
 +
 +        const lineStyle = lineData.getItemVisual(idx, 'style');
 +        const visualColor = lineStyle.stroke;
 +
 +        line.useStyle(lineStyle);
 +        line.style.fill = null;
 +        line.style.strokeNoScale = true;
 +
 +        line.ensureState('emphasis').style = emphasisLineStyle;
 +        line.ensureState('blur').style = blurLineStyle;
 +        line.ensureState('select').style = selectLineStyle;
 +
 +        // Update symbol
 +        zrUtil.each(SYMBOL_CATEGORIES, function (symbolCategory) {
 +            const symbol = this.childOfName(symbolCategory) as ECSymbol;
 +            if (symbol) {
 +                // Share opacity and color with line.
 +                symbol.setColor(visualColor);
 +                symbol.style.opacity = lineStyle.opacity;
 +
 +                for (let i = 0; i < SPECIAL_STATES.length; i++) {
 +                    const stateName = SPECIAL_STATES[i];
 +                    const lineState = line.getState(stateName);
 +                    if (lineState) {
 +                        const lineStateStyle = lineState.style || {};
 +                        const state = symbol.ensureState(stateName);
 +                        const stateStyle = state.style || (state.style = {});
 +                        if (lineStateStyle.stroke != null) {
 +                            stateStyle[symbol.__isEmptyBrush ? 'stroke' : 'fill'] = lineStateStyle.stroke;
 +                        }
 +                        if (lineStateStyle.opacity != null) {
 +                            stateStyle.opacity = lineStateStyle.opacity;
 +                        }
 +                    }
 +                }
 +
 +                symbol.markRedraw();
 +            }
 +        }, this);
 +
 +        const rawVal = seriesModel.getRawValue(idx) as number;
 +        setLabelStyle(this, labelStatesModels, {
 +            labelDataIndex: idx,
 +            labelFetcher: {
 +                getFormattedLabel(dataIndex, stateName) {
 +                    return seriesModel.getFormattedLabel(dataIndex, stateName, lineData.dataType);
 +                }
 +            },
 +            inheritColor: visualColor as ColorString || '#000',
 +            defaultText: (rawVal == null
 +                ? lineData.getName(idx)
 +                : isFinite(rawVal)
 +                ? round(rawVal)
 +                : rawVal) + ''
 +        });
 +        const label = this.getTextContent() as InnerLineLabel;
 +
 +        // Always set `textStyle` even if `normalStyle.text` is null, because default
 +        // values have to be set on `normalStyle`.
 +        if (label) {
 +            const labelNormalModel = labelStatesModels.normal as unknown as Model<LineLabelOption>;
 +            label.__align = label.style.align;
 +            label.__verticalAlign = label.style.verticalAlign;
 +            // 'start', 'middle', 'end'
 +            label.__position = labelNormalModel.get('position') || 'middle';
 +
 +            let distance = labelNormalModel.get('distance');
 +            if (!zrUtil.isArray(distance)) {
 +                distance = [distance, distance];
 +            }
 +            label.__labelDistance = distance;
 +        }
 +
 +        this.setTextConfig({
 +            position: null,
 +            local: true,
 +            inside: false   // Can't be inside for stroke element.
 +        });
 +
 +        enableHoverEmphasis(this);
 +    }
 +
 +    highlight() {
 +        enterEmphasis(this);
 +    }
 +
 +    downplay() {
 +        leaveEmphasis(this);
 +    }
 +
 +    updateLayout(lineData: List, idx: number) {
 +        this.setLinePoints(lineData.getItemLayout(idx));
 +    }
 +
 +    setLinePoints(points: number[][]) {
 +        const linePath = this.childOfName('line') as ECLinePath;
 +        setLinePoints(linePath.shape, points);
 +        linePath.dirty();
 +    }
 +
 +    beforeUpdate() {
 +        const lineGroup = this;
 +        const symbolFrom = lineGroup.childOfName('fromSymbol') as ECSymbol;
 +        const symbolTo = lineGroup.childOfName('toSymbol') as ECSymbol;
 +        const label = lineGroup.getTextContent() as InnerLineLabel;
 +        // Quick reject
 +        if (!symbolFrom && !symbolTo && (!label || label.ignore)) {
 +            return;
 +        }
 +
 +        let invScale = 1;
 +        let parentNode = this.parent;
 +        while (parentNode) {
 +            if (parentNode.scaleX) {
 +                invScale /= parentNode.scaleX;
 +            }
 +            parentNode = parentNode.parent;
 +        }
 +
 +        const line = lineGroup.childOfName('line') as ECLinePath;
 +        // If line not changed
 +        // FIXME Parent scale changed
 +        if (!this.__dirty && !line.__dirty) {
 +            return;
 +        }
 +
 +        const percent = line.shape.percent;
 +        const fromPos = line.pointAt(0);
 +        const toPos = line.pointAt(percent);
 +
 +        const d = vector.sub([], toPos, fromPos);
 +        vector.normalize(d, d);
 +
++        function setSymbolRotation(symbol: ECSymbol, percent: 0 | 1) {
++            // Fix #12388
++            // when symbol is set to be 'arrow' in markLine,
++            // symbolRotate value will be ignored, and compulsively use tangent angle.
++            // rotate by default if symbol rotation is not specified
++            const specifiedRotation = (symbol as LineECSymbol).__specifiedRotation;
++            if (specifiedRotation == null) {
++                const tangent = line.tangentAt(percent);
++                symbol.attr('rotation', (percent === 1 ? -1 : 1) * Math.PI / 2 - Math.atan2(
++                    tangent[1], tangent[0]
++                ));
++            }
++            else {
++                symbol.attr('rotation', specifiedRotation);
++            }
++        }
++
 +        if (symbolFrom) {
 +            symbolFrom.setPosition(fromPos);
++
++            setSymbolRotation(symbolFrom, 0);
++
 +            const tangent = line.tangentAt(0);
 +            symbolFrom.rotation = Math.PI / 2 - Math.atan2(
 +                tangent[1], tangent[0]
 +            );
 +            symbolFrom.scaleX = symbolFrom.scaleY = invScale * percent;
 +            symbolFrom.markRedraw();
 +        }
 +        if (symbolTo) {
 +            symbolTo.setPosition(toPos);
 +            const tangent = line.tangentAt(1);
++            setSymbolRotation(symbolFrom, 1);
++
 +            symbolTo.rotation = -Math.PI / 2 - Math.atan2(
 +                tangent[1], tangent[0]
 +            );
 +            symbolTo.scaleX = symbolTo.scaleY = invScale * percent;
 +            symbolTo.markRedraw();
 +        }
 +
 +        if (label && !label.ignore) {
 +            label.x = label.y = 0;
 +            label.originX = label.originY = 0;
 +
 +            let textAlign: ZRTextAlign;
 +            let textVerticalAlign: ZRTextVerticalAlign;
 +
 +            const distance = label.__labelDistance;
 +            const distanceX = distance[0] * invScale;
 +            const distanceY = distance[1] * invScale;
 +            const halfPercent = percent / 2;
 +            const tangent = line.tangentAt(halfPercent);
 +            const n = [tangent[1], -tangent[0]];
 +            const cp = line.pointAt(halfPercent);
 +            if (n[1] > 0) {
 +                n[0] = -n[0];
 +                n[1] = -n[1];
 +            }
 +            const dir = tangent[0] < 0 ? -1 : 1;
 +
 +            if (label.__position !== 'start' && label.__position !== 'end') {
 +                let rotation = -Math.atan2(tangent[1], tangent[0]);
 +                if (toPos[0] < fromPos[0]) {
 +                    rotation = Math.PI + rotation;
 +                }
 +                label.rotation = rotation;
 +            }
 +
 +            let dy;
 +            switch (label.__position) {
 +                case 'insideStartTop':
 +                case 'insideMiddleTop':
 +                case 'insideEndTop':
 +                case 'middle':
 +                    dy = -distanceY;
 +                    textVerticalAlign = 'bottom';
 +                    break;
 +
 +                case 'insideStartBottom':
 +                case 'insideMiddleBottom':
 +                case 'insideEndBottom':
 +                    dy = distanceY;
 +                    textVerticalAlign = 'top';
 +                    break;
 +
 +                default:
 +                    dy = 0;
 +                    textVerticalAlign = 'middle';
 +            }
 +
 +            switch (label.__position) {
 +                case 'end':
 +                    label.x = d[0] * distanceX + toPos[0];
 +                    label.y = d[1] * distanceY + toPos[1];
 +                    textAlign = d[0] > 0.8 ? 'left' : (d[0] < -0.8 ? 'right' : 'center');
 +                    textVerticalAlign = d[1] > 0.8 ? 'top' : (d[1] < -0.8 ? 'bottom' : 'middle');
 +                    break;
 +
 +                case 'start':
 +                    label.x = -d[0] * distanceX + fromPos[0];
 +                    label.y = -d[1] * distanceY + fromPos[1];
 +                    textAlign = d[0] > 0.8 ? 'right' : (d[0] < -0.8 ? 'left' : 'center');
 +                    textVerticalAlign = d[1] > 0.8 ? 'bottom' : (d[1] < -0.8 ? 'top' : 'middle');
 +                    break;
 +
 +                case 'insideStartTop':
 +                case 'insideStart':
 +                case 'insideStartBottom':
 +                    label.x = distanceX * dir + fromPos[0];
 +                    label.y = fromPos[1] + dy;
 +                    textAlign = tangent[0] < 0 ? 'right' : 'left';
 +                    label.originX = -distanceX * dir;
 +                    label.originY = -dy;
 +                    break;
 +
 +                case 'insideMiddleTop':
 +                case 'insideMiddle':
 +                case 'insideMiddleBottom':
 +                case 'middle':
 +                    label.x = cp[0];
 +                    label.y = cp[1] + dy;
 +                    textAlign = 'center';
 +                    label.originY = -dy;
 +                    break;
 +
 +                case 'insideEndTop':
 +                case 'insideEnd':
 +                case 'insideEndBottom':
 +                    label.x = -distanceX * dir + toPos[0];
 +                    label.y = toPos[1] + dy;
 +                    textAlign = tangent[0] >= 0 ? 'right' : 'left';
 +                    label.originX = distanceX * dir;
 +                    label.originY = -dy;
 +                    break;
 +            }
 +
 +            label.scaleX = label.scaleY = invScale;
 +            label.setStyle({
 +                // Use the user specified text align and baseline first
 +                verticalAlign: label.__verticalAlign || textVerticalAlign,
 +                align: label.__align || textAlign
 +            });
 +        }
 +    }
 +}
 +
 +export default Line;
diff --cc src/chart/helper/multipleGraphEdgeHelper.ts
index 0000000,0000000..964c5ae
new file mode 100644
--- /dev/null
+++ b/src/chart/helper/multipleGraphEdgeHelper.ts
@@@ -1,0 -1,0 +1,230 @@@
++/*
++* 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.
++*/
++
++// @ts-nocheck
++import * as zrUtil from 'zrender/src/core/util';
++
++const KEY_DELIMITER = '-->';
++/**
++ * params handler
++ * @param {module:echarts/model/SeriesModel} seriesModel
++ * @returns {*}
++ */
++const getAutoCurvenessParams = function (seriesModel) {
++    return seriesModel.get('autoCurveness') || null;
++};
++
++/**
++ * Generate a list of edge curvatures, 20 is the default
++ * @param {module:echarts/model/SeriesModel} seriesModel
++ * @param {number} appendLength
++ * @return  20 => [0, -0.2, 0.2, -0.4, 0.4, -0.6, 0.6, -0.8, 0.8, -1, 1, -1.2, 1.2, -1.4, 1.4, -1.6, 1.6, -1.8, 1.8, -2]
++ */
++const createCurveness = function (seriesModel, appendLength) {
++    const autoCurvenessParmas = getAutoCurvenessParams(seriesModel);
++    let length = 20;
++    let curvenessList = [];
++
++    // handler the function set
++    if (typeof autoCurvenessParmas === 'number') {
++        length = autoCurvenessParmas;
++    }
++    else if (zrUtil.isArray(autoCurvenessParmas)) {
++        seriesModel.__curvenessList = autoCurvenessParmas;
++        return;
++    }
++
++    // append length
++    if (appendLength > length) {
++        length = appendLength;
++    }
++
++    // make sure the length is even
++    const len = length % 2 ? length + 2 : length + 3;
++    curvenessList = [];
++
++    for (let i = 0; i < len; i++) {
++        curvenessList.push((i % 2 ? i + 1 : i) / 10 * (i % 2 ? -1 : 1));
++    }
++    seriesModel.__curvenessList = curvenessList;
++};
++
++/**
++ * Create different cache key data in the positive and negative directions, in order to set the curvature later
++ * @param {number|string|module:echarts/data/Graph.Node} n1
++ * @param {number|string|module:echarts/data/Graph.Node} n2
++ * @param {module:echarts/model/SeriesModel} seriesModel
++ * @returns {string} key
++ */
++const getKeyOfEdges = function (n1, n2, seriesModel) {
++    const source = [n1.id, n1.dataIndex].join('.');
++    const target = [n2.id, n2.dataIndex].join('.');
++    return [seriesModel.uid, source, target].join(KEY_DELIMITER);
++};
++
++/**
++ * get opposite key
++ * @param {string} key
++ * @returns {string}
++ */
++const getOppositeKey = function (key) {
++    const keys = key.split(KEY_DELIMITER);
++    return [keys[0], keys[2], keys[1]].join(KEY_DELIMITER);
++};
++
++/**
++ * get edgeMap with key
++ * @param edge
++ * @param {module:echarts/model/SeriesModel} seriesModel
++ */
++const getEdgeFromMap = function (edge, seriesModel) {
++    const key = getKeyOfEdges(edge.node1, edge.node2, seriesModel);
++    return seriesModel.__edgeMap[key];
++};
++
++/**
++ * calculate all cases total length
++ * @param edge
++ * @param seriesModel
++ * @returns {number}
++ */
++const getTotalLengthBetweenNodes = function (edge, seriesModel) {
++    const len = getEdgeMapLengthWithKey(getKeyOfEdges(edge.node1, edge.node2, seriesModel), seriesModel);
++    const lenV = getEdgeMapLengthWithKey(getKeyOfEdges(edge.node2, edge.node1, seriesModel), seriesModel);
++
++    return len + lenV;
++};
++
++/**
++ *
++ * @param key
++ */
++const getEdgeMapLengthWithKey = function (key, seriesModel) {
++    const edgeMap = seriesModel.__edgeMap;
++    return edgeMap[key] ? edgeMap[key].length : 0;
++};
++
++/**
++ * Count the number of edges between the same two points, used to obtain the curvature table and the parity of the edge
++ * @see /graph/GraphSeries.js@getInitialData
++ * @param {module:echarts/model/SeriesModel} seriesModel
++ */
++export function initCurvenessList(seriesModel) {
++    if (!getAutoCurvenessParams(seriesModel)) {
++        return;
++    }
++
++    seriesModel.__curvenessList = [];
++    seriesModel.__edgeMap = {};
++    // calc the array of curveness List
++    createCurveness(seriesModel);
++}
++
++/**
++ * set edgeMap with key
++ * @param {number|string|module:echarts/data/Graph.Node} n1
++ * @param {number|string|module:echarts/data/Graph.Node} n2
++ * @param {module:echarts/model/SeriesModel} seriesModel
++ * @param {number} index
++ */
++export function createEdgeMapForCurveness(n1, n2, seriesModel, index) {
++    if (!getAutoCurvenessParams(seriesModel)) {
++        return;
++    }
++
++    const key = getKeyOfEdges(n1, n2, seriesModel);
++    const edgeMap = seriesModel.__edgeMap;
++    const oppositeEdges = edgeMap[getOppositeKey(key)];
++    // set direction
++    if (edgeMap[key] && !oppositeEdges) {
++        edgeMap[key].isForward = true;
++    }
++    else if (oppositeEdges && edgeMap[key]) {
++        oppositeEdges.isForward = true;
++        edgeMap[key].isForward = false;
++    }
++
++    edgeMap[key] = edgeMap[key] || [];
++    edgeMap[key].push(index);
++}
++
++/**
++ * get curvature for edge
++ * @param edge
++ * @param {module:echarts/model/SeriesModel} seriesModel
++ * @param index
++ */
++export function getCurvenessForEdge(edge, seriesModel, index, needReverse?: boolean) {
++    const autoCurvenessParams = getAutoCurvenessParams(seriesModel);
++    const isArrayParam = zrUtil.isArray(autoCurvenessParams);
++    if (!autoCurvenessParams) {
++        return null;
++    }
++
++    const edgeArray = getEdgeFromMap(edge, seriesModel);
++    if (!edgeArray) {
++        return null;
++    }
++
++    let edgeIndex = -1;
++    for (const i = 0; i < edgeArray.length; i++) {
++        if (edgeArray[i] === index) {
++            edgeIndex = i;
++            break;
++        }
++    }
++    // if totalLen is Longer createCurveness
++    const totalLen = getTotalLengthBetweenNodes(edge, seriesModel);
++    createCurveness(seriesModel, totalLen);
++
++    edge.lineStyle = edge.lineStyle || {};
++    // if is opposite edge, must set curvenss to opposite number
++    const curKey = getKeyOfEdges(edge.node1, edge.node2, seriesModel);
++    const curvenessList = seriesModel.__curvenessList;
++    // if pass array no need parity
++    const parityCorrection = isArrayParam ? 0 : totalLen % 2 ? 0 : 1;
++
++    if (!edgeArray.isForward) {
++        // the opposite edge show outside
++        const oppositeKey = getOppositeKey(curKey);
++        const len = getEdgeMapLengthWithKey(oppositeKey, seriesModel);
++        const resValue = curvenessList[edgeIndex + len + parityCorrection];
++        // isNeedReverse, simple, force type need reverse the curveness in the junction of the forword and the opposite
++        if (needReverse) {
++            // set as array may make the parity handle with the len of opposite
++            if (isArrayParam) {
++                if (autoCurvenessParams && autoCurvenessParams[0] === 0) {
++                    return (len + parityCorrection) % 2 ? resValue : -resValue;
++                }
++                else {
++                    return ((len % 2 ? 0 : 1) + parityCorrection) % 2 ? resValue : -resValue;
++                }
++            }
++            else {
++                return (len + parityCorrection) % 2 ? resValue : -resValue;
++            }
++        }
++        else {
++            return curvenessList[edgeIndex + len + parityCorrection];
++        }
++    }
++    else {
++        return curvenessList[parityCorrection + edgeIndex];
++    }
++}
diff --cc src/chart/lines/LinesSeries.ts
index a75d7e4,0000000..b2da8ab
mode 100644,000000..100644
--- a/src/chart/lines/LinesSeries.ts
+++ b/src/chart/lines/LinesSeries.ts
@@@ -1,413 -1,0 +1,410 @@@
 +/*
 +* 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.
 +*/
 +
 +/* global Uint32Array, Float64Array, Float32Array */
 +
 +import SeriesModel from '../../model/Series';
 +import List from '../../data/List';
 +import { concatArray, mergeAll, map } from 'zrender/src/core/util';
 +import CoordinateSystem from '../../CoordinateSystem';
 +import {
 +    SeriesOption,
 +    SeriesOnCartesianOptionMixin,
 +    SeriesOnGeoOptionMixin,
 +    SeriesOnPolarOptionMixin,
 +    SeriesOnCalendarOptionMixin,
 +    SeriesLargeOptionMixin,
 +    LineStyleOption,
 +    OptionDataValue,
 +    LineLabelOption,
 +    StatesOptionMixin
 +} from '../../util/types';
 +import GlobalModel from '../../model/Global';
 +import type { LineDrawModelOption } from '../helper/LineDraw';
 +import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup';
 +
 +const Uint32Arr = typeof Uint32Array === 'undefined' ? Array : Uint32Array;
 +const Float64Arr = typeof Float64Array === 'undefined' ? Array : Float64Array;
 +
 +function compatEc2(seriesOpt: LinesSeriesOption) {
 +    const data = seriesOpt.data;
 +    if (data && data[0] && (data as LegacyDataItemOption[][])[0][0] && (data as LegacyDataItemOption[][])[0][0].coord) {
 +        if (__DEV__) {
 +            console.warn('Lines data configuration has been changed to'
 +                + ' { coords:[[1,2],[2,3]] }');
 +        }
 +        seriesOpt.data = map(data as LegacyDataItemOption[][], function (itemOpt) {
 +            const coords = [
 +                itemOpt[0].coord, itemOpt[1].coord
 +            ];
 +            const target: LinesDataItemOption = {
 +                coords: coords
 +            };
 +            if (itemOpt[0].name) {
 +                target.fromName = itemOpt[0].name;
 +            }
 +            if (itemOpt[1].name) {
 +                target.toName = itemOpt[1].name;
 +            }
 +            return mergeAll([target, itemOpt[0], itemOpt[1]]);
 +        });
 +    }
 +}
 +
 +type LinesCoords = number[][];
 +
 +type LinesValue = OptionDataValue | OptionDataValue[];
 +
 +interface LinesLineStyleOption extends LineStyleOption {
 +    curveness?: number
 +}
 +
 +// @deprecated
 +interface LegacyDataItemOption {
 +    coord: number[]
 +    name: string
 +}
 +
 +export interface LinesStateOption {
 +    lineStyle?: LinesLineStyleOption
 +    label?: LineLabelOption
 +}
 +
 +export interface LinesDataItemOption extends LinesStateOption, StatesOptionMixin<LinesStateOption> {
 +    name?: string
 +
 +    fromName?: string
 +    toName?: string
 +
 +    symbol?: string[] | string
 +    symbolSize?: number[] | number
 +
 +    coords?: LinesCoords
 +
 +    value?: LinesValue
 +}
 +
 +export interface LinesSeriesOption extends SeriesOption<LinesStateOption>, LinesStateOption,
 +    SeriesOnCartesianOptionMixin, SeriesOnGeoOptionMixin, SeriesOnPolarOptionMixin,
 +    SeriesOnCalendarOptionMixin, SeriesLargeOptionMixin {
 +
 +    type?: 'lines'
 +
 +    coordinateSystem?: string
 +
 +    symbol?: string[] | string
 +    symbolSize?: number[] | number
 +
 +    effect?: LineDrawModelOption['effect']
 +
 +    /**
 +     * If lines are polyline
 +     * polyline not support curveness, label, animation
 +     */
 +    polyline?: boolean
 +    /**
 +     * If clip the overflow.
 +     * Available when coordinateSystem is cartesian or polar.
 +     */
 +    clip?: boolean
 +
 +    data?: LinesDataItemOption[]
 +        // Stored as a flat array. In format
 +        // Points Count(2) | x | y | x | y | Points Count(3) | x |  y | x | y | x | y |
 +        | ArrayLike<number>
 +}
 +
 +class LinesSeriesModel extends SeriesModel<LinesSeriesOption> {
 +
 +    static readonly type = 'series.lines';
 +    readonly type = LinesSeriesModel.type;
 +
 +    static readonly dependencies = ['grid', 'polar', 'geo', 'calendar'];
 +
 +    visualStyleAccessPath = 'lineStyle';
 +    visualDrawType = 'stroke' as const;
 +
 +    private _flatCoords: ArrayLike<number>;
 +    private _flatCoordsOffset: ArrayLike<number>;
 +
 +    init(option: LinesSeriesOption) {
 +        // The input data may be null/undefined.
 +        option.data = option.data || [];
 +
 +        // Not using preprocessor because mergeOption may not have series.type
 +        compatEc2(option);
 +
 +        const result = this._processFlatCoordsArray(option.data);
 +        this._flatCoords = result.flatCoords;
 +        this._flatCoordsOffset = result.flatCoordsOffset;
 +        if (result.flatCoords) {
 +            option.data = new Float32Array(result.count);
 +        }
 +
 +        super.init.apply(this, arguments as any);
 +    }
 +
 +    mergeOption(option: LinesSeriesOption) {
-         // The input data may be null/undefined.
-         option.data = option.data || [];
- 
 +        compatEc2(option);
 +
 +        if (option.data) {
 +            // Only update when have option data to merge.
 +            const result = this._processFlatCoordsArray(option.data);
 +            this._flatCoords = result.flatCoords;
 +            this._flatCoordsOffset = result.flatCoordsOffset;
 +            if (result.flatCoords) {
 +                option.data = new Float32Array(result.count);
 +            }
 +        }
 +
 +        super.mergeOption.apply(this, arguments as any);
 +    }
 +
 +    appendData(params: Pick<LinesSeriesOption, 'data'>) {
 +        const result = this._processFlatCoordsArray(params.data);
 +        if (result.flatCoords) {
 +            if (!this._flatCoords) {
 +                this._flatCoords = result.flatCoords;
 +                this._flatCoordsOffset = result.flatCoordsOffset;
 +            }
 +            else {
 +                this._flatCoords = concatArray(this._flatCoords, result.flatCoords);
 +                this._flatCoordsOffset = concatArray(this._flatCoordsOffset, result.flatCoordsOffset);
 +            }
 +            params.data = new Float32Array(result.count);
 +        }
 +
 +        this.getRawData().appendData(params.data);
 +    }
 +
 +    _getCoordsFromItemModel(idx: number) {
 +        const itemModel = this.getData().getItemModel<LinesDataItemOption>(idx);
 +        const coords = (itemModel.option instanceof Array)
 +            ? itemModel.option : itemModel.getShallow('coords');
 +
 +        if (__DEV__) {
 +            if (!(coords instanceof Array && coords.length > 0 && coords[0] instanceof Array)) {
 +                throw new Error(
 +                    'Invalid coords ' + JSON.stringify(coords) + '. Lines must have 2d coords array in data item.'
 +                );
 +            }
 +        }
 +        return coords;
 +    }
 +
 +    getLineCoordsCount(idx: number) {
 +        if (this._flatCoordsOffset) {
 +            return this._flatCoordsOffset[idx * 2 + 1];
 +        }
 +        else {
 +            return this._getCoordsFromItemModel(idx).length;
 +        }
 +    }
 +
 +    getLineCoords(idx: number, out: number[][]) {
 +        if (this._flatCoordsOffset) {
 +            const offset = this._flatCoordsOffset[idx * 2];
 +            const len = this._flatCoordsOffset[idx * 2 + 1];
 +            for (let i = 0; i < len; i++) {
 +                out[i] = out[i] || [];
 +                out[i][0] = this._flatCoords[offset + i * 2];
 +                out[i][1] = this._flatCoords[offset + i * 2 + 1];
 +            }
 +            return len;
 +        }
 +        else {
 +            const coords = this._getCoordsFromItemModel(idx);
 +            for (let i = 0; i < coords.length; i++) {
 +                out[i] = out[i] || [];
 +                out[i][0] = coords[i][0];
 +                out[i][1] = coords[i][1];
 +            }
 +            return coords.length;
 +        }
 +    }
 +
 +    _processFlatCoordsArray(data: LinesSeriesOption['data']) {
 +        let startOffset = 0;
 +        if (this._flatCoords) {
 +            startOffset = this._flatCoords.length;
 +        }
 +        // Stored as a typed array. In format
 +        // Points Count(2) | x | y | x | y | Points Count(3) | x |  y | x | y | x | y |
 +        if (typeof data[0] === 'number') {
 +            const len = data.length;
 +            // Store offset and len of each segment
 +            const coordsOffsetAndLenStorage = new Uint32Arr(len) as Uint32Array;
 +            const coordsStorage = new Float64Arr(len) as Float64Array;
 +            let coordsCursor = 0;
 +            let offsetCursor = 0;
 +            let dataCount = 0;
 +            for (let i = 0; i < len;) {
 +                dataCount++;
 +                const count = data[i++] as number;
 +                // Offset
 +                coordsOffsetAndLenStorage[offsetCursor++] = coordsCursor + startOffset;
 +                // Len
 +                coordsOffsetAndLenStorage[offsetCursor++] = count;
 +                for (let k = 0; k < count; k++) {
 +                    const x = data[i++] as number;
 +                    const y = data[i++] as number;
 +                    coordsStorage[coordsCursor++] = x;
 +                    coordsStorage[coordsCursor++] = y;
 +
 +                    if (i > len) {
 +                        if (__DEV__) {
 +                            throw new Error('Invalid data format.');
 +                        }
 +                    }
 +                }
 +            }
 +
 +            return {
 +                flatCoordsOffset: new Uint32Array(coordsOffsetAndLenStorage.buffer, 0, offsetCursor),
 +                flatCoords: coordsStorage,
 +                count: dataCount
 +            };
 +        }
 +
 +        return {
 +            flatCoordsOffset: null,
 +            flatCoords: null,
 +            count: data.length
 +        };
 +    }
 +
 +    getInitialData(option: LinesSeriesOption, ecModel: GlobalModel) {
 +        if (__DEV__) {
 +            const CoordSys = CoordinateSystem.get(option.coordinateSystem);
 +            if (!CoordSys) {
 +                throw new Error('Unkown coordinate system ' + option.coordinateSystem);
 +            }
 +        }
 +
 +        const lineData = new List(['value'], this);
 +        lineData.hasItemOption = false;
 +
 +        lineData.initData(option.data, [], function (dataItem, dimName, dataIndex, dimIndex) {
 +            // dataItem is simply coords
 +            if (dataItem instanceof Array) {
 +                return NaN;
 +            }
 +            else {
 +                lineData.hasItemOption = true;
 +                const value = dataItem.value;
 +                if (value != null) {
 +                    return value instanceof Array ? value[dimIndex] : value;
 +                }
 +            }
 +        });
 +
 +        return lineData;
 +    }
 +
 +    formatTooltip(
 +        dataIndex: number,
 +        multipleSeries: boolean,
 +        dataType: string
 +    ) {
 +        const data = this.getData();
 +        const itemModel = data.getItemModel<LinesDataItemOption>(dataIndex);
 +        const name = itemModel.get('name');
 +        if (name) {
 +            return name;
 +        }
 +        const fromName = itemModel.get('fromName');
 +        const toName = itemModel.get('toName');
 +        const nameArr = [];
 +        fromName != null && nameArr.push(fromName);
 +        toName != null && nameArr.push(toName);
 +
 +        return createTooltipMarkup('nameValue', {
 +            name: nameArr.join(' > ')
 +        });
 +    }
 +
 +    preventIncremental() {
 +        return !!this.get(['effect', 'show']);
 +    }
 +
 +    getProgressive() {
 +        const progressive = this.option.progressive;
 +        if (progressive == null) {
 +            return this.option.large ? 1e4 : this.get('progressive');
 +        }
 +        return progressive;
 +    }
 +
 +    getProgressiveThreshold() {
 +        const progressiveThreshold = this.option.progressiveThreshold;
 +        if (progressiveThreshold == null) {
 +            return this.option.large ? 2e4 : this.get('progressiveThreshold');
 +        }
 +        return progressiveThreshold;
 +    }
 +
 +    static defaultOption: LinesSeriesOption = {
 +        coordinateSystem: 'geo',
 +        zlevel: 0,
 +        z: 2,
 +        legendHoverLink: true,
 +
 +        // Cartesian coordinate system
 +        xAxisIndex: 0,
 +        yAxisIndex: 0,
 +
 +        symbol: ['none', 'none'],
 +        symbolSize: [10, 10],
 +        // Geo coordinate system
 +        geoIndex: 0,
 +
 +        effect: {
 +            show: false,
 +            period: 4,
 +            constantSpeed: 0,
 +            symbol: 'circle',
 +            symbolSize: 3,
 +            loop: true,
 +            trailLength: 0.2
 +        },
 +
 +        large: false,
 +        // Available when large is true
 +        largeThreshold: 2000,
 +
 +        polyline: false,
 +
 +        clip: true,
 +
 +        label: {
 +            show: false,
 +            position: 'end'
 +            // distance: 5,
 +            // formatter: 标签文本格式器,同Tooltip.formatter,不支持异步回调
 +        },
 +
 +        lineStyle: {
 +            opacity: 0.5
 +        }
 +    };
 +}
 +
 +SeriesModel.registerClass(LinesSeriesModel);
 +
- export default LinesSeriesModel;
++export default LinesSeriesModel;
diff --cc src/chart/radar/RadarView.ts
index dd266ec,0000000..7842224
mode 100644,000000..100644
--- a/src/chart/radar/RadarView.ts
+++ b/src/chart/radar/RadarView.ts
@@@ -1,272 -1,0 +1,274 @@@
 +/*
 +* 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 * as graphic from '../../util/graphic';
 +import { setStatesStylesFromModel, enableHoverEmphasis } from '../../util/states';
 +import * as zrUtil from 'zrender/src/core/util';
 +import * as symbolUtil from '../../util/symbol';
 +import ChartView from '../../view/Chart';
 +import RadarSeriesModel, { RadarSeriesDataItemOption } from './RadarSeries';
 +import ExtensionAPI from '../../ExtensionAPI';
 +import List from '../../data/List';
 +import { ColorString } from '../../util/types';
 +import GlobalModel from '../../model/Global';
 +import { VectorArray } from 'zrender/src/core/vector';
 +import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle';
 +import ZRImage from 'zrender/src/graphic/Image';
 +
 +function normalizeSymbolSize(symbolSize: number | number[]) {
 +    if (!zrUtil.isArray(symbolSize)) {
 +        symbolSize = [+symbolSize, +symbolSize];
 +    }
 +    return symbolSize;
 +}
 +
 +type RadarSymbol = ReturnType<typeof symbolUtil.createSymbol> & {
 +    __dimIdx: number
 +};
 +
 +class RadarView extends ChartView {
 +    static type = 'radar';
 +    type = RadarView.type;
 +
 +    private _data: List<RadarSeriesModel>;
 +
 +    render(seriesModel: RadarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) {
 +        const polar = seriesModel.coordinateSystem;
 +        const group = this.group;
 +
 +        const data = seriesModel.getData();
 +        const oldData = this._data;
 +
 +        function createSymbol(data: List<RadarSeriesModel>, idx: number) {
 +            const symbolType = data.getItemVisual(idx, 'symbol') as string || 'circle';
 +            if (symbolType === 'none') {
 +                return;
 +            }
 +            const symbolSize = normalizeSymbolSize(
 +                data.getItemVisual(idx, 'symbolSize')
 +            );
 +            const symbolPath = symbolUtil.createSymbol(
 +                symbolType, -1, -1, 2, 2
 +            );
++            const symbolRotate = data.getItemVisual(idx, 'symbolRotate') || 0;
 +            symbolPath.attr({
 +                style: {
 +                    strokeNoScale: true
 +                },
 +                z2: 100,
 +                scaleX: symbolSize[0] / 2,
-                 scaleY: symbolSize[1] / 2
++                scaleY: symbolSize[1] / 2,
++                rotation: symbolRotate * Math.PI / 180 || 0
 +            });
 +            return symbolPath as RadarSymbol;
 +        }
 +
 +        function updateSymbols(
 +            oldPoints: VectorArray[],
 +            newPoints: VectorArray[],
 +            symbolGroup: graphic.Group,
 +            data: List<RadarSeriesModel>,
 +            idx: number,
 +            isInit?: boolean
 +        ) {
 +            // Simply rerender all
 +            symbolGroup.removeAll();
 +            for (let i = 0; i < newPoints.length - 1; i++) {
 +                const symbolPath = createSymbol(data, idx);
 +                if (symbolPath) {
 +                    symbolPath.__dimIdx = i;
 +                    if (oldPoints[i]) {
 +                        symbolPath.setPosition(oldPoints[i]);
 +                        graphic[isInit ? 'initProps' : 'updateProps'](
 +                            symbolPath, {
 +                                x: newPoints[i][0],
 +                                y: newPoints[i][1]
 +                            }, seriesModel, idx
 +                        );
 +                    }
 +                    else {
 +                        symbolPath.setPosition(newPoints[i]);
 +                    }
 +                    symbolGroup.add(symbolPath);
 +                }
 +            }
 +        }
 +
 +        function getInitialPoints(points: number[][]) {
 +            return zrUtil.map(points, function (pt) {
 +                return [polar.cx, polar.cy];
 +            });
 +        }
 +        data.diff(oldData)
 +            .add(function (idx) {
 +                const points = data.getItemLayout(idx);
 +                if (!points) {
 +                    return;
 +                }
 +                const polygon = new graphic.Polygon();
 +                const polyline = new graphic.Polyline();
 +                const target = {
 +                    shape: {
 +                        points: points
 +                    }
 +                };
 +
 +                polygon.shape.points = getInitialPoints(points);
 +                polyline.shape.points = getInitialPoints(points);
 +                graphic.initProps(polygon, target, seriesModel, idx);
 +                graphic.initProps(polyline, target, seriesModel, idx);
 +
 +                const itemGroup = new graphic.Group();
 +                const symbolGroup = new graphic.Group();
 +                itemGroup.add(polyline);
 +                itemGroup.add(polygon);
 +                itemGroup.add(symbolGroup);
 +
 +                updateSymbols(
 +                    polyline.shape.points, points, symbolGroup, data, idx, true
 +                );
 +
 +                data.setItemGraphicEl(idx, itemGroup);
 +            })
 +            .update(function (newIdx, oldIdx) {
 +                const itemGroup = oldData.getItemGraphicEl(oldIdx) as graphic.Group;
 +
 +                const polyline = itemGroup.childAt(0) as graphic.Polyline;
 +                const polygon = itemGroup.childAt(1) as graphic.Polygon;
 +                const symbolGroup = itemGroup.childAt(2) as graphic.Group;
 +                const target = {
 +                    shape: {
 +                        points: data.getItemLayout(newIdx)
 +                    }
 +                };
 +
 +                if (!target.shape.points) {
 +                    return;
 +                }
 +                updateSymbols(
 +                    polyline.shape.points,
 +                    target.shape.points,
 +                    symbolGroup,
 +                    data,
 +                    newIdx,
 +                    false
 +                );
 +
 +                graphic.updateProps(polyline, target, seriesModel);
 +                graphic.updateProps(polygon, target, seriesModel);
 +
 +                data.setItemGraphicEl(newIdx, itemGroup);
 +            })
 +            .remove(function (idx) {
 +                group.remove(oldData.getItemGraphicEl(idx));
 +            })
 +            .execute();
 +
 +        data.eachItemGraphicEl(function (itemGroup: graphic.Group, idx) {
 +            const itemModel = data.getItemModel<RadarSeriesDataItemOption>(idx);
 +            const polyline = itemGroup.childAt(0) as graphic.Polyline;
 +            const polygon = itemGroup.childAt(1) as graphic.Polygon;
 +            const symbolGroup = itemGroup.childAt(2) as graphic.Group;
 +            // Radar uses the visual encoded from itemStyle.
 +            const itemStyle = data.getItemVisual(idx, 'style');
 +            const color = itemStyle.fill;
 +
 +            group.add(itemGroup);
 +
 +            polyline.useStyle(
 +                zrUtil.defaults(
 +                    itemModel.getModel('lineStyle').getLineStyle(),
 +                    {
 +                        fill: 'none',
 +                        stroke: color
 +                    }
 +                )
 +            );
 +
 +            setStatesStylesFromModel(polyline, itemModel, 'lineStyle');
 +            setStatesStylesFromModel(polygon, itemModel, 'areaStyle');
 +
 +            const areaStyleModel = itemModel.getModel('areaStyle');
 +            const polygonIgnore = areaStyleModel.isEmpty() && areaStyleModel.parentModel.isEmpty();
 +
 +            polygon.ignore = polygonIgnore;
 +
 +            zrUtil.each(['emphasis', 'select', 'blur'] as const, function (stateName) {
 +                const stateModel = itemModel.getModel([stateName, 'areaStyle']);
 +                const stateIgnore = stateModel.isEmpty() && stateModel.parentModel.isEmpty();
 +                // Won't be ignore if normal state is not ignore.
 +                polygon.ensureState(stateName).ignore = stateIgnore && polygonIgnore;
 +            });
 +
 +            polygon.useStyle(
 +                zrUtil.defaults(
 +                    areaStyleModel.getAreaStyle(),
 +                    {
 +                        fill: color,
 +                        opacity: 0.7
 +                    }
 +                )
 +            );
 +            const emphasisModel = itemModel.getModel('emphasis');
 +            const itemHoverStyle = emphasisModel.getModel('itemStyle').getItemStyle();
 +            symbolGroup.eachChild(function (symbolPath: RadarSymbol) {
 +                if (symbolPath instanceof ZRImage) {
 +                    const pathStyle = symbolPath.style;
 +                    symbolPath.useStyle(zrUtil.extend({
 +                        // TODO other properties like x, y ?
 +                        image: pathStyle.image,
 +                        x: pathStyle.x, y: pathStyle.y,
 +                        width: pathStyle.width, height: pathStyle.height
 +                    }, itemStyle));
 +                }
 +                else {
 +                    symbolPath.useStyle(itemStyle);
 +                    symbolPath.setColor(color);
 +                }
 +
 +                const pathEmphasisState = symbolPath.ensureState('emphasis');
 +                pathEmphasisState.style = zrUtil.clone(itemHoverStyle);
 +                let defaultText = data.get(data.dimensions[symbolPath.__dimIdx], idx);
 +                (defaultText == null || isNaN(defaultText as number)) && (defaultText = '');
 +
 +                setLabelStyle(
 +                    symbolPath, getLabelStatesModels(itemModel),
 +                    {
 +                        labelFetcher: data.hostModel,
 +                        labelDataIndex: idx,
 +                        labelDimIndex: symbolPath.__dimIdx,
 +                        defaultText: defaultText as string,
 +                        inheritColor: color as ColorString
 +                    }
 +                );
 +            });
 +
 +            enableHoverEmphasis(itemGroup, emphasisModel.get('focus'), emphasisModel.get('blurScope'));
 +        });
 +
 +        this._data = data;
 +    }
 +
 +    remove() {
 +        this.group.removeAll();
 +        this._data = null;
 +    }
 +}
 +
 +ChartView.registerClass(RadarView);
diff --cc src/chart/sunburst/SunburstSeries.ts
index fff56bf,0000000..20cd920
mode 100644,000000..100644
--- a/src/chart/sunburst/SunburstSeries.ts
+++ b/src/chart/sunburst/SunburstSeries.ts
@@@ -1,303 -1,0 +1,311 @@@
 +/*
 +* 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 * as zrUtil from 'zrender/src/core/util';
 +import SeriesModel from '../../model/Series';
 +import Tree, { TreeNode } from '../../data/Tree';
 +import {wrapTreePathInfo} from '../helper/treeHelper';
 +import {
 +    SeriesOption,
 +    CircleLayoutOptionMixin,
 +    LabelOption,
 +    ItemStyleOption,
 +    OptionDataValue,
 +    CallbackDataParams,
 +    StatesOptionMixin,
 +    OptionDataItemObject
 +} from '../../util/types';
 +import GlobalModel from '../../model/Global';
++import List from '../../data/List';
++import Model from '../../model/Model';
 +
 +interface SunburstLabelOption extends Omit<LabelOption, 'rotate' | 'position'> {
 +    rotate?: 'radial' | 'tangential' | number
 +    minAngle?: number
 +    silent?: boolean
 +    position?: LabelOption['position'] | 'outside'
 +}
 +
 +interface SunburstDataParams extends CallbackDataParams {
 +    treePathInfo: {
 +        name: string,
 +        dataIndex: number
 +        value: SunburstSeriesNodeItemOption['value']
 +    }[]
 +}
 +
 +interface ExtraStateOption {
 +    emphasis?: {
 +        focus?: 'descendant' | 'ancestor'
 +    }
 +}
 +
 +export interface SunburstStateOption {
 +    itemStyle?: ItemStyleOption
 +    label?: SunburstLabelOption
 +}
 +
 +export interface SunburstSeriesNodeItemOption extends
 +    SunburstStateOption, StatesOptionMixin<SunburstStateOption, ExtraStateOption>,
 +    OptionDataItemObject<OptionDataValue>
 +{
 +    nodeClick?: 'rootToNode' | 'link'
 +    // Available when nodeClick is link
 +    link?: string
 +    target?: string
 +
 +    children?: SunburstSeriesNodeItemOption[]
 +
 +    collapsed?: boolean
 +
 +    cursor?: string
 +}
 +export interface SunburstSeriesLevelOption extends SunburstStateOption, StatesOptionMixin<SunburstStateOption> {
 +    highlight?: {
 +        itemStyle?: ItemStyleOption
 +        label?: SunburstLabelOption
 +    }
 +}
 +export interface SunburstSeriesOption extends
 +    SeriesOption<SunburstStateOption, ExtraStateOption>, SunburstStateOption,
 +    CircleLayoutOptionMixin {
 +
 +    type?: 'sunburst'
 +
 +    clockwise?: boolean
 +    startAngle?: number
 +    minAngle?: number
 +    /**
 +     * If still show when all data zero.
 +     */
 +    stillShowZeroSum?: boolean
 +    /**
 +     * Policy of highlighting pieces when hover on one
 +     * Valid values: 'none' (for not downplay others), 'descendant',
 +     * 'ancestor', 'self'
 +     */
 +    // highlightPolicy?: 'descendant' | 'ancestor' | 'self'
 +
 +    nodeClick?: 'rootToNode' | 'link'
 +
 +    renderLabelForZeroData?: boolean
 +
 +    levels?: SunburstSeriesLevelOption[]
 +
 +    animationType?: 'expansion' | 'scale'
 +
 +    sort?: 'desc' | 'asc' | ((a: TreeNode, b: TreeNode) => number)
 +}
 +
 +interface SunburstSeriesModel {
 +    getFormattedLabel(
 +        dataIndex: number,
 +        state?: 'emphasis' | 'normal' | 'highlight' | 'blur' | 'select'
 +    ): string
 +}
 +class SunburstSeriesModel extends SeriesModel<SunburstSeriesOption> {
 +
 +    static readonly type = 'series.sunburst';
 +    readonly type = SunburstSeriesModel.type;
 +
 +    ignoreStyleOnData = true;
 +
 +    private _viewRoot: TreeNode;
 +
 +    getInitialData(option: SunburstSeriesOption, ecModel: GlobalModel) {
 +        // Create a virtual root.
 +        const root = { name: option.name, children: option.data } as SunburstSeriesNodeItemOption;
 +
 +        completeTreeValue(root);
 +
-         const levels = option.levels || [];
- 
-         // levels = option.levels = setDefault(levels, ecModel);
- 
-         const treeOption = {
-             levels: levels
-         };
++        const levelModels = zrUtil.map(option.levels || [], function (levelDefine) {
++            return new Model(levelDefine, this, ecModel);
++        }, this);
 +
 +        // Make sure always a new tree is created when setOption,
 +        // in TreemapView, we check whether oldTree === newTree
 +        // to choose mappings approach among old shapes and new shapes.
-         return Tree.createTree(root, this, treeOption).data;
++        const tree = Tree.createTree(root, this, beforeLink);
++
++        function beforeLink(nodeData: List) {
++            nodeData.wrapMethod('getItemModel', function (model, idx) {
++                const node = tree.getNodeByDataIndex(idx);
++                const levelModel = levelModels[node.depth];
++                levelModel && (model.parentModel = levelModel);
++                return model;
++            });
++        }
++        return tree.data;
 +    }
 +
 +    optionUpdated() {
 +        this.resetViewRoot();
 +    }
 +
 +    /*
 +     * @override
 +     */
 +    getDataParams(dataIndex: number) {
 +        const params = super.getDataParams.apply(this, arguments as any) as SunburstDataParams;
 +
 +        const node = this.getData().tree.getNodeByDataIndex(dataIndex);
 +        params.treePathInfo = wrapTreePathInfo<SunburstSeriesNodeItemOption['value']>(node, this);
 +
 +        return params;
 +    }
 +
 +    static defaultOption: SunburstSeriesOption = {
 +        zlevel: 0,
 +        z: 2,
 +
 +        // 默认全局居中
 +        center: ['50%', '50%'],
 +        radius: [0, '75%'],
 +        // 默认顺时针
 +        clockwise: true,
 +        startAngle: 90,
 +        // 最小角度改为0
 +        minAngle: 0,
 +
 +        // If still show when all data zero.
 +        stillShowZeroSum: true,
 +
 +        // 'rootToNode', 'link', or false
 +        nodeClick: 'rootToNode',
 +
 +        renderLabelForZeroData: false,
 +
 +        label: {
 +            // could be: 'radial', 'tangential', or 'none'
 +            rotate: 'radial',
 +            show: true,
 +            opacity: 1,
 +            // 'left' is for inner side of inside, and 'right' is for outter
 +            // side for inside
 +            align: 'center',
 +            position: 'inside',
 +            distance: 5,
 +            silent: true
 +        },
 +        itemStyle: {
 +            borderWidth: 1,
 +            borderColor: 'white',
 +            borderType: 'solid',
 +            shadowBlur: 0,
 +            shadowColor: 'rgba(0, 0, 0, 0.2)',
 +            shadowOffsetX: 0,
 +            shadowOffsetY: 0,
 +            opacity: 1
 +        },
 +
 +        emphasis: {
 +            focus: 'descendant'
 +        },
 +
 +        blur: {
 +            itemStyle: {
 +                opacity: 0.2
 +            },
 +            label: {
 +                opacity: 0.1
 +            }
 +        },
 +
 +        // Animation type canbe expansion, scale
 +        animationType: 'expansion',
 +        animationDuration: 1000,
 +        animationDurationUpdate: 500,
 +
 +        data: [],
 +
 +        levels: [],
 +
 +        /**
 +         * Sort order.
 +         *
 +         * Valid values: 'desc', 'asc', null, or callback function.
 +         * 'desc' and 'asc' for descend and ascendant order;
 +         * null for not sorting;
 +         * example of callback function:
 +         * function(nodeA, nodeB) {
 +         *     return nodeA.getValue() - nodeB.getValue();
 +         * }
 +         */
 +        sort: 'desc'
 +    };
 +
 +    getViewRoot() {
 +        return this._viewRoot;
 +    }
 +
 +    resetViewRoot(viewRoot?: TreeNode) {
 +        viewRoot
 +            ? (this._viewRoot = viewRoot)
 +            : (viewRoot = this._viewRoot);
 +
 +        const root = this.getRawData().tree.root;
 +
 +        if (!viewRoot
 +            || (viewRoot !== root && !root.contains(viewRoot))
 +        ) {
 +            this._viewRoot = root;
 +        }
 +    }
 +}
 +
 +
 +
 +function completeTreeValue(dataNode: SunburstSeriesNodeItemOption) {
 +    // Postorder travel tree.
 +    // If value of none-leaf node is not set,
 +    // calculate it by suming up the value of all children.
 +    let sum = 0;
 +
 +    zrUtil.each(dataNode.children, function (child) {
 +
 +        completeTreeValue(child);
 +
 +        let childValue = child.value;
 +        // TODO First value of array must be a number
 +        zrUtil.isArray(childValue) && (childValue = childValue[0]);
 +        sum += childValue as number;
 +    });
 +
 +    let thisValue = dataNode.value as number;
 +    if (zrUtil.isArray(thisValue)) {
 +        thisValue = thisValue[0];
 +    }
 +
 +    if (thisValue == null || isNaN(thisValue)) {
 +        thisValue = sum;
 +    }
 +    // Value should not less than 0.
 +    if (thisValue < 0) {
 +        thisValue = 0;
 +    }
 +
 +    zrUtil.isArray(dataNode.value)
 +        ? (dataNode.value[0] = thisValue)
 +        : (dataNode.value = thisValue);
 +}
 +
 +
 +SeriesModel.registerClass(SunburstSeriesModel);
 +
 +export default SunburstSeriesModel;
diff --cc src/chart/themeRiver/ThemeRiverSeries.ts
index d1650e5,0000000..2bf5d4f
mode 100644,000000..100644
--- a/src/chart/themeRiver/ThemeRiverSeries.ts
+++ b/src/chart/themeRiver/ThemeRiverSeries.ts
@@@ -1,332 -1,0 +1,325 @@@
 +/*
 +* 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 SeriesModel from '../../model/Series';
 +import createDimensions from '../../data/helper/createDimensions';
 +import {getDimensionTypeByAxis} from '../../data/helper/dimensionHelper';
 +import List from '../../data/List';
 +import * as zrUtil from 'zrender/src/core/util';
 +import {groupData, SINGLE_REFERRING} from '../../util/model';
 +import LegendVisualProvider from '../../visual/LegendVisualProvider';
 +import {
 +    SeriesOption,
 +    SeriesOnSingleOptionMixin,
 +    LabelOption,
 +    OptionDataValueDate,
 +    OptionDataValueNumeric,
 +    ItemStyleOption,
 +    BoxLayoutOptionMixin,
-     ZRColor
++    ZRColor,
++    Dictionary
 +} from '../../util/types';
 +import SingleAxis from '../../coord/single/SingleAxis';
 +import GlobalModel from '../../model/Global';
 +import Single from '../../coord/single/Single';
 +import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup';
 +
 +const DATA_NAME_INDEX = 2;
 +
 +interface ThemeRiverSeriesLabelOption extends LabelOption {
 +    margin?: number
 +}
 +
++type ThemerRiverDataItem = [OptionDataValueDate, OptionDataValueNumeric, string];
++
 +export interface ThemeRiverStateOption {
 +    label?: ThemeRiverSeriesLabelOption
 +    itemStyle?: ItemStyleOption
 +}
 +
 +export interface ThemeRiverSeriesOption extends SeriesOption<ThemeRiverStateOption>, ThemeRiverStateOption,
 +    SeriesOnSingleOptionMixin, BoxLayoutOptionMixin {
 +    type?: 'themeRiver'
 +
 +    color?: ZRColor[]
 +
 +    coordinateSystem?: 'singleAxis'
 +
 +    /**
 +     * gap in axis's orthogonal orientation
 +     */
 +    boundaryGap?: (string | number)[]
 +    /**
 +     * [date, value, name]
 +     */
-     data?: [OptionDataValueDate, OptionDataValueNumeric, string][]
++    data?: ThemerRiverDataItem[]
 +}
 +
 +class ThemeRiverSeriesModel extends SeriesModel<ThemeRiverSeriesOption> {
 +    static readonly type = 'series.themeRiver';
 +    readonly type = ThemeRiverSeriesModel.type;
 +
 +    static readonly dependencies = ['singleAxis'];
 +
 +    nameMap: zrUtil.HashMap<number, string>;
 +
 +    coordinateSystem: Single;
 +
 +    useColorPaletteOnData = true;
 +
 +    /**
 +     * @override
 +     */
 +    init(option: ThemeRiverSeriesOption) {
 +        // eslint-disable-next-line
 +        super.init.apply(this, arguments as any);
 +
 +        // Put this function here is for the sake of consistency of code style.
 +        // Enable legend selection for each data item
 +        // Use a function instead of direct access because data reference may changed
 +        this.legendVisualProvider = new LegendVisualProvider(
 +            zrUtil.bind(this.getData, this), zrUtil.bind(this.getRawData, this)
 +        );
 +    }
 +
 +    /**
 +     * If there is no value of a certain point in the time for some event,set it value to 0.
 +     *
 +     * @param {Array} data  initial data in the option
 +     * @return {Array}
 +     */
 +    fixData(data: ThemeRiverSeriesOption['data']) {
 +        let rawDataLength = data.length;
++        /**
++         * Make sure every layer data get the same keys.
++         * The value index tells which layer has visited.
++         * {
++         *  2014/01/01: -1
++         * }
++         */
++        const timeValueKeys: Dictionary<number> = {};
 +
 +        // grouped data by name
-         const groupResult = groupData(data, function (item) {
++        const groupResult = groupData(data, (item: ThemerRiverDataItem) => {
++            if (!timeValueKeys.hasOwnProperty(item[0] + '')) {
++                timeValueKeys[item[0] + ''] = -1;
++            }
 +            return item[2];
 +        });
-         const layData: {
-             name: string,
-             dataList: ThemeRiverSeriesOption['data']
-         }[] = [];
++        const layerData: {name: string, dataList: ThemerRiverDataItem[]}[] = [];
 +        groupResult.buckets.each(function (items, key) {
-             layData.push({
-                 name: key,
-                 dataList: items
++            layerData.push({
++                name: key, dataList: items
 +            });
 +        });
- 
-         const layerNum = layData.length;
-         let largestLayer = -1;
-         let index = -1;
-         for (let i = 0; i < layerNum; ++i) {
-             const len = layData[i].dataList.length;
-             if (len > largestLayer) {
-                 largestLayer = len;
-                 index = i;
-             }
-         }
++        const layerNum = layerData.length;
 +
 +        for (let k = 0; k < layerNum; ++k) {
-             if (k === index) {
-                 continue;
++            const name = layerData[k].name;
++            for (let j = 0; j < layerData[k].dataList.length; ++j) {
++                const timeValue = layerData[k].dataList[j][0] = '';
++                timeValueKeys[timeValue] = k;
 +            }
-             const name = layData[k].name;
-             for (let j = 0; j < largestLayer; ++j) {
-                 const timeValue = layData[index].dataList[j][0];
-                 const length = layData[k].dataList.length;
-                 let keyIndex = -1;
-                 for (let l = 0; l < length; ++l) {
-                     const value = layData[k].dataList[l][0];
-                     if (value === timeValue) {
-                         keyIndex = l;
-                         break;
-                     }
-                 }
-                 if (keyIndex === -1) {
++
++            for (const timeValue in timeValueKeys) {
++                if (timeValueKeys.hasOwnProperty(timeValue) && timeValueKeys[timeValue] !== k) {
++                    timeValueKeys[timeValue] = k;
 +                    data[rawDataLength] = [timeValue, 0, name];
 +                    rawDataLength++;
- 
 +                }
 +            }
++
 +        }
 +        return data;
 +    }
 +
 +    /**
 +     * @override
 +     * @param  option  the initial option that user gived
 +     * @param  ecModel  the model object for themeRiver option
 +     */
 +    getInitialData(option: ThemeRiverSeriesOption, ecModel: GlobalModel): List {
 +
 +        const singleAxisModel = this.getReferringComponents('singleAxis', SINGLE_REFERRING).models[0];
 +
 +        const axisType = singleAxisModel.get('type');
 +
 +        // filter the data item with the value of label is undefined
 +        const filterData = zrUtil.filter(option.data, function (dataItem) {
 +            return dataItem[2] !== undefined;
 +        });
 +
 +        // ??? TODO design a stage to transfer data for themeRiver and lines?
 +        const data = this.fixData(filterData || []);
 +        const nameList = [];
 +        const nameMap = this.nameMap = zrUtil.createHashMap();
 +        let count = 0;
 +
 +        for (let i = 0; i < data.length; ++i) {
 +            nameList.push(data[i][DATA_NAME_INDEX]);
 +            if (!nameMap.get(data[i][DATA_NAME_INDEX] as string)) {
 +                nameMap.set(data[i][DATA_NAME_INDEX] as string, count);
 +                count++;
 +            }
 +        }
 +
 +        const dimensionsInfo = createDimensions(data, {
 +            coordDimensions: ['single'],
 +            dimensionsDefine: [
 +                {
 +                    name: 'time',
 +                    type: getDimensionTypeByAxis(axisType)
 +                },
 +                {
 +                    name: 'value',
 +                    type: 'float'
 +                },
 +                {
 +                    name: 'name',
 +                    type: 'ordinal'
 +                }
 +            ],
 +            encodeDefine: {
 +                single: 0,
 +                value: 1,
 +                itemName: 2
 +            }
 +        });
 +
 +        const list = new List(dimensionsInfo, this);
 +        list.initData(data);
 +
 +        return list;
 +    }
 +
 +    /**
 +     * The raw data is divided into multiple layers and each layer
 +     *     has same name.
 +     */
 +    getLayerSeries() {
 +        const data = this.getData();
 +        const lenCount = data.count();
 +        const indexArr = [];
 +
 +        for (let i = 0; i < lenCount; ++i) {
 +            indexArr[i] = i;
 +        }
 +
 +        const timeDim = data.mapDimension('single');
 +
 +        // data group by name
 +        const groupResult = groupData(indexArr, function (index) {
 +            return data.get('name', index) as string;
 +        });
 +        const layerSeries: {
 +            name: string
 +            indices: number[]
 +        }[] = [];
 +        groupResult.buckets.each(function (items: number[], key: string) {
 +            items.sort(function (index1: number, index2: number) {
 +                return data.get(timeDim, index1) as number - (data.get(timeDim, index2) as number);
 +            });
 +            layerSeries.push({
 +                name: key,
 +                indices: items
 +            });
 +        });
 +
 +        return layerSeries;
 +    }
 +
 +    /**
 +     * Get data indices for show tooltip content
 +     */
 +    getAxisTooltipData(dim: string | string[], value: number, baseAxis: SingleAxis) {
 +        if (!zrUtil.isArray(dim)) {
 +            dim = dim ? [dim] : [];
 +        }
 +
 +        const data = this.getData();
 +        const layerSeries = this.getLayerSeries();
 +        const indices = [];
 +        const layerNum = layerSeries.length;
 +        let nestestValue;
 +
 +        for (let i = 0; i < layerNum; ++i) {
 +            let minDist = Number.MAX_VALUE;
 +            let nearestIdx = -1;
 +            const pointNum = layerSeries[i].indices.length;
 +            for (let j = 0; j < pointNum; ++j) {
 +                const theValue = data.get(dim[0], layerSeries[i].indices[j]) as number;
 +                const dist = Math.abs(theValue - value);
 +                if (dist <= minDist) {
 +                    nestestValue = theValue;
 +                    minDist = dist;
 +                    nearestIdx = layerSeries[i].indices[j];
 +                }
 +            }
 +            indices.push(nearestIdx);
 +        }
 +
 +        return {dataIndices: indices, nestestValue: nestestValue};
 +    }
 +
 +    formatTooltip(
 +        dataIndex: number,
 +        multipleSeries: boolean,
 +        dataType: string
 +    ) {
 +        const data = this.getData();
 +        const name = data.getName(dataIndex);
 +        const value = data.get(data.mapDimension('value'), dataIndex);
 +
 +        return createTooltipMarkup('nameValue', { name: name, value: value });
 +    }
 +
 +    static defaultOption: ThemeRiverSeriesOption = {
 +        zlevel: 0,
 +        z: 2,
 +
 +        coordinateSystem: 'singleAxis',
 +
 +        // gap in axis's orthogonal orientation
 +        boundaryGap: ['10%', '10%'],
 +
 +        // legendHoverLink: true,
 +
 +        singleAxisIndex: 0,
 +
 +        animationEasing: 'linear',
 +
 +        label: {
 +            margin: 4,
 +            show: true,
 +            position: 'left',
 +            fontSize: 11
 +        },
 +
 +        emphasis: {
 +
 +            label: {
 +                show: true
 +            }
 +        }
 +    };
 +}
 +
 +SeriesModel.registerClass(ThemeRiverSeriesModel);
 +
 +export default ThemeRiverSeriesModel;
diff --cc src/chart/tree/TreeSeries.ts
index e5a49f6,0000000..912e9a6
mode 100644,000000..100644
--- a/src/chart/tree/TreeSeries.ts
+++ b/src/chart/tree/TreeSeries.ts
@@@ -1,291 -1,0 +1,291 @@@
 +/*
 +* 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 SeriesModel from '../../model/Series';
 +import Tree from '../../data/Tree';
 +import {
 +    SeriesOption,
 +    SymbolOptionMixin,
 +    BoxLayoutOptionMixin,
 +    RoamOptionMixin,
 +    LineStyleOption,
 +    ItemStyleOption,
 +    LabelOption,
 +    OptionDataValue,
 +    StatesOptionMixin,
 +    OptionDataItemObject
 +} from '../../util/types';
 +import List from '../../data/List';
 +import View from '../../coord/View';
 +import { LayoutRect } from '../../util/layout';
 +import Model from '../../model/Model';
 +import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup';
 +
 +interface CurveLineStyleOption extends LineStyleOption{
 +    curveness?: number
 +}
 +
 +export interface TreeSeriesStateOption {
 +    itemStyle?: ItemStyleOption
 +    /**
 +     * Line style of the edge between node and it's parent.
 +     */
 +    lineStyle?: CurveLineStyleOption
 +    label?: LabelOption
 +}
 +
 +interface ExtraStateOption {
 +    emphasis?: {
 +        focus?: 'ancestor' | 'descendant'
 +        scale?: boolean
 +    }
 +}
 +
 +export interface TreeSeriesNodeItemOption extends SymbolOptionMixin,
 +    TreeSeriesStateOption, StatesOptionMixin<TreeSeriesStateOption, ExtraStateOption>,
 +    OptionDataItemObject<OptionDataValue> {
 +
 +    children?: TreeSeriesNodeItemOption[]
 +
 +    collapsed?: boolean
 +
 +    link?: string
 +    target?: string
 +}
 +
 +/**
 + * Configuration of leaves nodes.
 + */
 +export interface TreeSeriesLeavesOption extends TreeSeriesStateOption, StatesOptionMixin<TreeSeriesStateOption> {
 +
 +}
 +
 +export interface TreeSeriesOption extends
 +    SeriesOption<TreeSeriesStateOption, ExtraStateOption>, TreeSeriesStateOption,
 +    SymbolOptionMixin, BoxLayoutOptionMixin, RoamOptionMixin {
 +    type?: 'tree'
 +
 +    layout?: 'orthogonal' | 'radial'
 +
 +    edgeShape?: 'polyline' | 'curve'
 +
 +    /**
 +     * Available when edgeShape is polyline
 +     */
 +    edgeForkPosition?: string | number
 +
 +    nodeScaleRatio?: number
 +
 +    /**
 +     * The orient of orthoginal layout, can be setted to 'LR', 'TB', 'RL', 'BT'.
 +     * and the backward compatibility configuration 'horizontal = LR', 'vertical = TB'.
 +     */
 +    orient?: 'LR' | 'TB' | 'RL' | 'BT' | 'horizontal' | 'vertical'
 +
 +    expandAndCollapse?: boolean
 +
 +    /**
 +     * The initial expanded depth of tree
 +     */
 +    initialTreeDepth?: number
 +
 +    leaves?: TreeSeriesLeavesOption
 +
 +    data?: TreeSeriesNodeItemOption[]
 +}
 +
 +class TreeSeriesModel extends SeriesModel<TreeSeriesOption> {
 +    static readonly type = 'series.tree';
 +
 +    // can support the position parameters 'left', 'top','right','bottom', 'width',
 +    // 'height' in the setOption() with 'merge' mode normal.
 +    static readonly layoutMode = 'box';
 +
 +    coordinateSystem: View;
 +
 +    layoutInfo: LayoutRect;
 +
 +    hasSymbolVisual = true;
 +
 +    // Do it self.
 +    ignoreStyleOnData = true;
 +
 +    /**
 +     * Init a tree data structure from data in option series
 +     * @param  option  the object used to config echarts view
 +     * @return storage initial data
 +     */
 +    getInitialData(option: TreeSeriesOption): List {
 +
 +        //create an virtual root
 +        const root: TreeSeriesNodeItemOption = {
 +            name: option.name,
 +            children: option.data
 +        };
 +
 +        const leaves = option.leaves || {};
 +        const leavesModel = new Model(leaves, this, this.ecModel);
 +
-         const tree = Tree.createTree(root, this, {}, beforeLink);
++        const tree = Tree.createTree(root, this, beforeLink);
 +
 +        function beforeLink(nodeData: List) {
 +            nodeData.wrapMethod('getItemModel', function (model, idx) {
 +                const node = tree.getNodeByDataIndex(idx);
 +                if (!node.children.length || !node.isExpand) {
 +                    model.parentModel = leavesModel;
 +                }
 +                return model;
 +            });
 +        }
 +
 +        let treeDepth = 0;
 +
 +        tree.eachNode('preorder', function (node) {
 +            if (node.depth > treeDepth) {
 +                treeDepth = node.depth;
 +            }
 +        });
 +
 +        const expandAndCollapse = option.expandAndCollapse;
 +        const expandTreeDepth = (expandAndCollapse && option.initialTreeDepth >= 0)
 +            ? option.initialTreeDepth : treeDepth;
 +
 +        tree.root.eachNode('preorder', function (node) {
 +            const item = node.hostTree.data.getRawDataItem(node.dataIndex) as TreeSeriesNodeItemOption;
 +            // Add item.collapsed != null, because users can collapse node original in the series.data.
 +            node.isExpand = (item && item.collapsed != null)
 +                ? !item.collapsed
 +                : node.depth <= expandTreeDepth;
 +        });
 +
 +        return tree.data;
 +    }
 +
 +    /**
 +     * Make the configuration 'orient' backward compatibly, with 'horizontal = LR', 'vertical = TB'.
 +     * @returns {string} orient
 +     */
 +    getOrient() {
 +        let orient = this.get('orient');
 +        if (orient === 'horizontal') {
 +            orient = 'LR';
 +        }
 +        else if (orient === 'vertical') {
 +            orient = 'TB';
 +        }
 +        return orient;
 +    }
 +
 +    setZoom(zoom: number) {
 +        this.option.zoom = zoom;
 +    }
 +
 +    setCenter(center: number[]) {
 +        this.option.center = center;
 +    }
 +
 +    formatTooltip(
 +        dataIndex: number,
 +        multipleSeries: boolean,
 +        dataType: string
 +    ) {
 +        const tree = this.getData().tree;
 +        const realRoot = tree.root.children[0];
 +        let node = tree.getNodeByDataIndex(dataIndex);
 +        const value = node.getValue();
 +        let name = node.name;
 +        while (node && (node !== realRoot)) {
 +            name = node.parentNode.name + '.' + name;
 +            node = node.parentNode;
 +        }
 +
 +        return createTooltipMarkup('nameValue', {
 +            name: name,
 +            value: value,
 +            noValue: isNaN(value as number) || value == null
 +        });
 +    }
 +
 +    static defaultOption: TreeSeriesOption = {
 +        zlevel: 0,
 +        z: 2,
 +        coordinateSystem: 'view',
 +
 +        // the position of the whole view
 +        left: '12%',
 +        top: '12%',
 +        right: '12%',
 +        bottom: '12%',
 +
 +        // the layout of the tree, two value can be selected, 'orthogonal' or 'radial'
 +        layout: 'orthogonal',
 +
 +        // value can be 'polyline'
 +        edgeShape: 'curve',
 +
 +        edgeForkPosition: '50%',
 +
 +        // true | false | 'move' | 'scale', see module:component/helper/RoamController.
 +        roam: false,
 +
 +        // Symbol size scale ratio in roam
 +        nodeScaleRatio: 0.4,
 +
 +        // Default on center of graph
 +        center: null,
 +
 +        zoom: 1,
 +
 +        orient: 'LR',
 +
 +        symbol: 'emptyCircle',
 +
 +        symbolSize: 7,
 +
 +        expandAndCollapse: true,
 +
 +        initialTreeDepth: 2,
 +
 +        lineStyle: {
 +            color: '#ccc',
 +            width: 1.5,
 +            curveness: 0.5
 +        },
 +
 +        itemStyle: {
 +            color: 'lightsteelblue',
 +            borderColor: '#c23531',
 +            borderWidth: 1.5
 +        },
 +
 +        label: {
 +            show: true
 +        },
 +
 +        animationEasing: 'linear',
 +
 +        animationDuration: 700,
 +
 +        animationDurationUpdate: 500
 +    };
 +}
 +
 +SeriesModel.registerClass(TreeSeriesModel);
 +
 +export default TreeSeriesModel;
diff --cc src/chart/treemap/TreemapSeries.ts
index 5444a26,0000000..9bc1852
mode 100644,000000..100644
--- a/src/chart/treemap/TreemapSeries.ts
+++ b/src/chart/treemap/TreemapSeries.ts
@@@ -1,543 -1,0 +1,553 @@@
 +/*
 +* 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 * as zrUtil from 'zrender/src/core/util';
 +import SeriesModel from '../../model/Series';
 +import Tree, { TreeNode } from '../../data/Tree';
 +import Model from '../../model/Model';
 +import {wrapTreePathInfo} from '../helper/treeHelper';
 +import {
 +    SeriesOption,
 +    BoxLayoutOptionMixin,
 +    ItemStyleOption,
 +    LabelOption,
 +    RoamOptionMixin,
 +    CallbackDataParams,
 +    ColorString,
 +    StatesOptionMixin
 +} from '../../util/types';
 +import GlobalModel from '../../model/Global';
 +import { LayoutRect } from '../../util/layout';
 +import List from '../../data/List';
 +import { normalizeToArray } from '../../util/model';
 +import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup';
 +
 +// Only support numberic value.
 +type TreemapSeriesDataValue = number | number[];
 +
 +interface BreadcrumbItemStyleOption extends ItemStyleOption {
 +    // TODO: textStyle should be in breadcrumb.label
 +    textStyle?: LabelOption
 +}
 +
 +interface TreemapSeriesLabelOption extends LabelOption {
 +    ellipsis?: boolean
 +    formatter?: string | ((params: CallbackDataParams) => string)
 +}
 +
 +interface TreemapSeriesItemStyleOption extends ItemStyleOption {
 +    borderRadius?: number | number[]
 +
 +    colorAlpha?: number
 +    colorSaturation?: number
 +
 +    borderColorSaturation?: number
 +
 +    gapWidth?: number
 +}
 +
 +interface TreePathInfo {
 +    name: string
 +    dataIndex: number
 +    value: TreemapSeriesDataValue
 +}
 +
 +interface TreemapSeriesCallbackDataParams extends CallbackDataParams {
 +    treePathInfo?: TreePathInfo[]
 +}
 +
 +interface ExtraStateOption {
 +    emphasis?: {
 +        focus?: 'descendant' | 'ancestor'
 +    }
 +}
 +
 +export interface TreemapStateOption {
 +    itemStyle?: TreemapSeriesItemStyleOption
 +    label?: TreemapSeriesLabelOption
 +    upperLabel?: TreemapSeriesLabelOption
 +}
 +
 +export interface TreemapSeriesVisualOption {
 +    /**
 +     * Which dimension will be applied with the visual properties.
 +     */
 +    visualDimension?: number | string
 +
 +    colorMappingBy?: 'value' | 'index' | 'id'
 +
 +    visualMin?: number
 +    visualMax?: number
 +
 +    colorAlpha?: number[] | 'none'
 +    colorSaturation?: number[] | 'none'
 +    // A color list for a level. Each node in the level will obtain a color from the color list.
 +    // Only suuport ColorString for interpolation
 +    // color?: ColorString[]
 +
 +    /**
 +     * A node will not be shown when its area size is smaller than this value (unit: px square).
 +     */
 +    visibleMin?: number
 +    /**
 +     * Children will not be shown when area size of a node is smaller than this value (unit: px square).
 +     */
 +    childrenVisibleMin?: number
 +}
 +
 +export interface TreemapSeriesLevelOption extends TreemapSeriesVisualOption,
 +    TreemapStateOption, StatesOptionMixin<TreemapStateOption, ExtraStateOption> {
 +
 +    color?: ColorString[] | 'none'
 +}
 +
 +export interface TreemapSeriesNodeItemOption extends TreemapSeriesVisualOption,
 +    TreemapStateOption, StatesOptionMixin<TreemapStateOption, ExtraStateOption> {
 +    id?: string
 +    name?: string
 +
 +    value?: TreemapSeriesDataValue
 +
 +    children?: TreemapSeriesNodeItemOption[]
 +
 +    color?: ColorString[] | 'none'
 +}
 +
 +export interface TreemapSeriesOption
 +    extends SeriesOption<TreemapStateOption, ExtraStateOption>, TreemapStateOption,
 +    BoxLayoutOptionMixin,
 +    RoamOptionMixin,
 +    TreemapSeriesVisualOption {
 +
 +    type?: 'treemap'
 +
 +    /**
 +     * configuration in echarts2
 +     * @deprecated
 +     */
 +    size?: (number | string)[]
 +
 +    /**
 +     * If sort in desc order.
 +     * Default to be desc. asc has strange effect
 +     */
 +    sort?: boolean | 'asc' | 'desc'
 +
 +    /**
 +     * Size of clipped window when zooming. 'origin' or 'fullscreen'
 +     */
 +    clipWindow?: 'origin' | 'fullscreen'
 +
 +    squareRatio: number
 +    /**
 +     * Nodes on depth from root are regarded as leaves.
 +     * Count from zero (zero represents only view root).
 +     */
 +    leafDepth: number
 +
 +    drillDownIcon?: string
 +
 +    /**
 +     * Be effective when using zoomToNode. Specify the proportion of the
 +     * target node area in the view area.
 +     */
 +    zoomToNodeRatio?: number
 +    /**
 +     * Leaf node click behaviour: 'zoomToNode', 'link', false.
 +     * If leafDepth is set and clicking a node which has children but
 +     * be on left depth, the behaviour would be changing root. Otherwise
 +     * use behavious defined above.
 +     */
 +    nodeClick?: 'zoomToNode' | 'link'
 +
 +    breadcrumb?: BoxLayoutOptionMixin & {
 +        show?: boolean
 +        height?: number
 +
 +        emptyItemWidth: number  // With of empty width
 +        itemStyle?: BreadcrumbItemStyleOption
 +
 +        emphasis?: {
 +            itemStyle?: BreadcrumbItemStyleOption
 +        }
 +    }
 +
 +    levels?: TreemapSeriesLevelOption[]
 +
 +    data?: TreemapSeriesNodeItemOption[]
 +}
 +
 +class TreemapSeriesModel extends SeriesModel<TreemapSeriesOption> {
 +
 +    static type = 'series.treemap';
 +    type = TreemapSeriesModel.type;
 +
 +    static layoutMode = 'box' as const;
 +
 +    preventUsingHoverLayer = true;
 +
 +    layoutInfo: LayoutRect;
 +
++    designatedVisualItemStyle: TreemapSeriesItemStyleOption;
++
 +    private _viewRoot: TreeNode;
 +    private _idIndexMap: zrUtil.HashMap<number>;
 +    private _idIndexMapCount: number;
 +
 +    static defaultOption: TreemapSeriesOption = {
 +        // Disable progressive rendering
 +        progressive: 0,
 +        // size: ['80%', '80%'],            // deprecated, compatible with ec2.
 +        left: 'center',
 +        top: 'middle',
 +        width: '80%',
 +        height: '80%',
 +        sort: true,
 +
 +        clipWindow: 'origin',
 +        squareRatio: 0.5 * (1 + Math.sqrt(5)), // golden ratio
 +        leafDepth: null,
 +
 +        drillDownIcon: '▶',                 // Use html character temporarily because it is complicated
 +                                            // to align specialized icon. ▷▶❒❐▼✚
 +
 +        zoomToNodeRatio: 0.32 * 0.32,
 +
 +        roam: true,
 +        nodeClick: 'zoomToNode',
 +        animation: true,
 +        animationDurationUpdate: 900,
 +        animationEasing: 'quinticInOut',
 +        breadcrumb: {
 +            show: true,
 +            height: 22,
 +            left: 'center',
 +            top: 'bottom',
 +            // right
 +            // bottom
 +            emptyItemWidth: 25,             // Width of empty node.
 +            itemStyle: {
 +                color: 'rgba(0,0,0,0.7)', //'#5793f3',
 +                textStyle: {
 +                    color: '#fff'
 +                }
 +            }
 +        },
 +        label: {
 +            show: true,
 +            // Do not use textDistance, for ellipsis rect just the same as treemap node rect.
 +            distance: 0,
 +            padding: 5,
 +            position: 'inside', // Can be [5, '5%'] or position stirng like 'insideTopLeft', ...
 +            // formatter: null,
 +            color: '#fff',
 +            overflow: 'truncate'
 +            // align
 +            // verticalAlign
 +        },
 +        upperLabel: {                   // Label when node is parent.
 +            show: false,
 +            position: [0, '50%'],
 +            height: 20,
 +            // formatter: null,
 +            // color: '#fff',
 +            overflow: 'truncate',
 +            // align: null,
 +            verticalAlign: 'middle'
 +        },
 +        itemStyle: {
 +            color: null,            // Can be 'none' if not necessary.
 +            colorAlpha: null,       // Can be 'none' if not necessary.
 +            colorSaturation: null,  // Can be 'none' if not necessary.
 +            borderWidth: 0,
 +            gapWidth: 0,
 +            borderColor: '#fff',
 +            borderColorSaturation: null // If specified, borderColor will be ineffective, and the
 +                                        // border color is evaluated by color of current node and
 +                                        // borderColorSaturation.
 +        },
 +        emphasis: {
 +            upperLabel: {
 +                show: true,
 +                position: [0, '50%'],
 +                ellipsis: true,
 +                verticalAlign: 'middle'
 +            }
 +        },
 +
 +        visualDimension: 0,                 // Can be 0, 1, 2, 3.
 +        visualMin: null,
 +        visualMax: null,
 +
 +        color: [],                  // + treemapSeries.color should not be modified. Please only modified
 +                                    // level[n].color (if necessary).
 +                                    // + Specify color list of each level. level[0].color would be global
 +                                    // color list if not specified. (see method `setDefault`).
 +                                    // + But set as a empty array to forbid fetch color from global palette
 +                                    // when using nodeModel.get('color'), otherwise nodes on deep level
 +                                    // will always has color palette set and are not able to inherit color
 +                                    // from parent node.
 +                                    // + TreemapSeries.color can not be set as 'none', otherwise effect
 +                                    // legend color fetching (see seriesColor.js).
 +        colorAlpha: null,           // Array. Specify color alpha range of each level, like [0.2, 0.8]
 +        colorSaturation: null,      // Array. Specify color saturation of each level, like [0.2, 0.5]
 +        colorMappingBy: 'index',    // 'value' or 'index' or 'id'.
 +        visibleMin: 10,             // If area less than this threshold (unit: pixel^2), node will not
 +                                    // be rendered. Only works when sort is 'asc' or 'desc'.
 +        childrenVisibleMin: null,   // If area of a node less than this threshold (unit: pixel^2),
 +                                    // grandchildren will not show.
 +                                    // Why grandchildren? If not grandchildren but children,
 +                                    // some siblings show children and some not,
 +                                    // the appearance may be mess and not consistent,
 +        levels: []                  // Each item: {
 +                                    //     visibleMin, itemStyle, visualDimension, label
 +                                    // }
 +        // data: {
 +        //      value: [],
 +        //      children: [],
 +        //      link: 'http://xxx.xxx.xxx',
 +        //      target: 'blank' or 'self'
 +        // }
 +    };
 +
 +    /**
 +     * @override
 +     */
 +    getInitialData(option: TreemapSeriesOption, ecModel: GlobalModel) {
 +        // Create a virtual root.
 +        const root: TreemapSeriesNodeItemOption = {
 +            name: option.name,
 +            children: option.data
 +        };
 +
 +        completeTreeValue(root);
 +
 +        let levels = option.levels || [];
 +
++        // Used in "visual priority" in `treemapVisual.js`.
++        // This way is a little tricky, must satisfy the precondition:
++        //   1. There is no `treeNode.getModel('itemStyle.xxx')` used.
++        //   2. The `Model.prototype.getModel()` will not use any clone-like way.
++        const designatedVisualItemStyle = this.designatedVisualItemStyle = {};
++        const designatedVisualModel = new Model({itemStyle: designatedVisualItemStyle}, this, ecModel);
++
 +        levels = option.levels = setDefault(levels, ecModel);
 +        const levelModels = zrUtil.map(levels || [], function (levelDefine) {
-             return new Model(levelDefine, this, ecModel);
++            return new Model(levelDefine, designatedVisualModel, ecModel);
 +        }, this);
 +
 +        // Make sure always a new tree is created when setOption,
 +        // in TreemapView, we check whether oldTree === newTree
 +        // to choose mappings approach among old shapes and new shapes.
-         const tree = Tree.createTree(root, this, null, beforeLink);
++        const tree = Tree.createTree(root, this, beforeLink);
 +
 +        function beforeLink(nodeData: List) {
 +            nodeData.wrapMethod('getItemModel', function (model, idx) {
 +                const node = tree.getNodeByDataIndex(idx);
 +                const levelModel = levelModels[node.depth];
-                 levelModel && (model.parentModel = levelModel);
++                // If no levelModel, we also need `designatedVisualModel`.
++                model.parentModel = levelModel || designatedVisualModel;
 +                return model;
 +            });
 +        }
 +
 +        return tree.data;
 +    }
 +
 +    optionUpdated() {
 +        this.resetViewRoot();
 +    }
 +
 +    /**
 +     * @override
 +     * @param {number} dataIndex
 +     * @param {boolean} [mutipleSeries=false]
 +     */
 +    formatTooltip(
 +        dataIndex: number,
 +        multipleSeries: boolean,
 +        dataType: string
 +    ) {
 +        const data = this.getData();
 +        const value = this.getRawValue(dataIndex) as TreemapSeriesDataValue;
 +        const name = data.getName(dataIndex);
 +
 +        return createTooltipMarkup('nameValue', { name: name, value: value });
 +    }
 +
 +    /**
 +     * Add tree path to tooltip param
 +     *
 +     * @override
 +     * @param {number} dataIndex
 +     * @return {Object}
 +     */
 +    getDataParams(dataIndex: number) {
 +        const params = super.getDataParams.apply(this, arguments as any) as TreemapSeriesCallbackDataParams;
 +
 +        const node = this.getData().tree.getNodeByDataIndex(dataIndex);
 +        params.treePathInfo = wrapTreePathInfo(node, this);
 +
 +        return params;
 +    }
 +
 +    /**
 +     * @public
 +     * @param {Object} layoutInfo {
 +     *                                x: containerGroup x
 +     *                                y: containerGroup y
 +     *                                width: containerGroup width
 +     *                                height: containerGroup height
 +     *                            }
 +     */
 +    setLayoutInfo(layoutInfo: LayoutRect) {
 +        /**
 +         * @readOnly
 +         * @type {Object}
 +         */
 +        this.layoutInfo = this.layoutInfo || {} as LayoutRect;
 +        zrUtil.extend(this.layoutInfo, layoutInfo);
 +    }
 +
 +    /**
 +     * @param  {string} id
 +     * @return {number} index
 +     */
 +    mapIdToIndex(id: string): number {
 +        // A feature is implemented:
 +        // index is monotone increasing with the sequence of
 +        // input id at the first time.
 +        // This feature can make sure that each data item and its
 +        // mapped color have the same index between data list and
 +        // color list at the beginning, which is useful for user
 +        // to adjust data-color mapping.
 +
 +        /**
 +         * @private
 +         * @type {Object}
 +         */
 +        let idIndexMap = this._idIndexMap;
 +
 +        if (!idIndexMap) {
 +            idIndexMap = this._idIndexMap = zrUtil.createHashMap();
 +            /**
 +             * @private
 +             * @type {number}
 +             */
 +            this._idIndexMapCount = 0;
 +        }
 +
 +        let index = idIndexMap.get(id);
 +        if (index == null) {
 +            idIndexMap.set(id, index = this._idIndexMapCount++);
 +        }
 +
 +        return index;
 +    }
 +
 +    getViewRoot() {
 +        return this._viewRoot;
 +    }
 +
 +    resetViewRoot(viewRoot?: TreeNode) {
 +        viewRoot
 +            ? (this._viewRoot = viewRoot)
 +            : (viewRoot = this._viewRoot);
 +
 +        const root = this.getRawData().tree.root;
 +
 +        if (!viewRoot
 +            || (viewRoot !== root && !root.contains(viewRoot))
 +        ) {
 +            this._viewRoot = root;
 +        }
 +    }
 +}
 +
 +/**
 + * @param {Object} dataNode
 + */
 +function completeTreeValue(dataNode: TreemapSeriesNodeItemOption) {
 +    // Postorder travel tree.
 +    // If value of none-leaf node is not set,
 +    // calculate it by suming up the value of all children.
 +    let sum = 0;
 +
 +    zrUtil.each(dataNode.children, function (child) {
 +
 +        completeTreeValue(child);
 +
 +        let childValue = child.value;
 +        zrUtil.isArray(childValue) && (childValue = childValue[0]);
 +
 +        sum += childValue;
 +    });
 +
 +    let thisValue = dataNode.value;
 +    if (zrUtil.isArray(thisValue)) {
 +        thisValue = thisValue[0];
 +    }
 +
 +    if (thisValue == null || isNaN(thisValue)) {
 +        thisValue = sum;
 +    }
 +    // Value should not less than 0.
 +    if (thisValue < 0) {
 +        thisValue = 0;
 +    }
 +
 +    zrUtil.isArray(dataNode.value)
 +        ? (dataNode.value[0] = thisValue)
 +        : (dataNode.value = thisValue);
 +}
 +
 +/**
 + * set default to level configuration
 + */
 +function setDefault(levels: TreemapSeriesLevelOption[], ecModel: GlobalModel) {
 +    const globalColorList = normalizeToArray(ecModel.get('color')) as ColorString[];
 +
 +    if (!globalColorList) {
 +        return;
 +    }
 +
 +    levels = levels || [];
 +    let hasColorDefine;
 +    zrUtil.each(levels, function (levelDefine) {
 +        const model = new Model(levelDefine);
 +        const modelColor = model.get('color');
 +
 +        if (model.get(['itemStyle', 'color'])
 +            || (modelColor && modelColor !== 'none')
 +        ) {
 +            hasColorDefine = true;
 +        }
 +    });
 +
 +    if (!hasColorDefine) {
 +        const level0 = levels[0] || (levels[0] = {});
 +        level0.color = globalColorList.slice();
 +    }
 +
 +    return levels;
 +}
 +
 +SeriesModel.registerClass(TreemapSeriesModel);
 +
 +export default TreemapSeriesModel;
diff --cc src/chart/treemap/treemapLayout.ts
index 7939b9e,0000000..9046d32
mode 100644,000000..100644
--- a/src/chart/treemap/treemapLayout.ts
+++ b/src/chart/treemap/treemapLayout.ts
@@@ -1,705 -1,0 +1,705 @@@
 +/*
 +* 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.
 +*/
 +
 +/*
 +* A third-party license is embeded for some of the code in this file:
 +* The treemap layout implementation was originally copied from
 +* "d3.js" with some modifications made for this project.
 +* (See more details in the comment of the method "squarify" below.)
 +* The use of the source code of this file is also subject to the terms
 +* and consitions of the license of "d3.js" (BSD-3Clause, see
 +* </licenses/LICENSE-d3>).
 +*/
 +
 +import * as zrUtil from 'zrender/src/core/util';
 +import BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect';
 +import {parsePercent, MAX_SAFE_INTEGER} from '../../util/number';
 +import * as layout from '../../util/layout';
 +import * as helper from '../helper/treeHelper';
 +import TreemapSeriesModel, { TreemapSeriesNodeItemOption } from './TreemapSeries';
 +import GlobalModel from '../../model/Global';
 +import ExtensionAPI from '../../ExtensionAPI';
 +import { TreeNode } from '../../data/Tree';
 +import Model from '../../model/Model';
 +import { TreemapRenderPayload, TreemapMovePayload, TreemapZoomToNodePayload } from './treemapAction';
 +
 +const mathMax = Math.max;
 +const mathMin = Math.min;
 +const retrieveValue = zrUtil.retrieve;
 +const each = zrUtil.each;
 +
 +const PATH_BORDER_WIDTH = ['itemStyle', 'borderWidth'] as const;
 +const PATH_GAP_WIDTH = ['itemStyle', 'gapWidth'] as const;
 +const PATH_UPPER_LABEL_SHOW = ['upperLabel', 'show'] as const;
 +const PATH_UPPER_LABEL_HEIGHT = ['upperLabel', 'height'] as const;
 +
 +export interface TreemapLayoutNode extends TreeNode {
 +    parentNode: TreemapLayoutNode
 +    children: TreemapLayoutNode[]
 +    viewChildren: TreemapLayoutNode[]
 +}
 +
 +export interface TreemapItemLayout extends RectLike {
 +    area: number
 +    isLeafRoot: boolean
 +    dataExtent: [number, number]
 +
 +    borderWidth: number
 +    upperHeight: number
 +    upperLabelHeight: number
 +
 +    isInView: boolean
 +    invisible: boolean
 +
 +    isAboveViewRoot: boolean
 +};
 +
 +type NodeModel = Model<TreemapSeriesNodeItemOption>;
 +
 +type OrderBy = 'asc' | 'desc' | boolean;
 +
 +type LayoutRow = TreemapLayoutNode[] & {
 +    area: number
 +};
 +/**
 + * @public
 + */
 +export default {
 +    seriesType: 'treemap',
 +    reset: function (
 +        seriesModel: TreemapSeriesModel,
 +        ecModel: GlobalModel,
 +        api: ExtensionAPI,
 +        payload?: TreemapZoomToNodePayload | TreemapRenderPayload | TreemapMovePayload
 +    ) {
 +        // Layout result in each node:
 +        // {x, y, width, height, area, borderWidth}
 +        const ecWidth = api.getWidth();
 +        const ecHeight = api.getHeight();
 +        const seriesOption = seriesModel.option;
 +
 +        const layoutInfo = layout.getLayoutRect(
 +            seriesModel.getBoxLayoutParams(),
 +            {
 +                width: api.getWidth(),
 +                height: api.getHeight()
 +            }
 +        );
 +
 +        const size = seriesOption.size || []; // Compatible with ec2.
 +        const containerWidth = parsePercent(
 +            retrieveValue(layoutInfo.width, size[0]),
 +            ecWidth
 +        );
 +        const containerHeight = parsePercent(
 +            retrieveValue(layoutInfo.height, size[1]),
 +            ecHeight
 +        );
 +
 +        // Fetch payload info.
 +        const payloadType = payload && payload.type;
 +        const types = ['treemapZoomToNode', 'treemapRootToNode'];
 +        const targetInfo = helper
 +            .retrieveTargetInfo(payload, types, seriesModel);
 +        const rootRect = (payloadType === 'treemapRender' || payloadType === 'treemapMove')
 +            ? payload.rootRect : null;
 +        const viewRoot = seriesModel.getViewRoot();
 +        const viewAbovePath = helper.getPathToRoot(viewRoot) as TreemapLayoutNode[];
 +
 +        if (payloadType !== 'treemapMove') {
 +            const rootSize = payloadType === 'treemapZoomToNode'
 +                ? estimateRootSize(
 +                    seriesModel, targetInfo, viewRoot, containerWidth, containerHeight
 +                )
 +                : rootRect
 +                ? [rootRect.width, rootRect.height]
 +                : [containerWidth, containerHeight];
 +
 +            let sort = seriesOption.sort;
 +            if (sort && sort !== 'asc' && sort !== 'desc') {
 +                // Default to be desc order.
 +                sort = 'desc';
 +            }
 +            const options = {
 +                squareRatio: seriesOption.squareRatio,
 +                sort: sort,
 +                leafDepth: seriesOption.leafDepth
 +            };
 +
 +            // layout should be cleared because using updateView but not update.
 +            viewRoot.hostTree.clearLayouts();
 +
 +            // TODO
 +            // optimize: if out of view clip, do not layout.
 +            // But take care that if do not render node out of view clip,
 +            // how to calculate start po
 +
 +            let viewRootLayout = {
 +                x: 0,
 +                y: 0,
 +                width: rootSize[0],
 +                height: rootSize[1],
 +                area: rootSize[0] * rootSize[1]
 +            };
 +            viewRoot.setLayout(viewRootLayout);
 +
 +            squarify(viewRoot, options, false, 0);
 +            // Supplement layout.
 +            viewRootLayout = viewRoot.getLayout();
 +            each(viewAbovePath, function (node, index) {
 +                const childValue = (viewAbovePath[index + 1] || viewRoot).getValue();
 +                node.setLayout(zrUtil.extend(
 +                    {
 +                        dataExtent: [childValue, childValue],
 +                        borderWidth: 0,
 +                        upperHeight: 0
 +                    },
 +                    viewRootLayout
 +                ));
 +            });
 +        }
 +
 +        const treeRoot = seriesModel.getData().tree.root;
 +
 +        treeRoot.setLayout(
 +            calculateRootPosition(layoutInfo, rootRect, targetInfo),
 +            true
 +        );
 +
 +        seriesModel.setLayoutInfo(layoutInfo);
 +
 +        // FIXME
 +        // 现在没有clip功能,暂时取ec高宽。
 +        prunning(
 +            treeRoot,
 +            // Transform to base element coordinate system.
 +            new BoundingRect(-layoutInfo.x, -layoutInfo.y, ecWidth, ecHeight),
 +            viewAbovePath,
 +            viewRoot,
 +            0
 +        );
 +    }
 +};
 +
 +/**
 + * Layout treemap with squarify algorithm.
 + * The original presentation of this algorithm
 + * was made by Mark Bruls, Kees Huizing, and Jarke J. van Wijk
 + * <https://graphics.ethz.ch/teaching/scivis_common/Literature/squarifiedTreeMaps.pdf>.
 + * The implementation of this algorithm was originally copied from "d3.js"
 + * <https://github.com/d3/d3/blob/9cc9a875e636a1dcf36cc1e07bdf77e1ad6e2c74/src/layout/treemap.js>
 + * with some modifications made for this program.
 + * See the license statement at the head of this file.
 + *
 + * @protected
 + * @param {module:echarts/data/Tree~TreeNode} node
 + * @param {Object} options
 + * @param {string} options.sort 'asc' or 'desc'
 + * @param {number} options.squareRatio
 + * @param {boolean} hideChildren
 + * @param {number} depth
 + */
 +function squarify(
 +    node: TreemapLayoutNode,
 +    options: {
 +        sort?: OrderBy
 +        squareRatio?: number
 +        leafDepth?: number
 +    },
 +    hideChildren: boolean,
 +    depth: number
 +) {
 +    let width;
 +    let height;
 +
 +    if (node.isRemoved()) {
 +        return;
 +    }
 +
 +    const thisLayout = node.getLayout();
 +    width = thisLayout.width;
 +    height = thisLayout.height;
 +
 +    // Considering border and gap
 +    const nodeModel = node.getModel<TreemapSeriesNodeItemOption>();
 +    const borderWidth = nodeModel.get(PATH_BORDER_WIDTH);
 +    const halfGapWidth = nodeModel.get(PATH_GAP_WIDTH) / 2;
 +    const upperLabelHeight = getUpperLabelHeight(nodeModel);
 +    const upperHeight = Math.max(borderWidth, upperLabelHeight);
 +    const layoutOffset = borderWidth - halfGapWidth;
 +    const layoutOffsetUpper = upperHeight - halfGapWidth;
 +
 +    node.setLayout({
 +        borderWidth: borderWidth,
 +        upperHeight: upperHeight,
 +        upperLabelHeight: upperLabelHeight
 +    }, true);
 +
 +    width = mathMax(width - 2 * layoutOffset, 0);
 +    height = mathMax(height - layoutOffset - layoutOffsetUpper, 0);
 +
 +    const totalArea = width * height;
 +    const viewChildren = initChildren(
 +        node, nodeModel, totalArea, options, hideChildren, depth
 +    );
 +
 +    if (!viewChildren.length) {
 +        return;
 +    }
 +
 +    const rect = {x: layoutOffset, y: layoutOffsetUpper, width: width, height: height};
 +    let rowFixedLength = mathMin(width, height);
 +    let best = Infinity; // the best row score so far
 +    const row = [] as LayoutRow;
 +    row.area = 0;
 +
 +    for (let i = 0, len = viewChildren.length; i < len;) {
 +        const child = viewChildren[i];
 +
 +        row.push(child);
 +        row.area += child.getLayout().area;
 +        const score = worst(row, rowFixedLength, options.squareRatio);
 +
 +        // continue with this orientation
 +        if (score <= best) {
 +            i++;
 +            best = score;
 +        }
 +        // abort, and try a different orientation
 +        else {
 +            row.area -= row.pop().getLayout().area;
 +            position(row, rowFixedLength, rect, halfGapWidth, false);
 +            rowFixedLength = mathMin(rect.width, rect.height);
 +            row.length = row.area = 0;
 +            best = Infinity;
 +        }
 +    }
 +
 +    if (row.length) {
 +        position(row, rowFixedLength, rect, halfGapWidth, true);
 +    }
 +
 +    if (!hideChildren) {
 +        const childrenVisibleMin = nodeModel.get('childrenVisibleMin');
 +        if (childrenVisibleMin != null && totalArea < childrenVisibleMin) {
 +            hideChildren = true;
 +        }
 +    }
 +
 +    for (let i = 0, len = viewChildren.length; i < len; i++) {
 +        squarify(viewChildren[i], options, hideChildren, depth + 1);
 +    }
 +}
 +
 +/**
 + * Set area to each child, and calculate data extent for visual coding.
 + */
 +function initChildren(
 +    node: TreemapLayoutNode,
 +    nodeModel: NodeModel,
 +    totalArea: number,
 +    options: {
 +        sort?: OrderBy
 +        leafDepth?: number
 +    },
 +    hideChildren: boolean,
 +    depth: number
 +) {
 +    let viewChildren = node.children || [];
 +    let orderBy = options.sort;
 +    orderBy !== 'asc' && orderBy !== 'desc' && (orderBy = null);
 +
 +    const overLeafDepth = options.leafDepth != null && options.leafDepth <= depth;
 +
 +    // leafDepth has higher priority.
 +    if (hideChildren && !overLeafDepth) {
 +        return (node.viewChildren = []);
 +    }
 +
 +    // Sort children, order by desc.
 +    viewChildren = zrUtil.filter(viewChildren, function (child) {
 +        return !child.isRemoved();
 +    });
 +
 +    sort(viewChildren, orderBy);
 +
 +    const info = statistic(nodeModel, viewChildren, orderBy);
 +
 +    if (info.sum === 0) {
 +        return (node.viewChildren = []);
 +    }
 +
 +    info.sum = filterByThreshold(nodeModel, totalArea, info.sum, orderBy, viewChildren);
 +
 +    if (info.sum === 0) {
 +        return (node.viewChildren = []);
 +    }
 +
 +    // Set area to each child.
 +    for (let i = 0, len = viewChildren.length; i < len; i++) {
 +        const area = viewChildren[i].getValue() as number / info.sum * totalArea;
 +        // Do not use setLayout({...}, true), because it is needed to clear last layout.
 +        viewChildren[i].setLayout({
 +            area: area
 +        });
 +    }
 +
 +    if (overLeafDepth) {
 +        viewChildren.length && node.setLayout({
 +            isLeafRoot: true
 +        }, true);
 +        viewChildren.length = 0;
 +    }
 +
 +    node.viewChildren = viewChildren;
 +    node.setLayout({
 +        dataExtent: info.dataExtent
 +    }, true);
 +
 +    return viewChildren;
 +}
 +
 +/**
 + * Consider 'visibleMin'. Modify viewChildren and get new sum.
 + */
 +function filterByThreshold(
 +    nodeModel: NodeModel,
 +    totalArea: number,
 +    sum: number,
 +    orderBy: OrderBy,
 +    orderedChildren: TreemapLayoutNode[]
 +) {
 +
 +    // visibleMin is not supported yet when no option.sort.
 +    if (!orderBy) {
 +        return sum;
 +    }
 +
 +    const visibleMin = nodeModel.get('visibleMin');
 +    const len = orderedChildren.length;
 +    let deletePoint = len;
 +
 +    // Always travel from little value to big value.
 +    for (let i = len - 1; i >= 0; i--) {
 +        const value = orderedChildren[
 +            orderBy === 'asc' ? len - i - 1 : i
 +        ].getValue() as number;
 +
 +        if (value / sum * totalArea < visibleMin) {
 +            deletePoint = i;
 +            sum -= value;
 +        }
 +    }
 +
 +    orderBy === 'asc'
 +        ? orderedChildren.splice(0, len - deletePoint)
 +        : orderedChildren.splice(deletePoint, len - deletePoint);
 +
 +    return sum;
 +}
 +
 +/**
 + * Sort
 + */
 +function sort(
 +    viewChildren: TreemapLayoutNode[],
 +    orderBy: OrderBy
 +) {
 +    if (orderBy) {
 +        viewChildren.sort(function (a, b) {
 +            const diff = orderBy === 'asc'
 +                ? a.getValue() as number - (b.getValue() as number)
 +                : b.getValue() as number - (a.getValue() as number);
 +            return diff === 0
 +                ? (orderBy === 'asc'
 +                    ? a.dataIndex - b.dataIndex : b.dataIndex - a.dataIndex
 +                )
 +                : diff;
 +        });
 +    }
 +    return viewChildren;
 +}
 +
 +/**
 + * Statistic
 + */
 +function statistic(
 +    nodeModel: NodeModel,
 +    children: TreemapLayoutNode[],
 +    orderBy: OrderBy
 +) {
 +    // Calculate sum.
 +    let sum = 0;
 +    for (let i = 0, len = children.length; i < len; i++) {
 +        sum += children[i].getValue() as number;
 +    }
 +
 +    // Statistic data extent for latter visual coding.
 +    // Notice: data extent should be calculate based on raw children
 +    // but not filtered view children, otherwise visual mapping will not
 +    // be stable when zoom (where children is filtered by visibleMin).
 +
 +    const dimension = nodeModel.get('visualDimension');
 +    let dataExtent: number[];
 +
 +    // The same as area dimension.
 +    if (!children || !children.length) {
 +        dataExtent = [NaN, NaN];
 +    }
 +    else if (dimension === 'value' && orderBy) {
 +        dataExtent = [
 +            children[children.length - 1].getValue() as number,
 +            children[0].getValue() as number
 +        ];
 +        orderBy === 'asc' && dataExtent.reverse();
 +    }
 +    // Other dimension.
 +    else {
 +        dataExtent = [Infinity, -Infinity];
 +        each(children, function (child) {
 +            const value = child.getValue(dimension) as number;
 +            value < dataExtent[0] && (dataExtent[0] = value);
 +            value > dataExtent[1] && (dataExtent[1] = value);
 +        });
 +    }
 +
 +    return {sum: sum, dataExtent: dataExtent};
 +}
 +
 +/**
 + * Computes the score for the specified row,
 + * as the worst aspect ratio.
 + */
 +function worst(row: LayoutRow, rowFixedLength: number, ratio: number) {
 +    let areaMax = 0;
 +    let areaMin = Infinity;
 +
 +    for (let i = 0, area, len = row.length; i < len; i++) {
 +        area = row[i].getLayout().area;
 +        if (area) {
 +            area < areaMin && (areaMin = area);
 +            area > areaMax && (areaMax = area);
 +        }
 +    }
 +
 +    const squareArea = row.area * row.area;
 +    const f = rowFixedLength * rowFixedLength * ratio;
 +
 +    return squareArea
 +        ? mathMax(
 +            (f * areaMax) / squareArea,
 +            squareArea / (f * areaMin)
 +        )
 +        : Infinity;
 +}
 +
 +/**
 + * Positions the specified row of nodes. Modifies `rect`.
 + */
 +function position(
 +    row: LayoutRow,
 +    rowFixedLength: number,
 +    rect: RectLike,
 +    halfGapWidth: number,
 +    flush?: boolean
 +) {
 +    // When rowFixedLength === rect.width,
 +    // it is horizontal subdivision,
 +    // rowFixedLength is the width of the subdivision,
 +    // rowOtherLength is the height of the subdivision,
 +    // and nodes will be positioned from left to right.
 +
 +    // wh[idx0WhenH] means: when horizontal,
 +    //      wh[idx0WhenH] => wh[0] => 'width'.
 +    //      xy[idx1WhenH] => xy[1] => 'y'.
 +    const idx0WhenH = rowFixedLength === rect.width ? 0 : 1;
 +    const idx1WhenH = 1 - idx0WhenH;
 +    const xy = ['x', 'y'] as const;
 +    const wh = ['width', 'height'] as const;
 +
 +    let last = rect[xy[idx0WhenH]];
 +    let rowOtherLength = rowFixedLength
 +        ? row.area / rowFixedLength : 0;
 +
 +    if (flush || rowOtherLength > rect[wh[idx1WhenH]]) {
 +        rowOtherLength = rect[wh[idx1WhenH]]; // over+underflow
 +    }
 +    for (let i = 0, rowLen = row.length; i < rowLen; i++) {
 +        const node = row[i];
 +        const nodeLayout = {} as TreemapItemLayout;
 +        const step = rowOtherLength
 +            ? node.getLayout().area / rowOtherLength : 0;
 +
 +        const wh1 = nodeLayout[wh[idx1WhenH]] = mathMax(rowOtherLength - 2 * halfGapWidth, 0);
 +
 +        // We use Math.max/min to avoid negative width/height when considering gap width.
 +        const remain = rect[xy[idx0WhenH]] + rect[wh[idx0WhenH]] - last;
 +        const modWH = (i === rowLen - 1 || remain < step) ? remain : step;
 +        const wh0 = nodeLayout[wh[idx0WhenH]] = mathMax(modWH - 2 * halfGapWidth, 0);
 +
 +        nodeLayout[xy[idx1WhenH]] = rect[xy[idx1WhenH]] + mathMin(halfGapWidth, wh1 / 2);
 +        nodeLayout[xy[idx0WhenH]] = last + mathMin(halfGapWidth, wh0 / 2);
 +
 +        last += modWH;
 +        node.setLayout(nodeLayout, true);
 +    }
 +
 +    rect[xy[idx1WhenH]] += rowOtherLength;
 +    rect[wh[idx1WhenH]] -= rowOtherLength;
 +}
 +
- // Return [containerWidth, containerHeight] as defualt.
++// Return [containerWidth, containerHeight] as default.
 +function estimateRootSize(
 +    seriesModel: TreemapSeriesModel,
 +    targetInfo: { node: TreemapLayoutNode },
 +    viewRoot: TreemapLayoutNode,
 +    containerWidth: number,
 +    containerHeight: number
 +) {
 +    // If targetInfo.node exists, we zoom to the node,
 +    // so estimate whold width and heigth by target node.
 +    let currNode = (targetInfo || {}).node;
 +    const defaultSize = [containerWidth, containerHeight];
 +
 +    if (!currNode || currNode === viewRoot) {
 +        return defaultSize;
 +    }
 +
 +    let parent;
 +    const viewArea = containerWidth * containerHeight;
 +    let area = viewArea * seriesModel.option.zoomToNodeRatio;
 +
 +    while (parent = currNode.parentNode) { // jshint ignore:line
 +        let sum = 0;
 +        const siblings = parent.children;
 +
 +        for (let i = 0, len = siblings.length; i < len; i++) {
 +            sum += siblings[i].getValue() as number;
 +        }
 +        const currNodeValue = currNode.getValue() as number;
 +        if (currNodeValue === 0) {
 +            return defaultSize;
 +        }
 +        area *= sum / currNodeValue;
 +
 +        // Considering border, suppose aspect ratio is 1.
 +        const parentModel = parent.getModel<TreemapSeriesNodeItemOption>();
 +        const borderWidth = parentModel.get(PATH_BORDER_WIDTH);
 +        const upperHeight = Math.max(borderWidth, getUpperLabelHeight(parentModel));
 +        area += 4 * borderWidth * borderWidth
 +            + (3 * borderWidth + upperHeight) * Math.pow(area, 0.5);
 +
 +        area > MAX_SAFE_INTEGER && (area = MAX_SAFE_INTEGER);
 +
 +        currNode = parent;
 +    }
 +
 +    area < viewArea && (area = viewArea);
 +    const scale = Math.pow(area / viewArea, 0.5);
 +
 +    return [containerWidth * scale, containerHeight * scale];
 +}
 +
 +// Root postion base on coord of containerGroup
 +function calculateRootPosition(
 +    layoutInfo: layout.LayoutRect,
 +    rootRect: RectLike,
 +    targetInfo: { node: TreemapLayoutNode }
 +) {
 +    if (rootRect) {
 +        return {x: rootRect.x, y: rootRect.y};
 +    }
 +
 +    const defaultPosition = {x: 0, y: 0};
 +    if (!targetInfo) {
 +        return defaultPosition;
 +    }
 +
 +    // If targetInfo is fetched by 'retrieveTargetInfo',
 +    // old tree and new tree are the same tree,
 +    // so the node still exists and we can visit it.
 +
 +    const targetNode = targetInfo.node;
 +    const layout = targetNode.getLayout();
 +
 +    if (!layout) {
 +        return defaultPosition;
 +    }
 +
 +    // Transform coord from local to container.
 +    const targetCenter = [layout.width / 2, layout.height / 2];
 +    let node = targetNode;
 +    while (node) {
 +        const nodeLayout = node.getLayout();
 +        targetCenter[0] += nodeLayout.x;
 +        targetCenter[1] += nodeLayout.y;
 +        node = node.parentNode;
 +    }
 +
 +    return {
 +        x: layoutInfo.width / 2 - targetCenter[0],
 +        y: layoutInfo.height / 2 - targetCenter[1]
 +    };
 +}
 +
 +// Mark nodes visible for prunning when visual coding and rendering.
 +// Prunning depends on layout and root position, so we have to do it after layout.
 +function prunning(
 +    node: TreemapLayoutNode,
 +    clipRect: BoundingRect,
 +    viewAbovePath: TreemapLayoutNode[],
 +    viewRoot: TreemapLayoutNode,
 +    depth: number
 +) {
 +    const nodeLayout = node.getLayout();
 +    const nodeInViewAbovePath = viewAbovePath[depth];
 +    const isAboveViewRoot = nodeInViewAbovePath && nodeInViewAbovePath === node;
 +
 +    if (
 +        (nodeInViewAbovePath && !isAboveViewRoot)
 +        || (depth === viewAbovePath.length && node !== viewRoot)
 +    ) {
 +        return;
 +    }
 +
 +    node.setLayout({
 +        // isInView means: viewRoot sub tree + viewAbovePath
 +        isInView: true,
 +        // invisible only means: outside view clip so that the node can not
 +        // see but still layout for animation preparation but not render.
 +        invisible: !isAboveViewRoot && !clipRect.intersect(nodeLayout),
 +        isAboveViewRoot
 +    }, true);
 +
 +    // Transform to child coordinate.
 +    const childClipRect = new BoundingRect(
 +        clipRect.x - nodeLayout.x,
 +        clipRect.y - nodeLayout.y,
 +        clipRect.width,
 +        clipRect.height
 +    );
 +
 +    each(node.viewChildren || [], function (child) {
 +        prunning(child, childClipRect, viewAbovePath, viewRoot, depth + 1);
 +    });
 +}
 +
 +function getUpperLabelHeight(model: NodeModel): number {
 +    return model.get(PATH_UPPER_LABEL_SHOW) ? model.get(PATH_UPPER_LABEL_HEIGHT) : 0;
- }
++}
diff --cc src/chart/treemap/treemapVisual.ts
index 0ff4b8e,0000000..2796338
mode 100644,000000..100644
--- a/src/chart/treemap/treemapVisual.ts
+++ b/src/chart/treemap/treemapVisual.ts
@@@ -1,287 -1,0 +1,270 @@@
 +/*
 +* 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 VisualMapping, { VisualMappingOption } from '../../visual/VisualMapping';
 +import { map, each, extend, isArray } from 'zrender/src/core/util';
 +import TreemapSeriesModel, { TreemapSeriesNodeItemOption, TreemapSeriesOption } from './TreemapSeries';
 +import { TreemapLayoutNode, TreemapItemLayout } from './treemapLayout';
 +import Model from '../../model/Model';
 +import { ColorString, ZRColor } from '../../util/types';
 +import { modifyHSL, modifyAlpha } from 'zrender/src/tool/color';
 +import { makeInner } from '../../util/model';
 +
 +type NodeModel = Model<TreemapSeriesNodeItemOption>;
 +type NodeItemStyleModel = Model<TreemapSeriesNodeItemOption['itemStyle']>;
 +
 +const ITEM_STYLE_NORMAL = 'itemStyle';
 +
 +const inner = makeInner<{
 +    drColorMappingBy: TreemapSeriesNodeItemOption['colorMappingBy']
 +}, VisualMapping>();
 +
 +interface TreemapVisual {
 +    color?: ZRColor
 +    colorAlpha?: number
 +    colorSaturation?: number
 +}
 +
 +type TreemapLevelItemStyleOption = TreemapSeriesOption['levels'][number]['itemStyle'];
 +
 +export default {
 +    seriesType: 'treemap',
 +    reset(seriesModel: TreemapSeriesModel) {
 +        const tree = seriesModel.getData().tree;
 +        const root = tree.root;
-         const seriesItemStyleModel = seriesModel.getModel(ITEM_STYLE_NORMAL);
 +
 +        if (root.isRemoved()) {
 +            return;
 +        }
 +
-         const levelItemStyles = map(tree.levelModels, function (levelModel) {
-             return levelModel ? levelModel.get(ITEM_STYLE_NORMAL) : null;
-         });
- 
 +        travelTree(
 +            root, // Visual should calculate from tree root but not view root.
 +            {},
-             levelItemStyles,
-             seriesItemStyleModel,
 +            seriesModel.getViewRoot().getAncestors(),
 +            seriesModel
 +        );
 +    }
 +};
 +
 +function travelTree(
 +    node: TreemapLayoutNode,
 +    designatedVisual: TreemapVisual,
-     levelItemStyles: TreemapLevelItemStyleOption[],
-     seriesItemStyleModel: Model<TreemapSeriesOption['itemStyle']>,
 +    viewRootAncestors: TreemapLayoutNode[],
 +    seriesModel: TreemapSeriesModel
 +) {
 +    const nodeModel = node.getModel<TreemapSeriesNodeItemOption>();
 +    const nodeLayout = node.getLayout();
 +    const data = node.hostTree.data;
 +
 +    // Optimize
 +    if (!nodeLayout || nodeLayout.invisible || !nodeLayout.isInView) {
 +        return;
 +    }
- 
 +    const nodeItemStyleModel = nodeModel.getModel(ITEM_STYLE_NORMAL);
-     const levelItemStyle = levelItemStyles[node.depth];
-     const visuals = buildVisuals(
-         nodeItemStyleModel, designatedVisual, levelItemStyle, seriesItemStyleModel
-     );
++    const visuals = buildVisuals(nodeItemStyleModel, designatedVisual, seriesModel);
 +
 +    const existsStyle = data.ensureUniqueItemVisual(node.dataIndex, 'style');
 +    // calculate border color
 +    let borderColor = nodeItemStyleModel.get('borderColor');
 +    const borderColorSaturation = nodeItemStyleModel.get('borderColorSaturation');
 +    let thisNodeColor;
 +    if (borderColorSaturation != null) {
 +        // For performance, do not always execute 'calculateColor'.
 +        thisNodeColor = calculateColor(visuals);
 +        borderColor = calculateBorderColor(borderColorSaturation, thisNodeColor);
 +    }
 +    existsStyle.stroke = borderColor;
 +
 +    const viewChildren = node.viewChildren;
 +    if (!viewChildren || !viewChildren.length) {
 +        thisNodeColor = calculateColor(visuals);
 +        // Apply visual to this node.
 +        existsStyle.fill = thisNodeColor;
 +    }
 +    else {
 +        const mapping = buildVisualMapping(
 +            node, nodeModel, nodeLayout, nodeItemStyleModel, visuals, viewChildren
 +        );
 +
 +        // Designate visual to children.
 +        each(viewChildren, function (child, index) {
 +            // If higher than viewRoot, only ancestors of viewRoot is needed to visit.
 +            if (child.depth >= viewRootAncestors.length
 +                || child === viewRootAncestors[child.depth]
 +            ) {
 +                const childVisual = mapVisual(
 +                    nodeModel, visuals, child, index, mapping, seriesModel
 +                );
-                 travelTree(
-                     child, childVisual, levelItemStyles, seriesItemStyleModel,
-                     viewRootAncestors, seriesModel
-                 );
++                travelTree(child, childVisual, viewRootAncestors, seriesModel);
 +            }
 +        });
 +    }
 +}
 +
 +function buildVisuals(
 +    nodeItemStyleModel: Model<TreemapSeriesNodeItemOption['itemStyle']>,
 +    designatedVisual: TreemapVisual,
-     levelItemStyle: TreemapLevelItemStyleOption,
-     seriesItemStyleModel: Model<TreemapSeriesOption['itemStyle']>
++    seriesModel: TreemapSeriesModel
 +) {
 +    const visuals = extend({}, designatedVisual);
++    const designatedVisualItemStyle = seriesModel.designatedVisualItemStyle;
 +
 +    each(['color', 'colorAlpha', 'colorSaturation'] as const, function (visualName) {
 +        // Priority: thisNode > thisLevel > parentNodeDesignated > seriesModel
-         let val = nodeItemStyleModel.get(visualName, true); // Ignore parent
-         val == null && levelItemStyle && (val = levelItemStyle[visualName]);
-         val == null && (val = designatedVisual[visualName]);
-         val == null && (val = seriesItemStyleModel.get(visualName));
++        (designatedVisualItemStyle as any)[visualName] = designatedVisual[visualName];
++        const val = nodeItemStyleModel.get(visualName);
++        designatedVisualItemStyle[visualName] = null;
 +
 +        val != null && ((visuals as any)[visualName] = val);
 +    });
 +
 +    return visuals;
 +}
 +
 +function calculateColor(visuals: TreemapVisual) {
 +    let color = getValueVisualDefine(visuals, 'color') as ColorString;
 +
 +    if (color) {
 +        const colorAlpha = getValueVisualDefine(visuals, 'colorAlpha') as number;
 +        const colorSaturation = getValueVisualDefine(visuals, 'colorSaturation') as number;
 +        if (colorSaturation) {
 +            color = modifyHSL(color, null, null, colorSaturation);
 +        }
 +        if (colorAlpha) {
 +            color = modifyAlpha(color, colorAlpha);
 +        }
 +
 +        return color;
 +    }
 +}
 +
 +function calculateBorderColor(
 +    borderColorSaturation: number,
 +    thisNodeColor: ColorString
 +) {
 +    return thisNodeColor != null
 +            // Can only be string
 +            ? modifyHSL(thisNodeColor, null, null, borderColorSaturation)
 +            : null;
 +}
 +
 +function getValueVisualDefine(visuals: TreemapVisual, name: keyof TreemapVisual) {
 +    const value = visuals[name];
 +    if (value != null && value !== 'none') {
 +        return value;
 +    }
 +}
 +
 +function buildVisualMapping(
 +    node: TreemapLayoutNode,
 +    nodeModel: NodeModel,
 +    nodeLayout: TreemapItemLayout,
 +    nodeItemStyleModel: NodeItemStyleModel,
 +    visuals: TreemapVisual,
 +    viewChildren: TreemapLayoutNode[]
 +) {
 +    if (!viewChildren || !viewChildren.length) {
 +        return;
 +    }
 +
 +    const rangeVisual = getRangeVisual(nodeModel, 'color')
 +        || (
 +            visuals.color != null
 +            && visuals.color !== 'none'
 +            && (
 +                getRangeVisual(nodeModel, 'colorAlpha')
 +                || getRangeVisual(nodeModel, 'colorSaturation')
 +            )
 +        );
 +
 +    if (!rangeVisual) {
 +        return;
 +    }
 +
 +    const visualMin = nodeModel.get('visualMin');
 +    const visualMax = nodeModel.get('visualMax');
 +    const dataExtent = nodeLayout.dataExtent.slice() as [number, number];
 +    visualMin != null && visualMin < dataExtent[0] && (dataExtent[0] = visualMin);
 +    visualMax != null && visualMax > dataExtent[1] && (dataExtent[1] = visualMax);
 +
 +    const colorMappingBy = nodeModel.get('colorMappingBy');
 +    const opt: VisualMappingOption = {
 +        type: rangeVisual.name,
 +        dataExtent: dataExtent,
 +        visual: rangeVisual.range
 +    };
 +    if (opt.type === 'color'
 +        && (colorMappingBy === 'index' || colorMappingBy === 'id')
 +    ) {
 +        opt.mappingMethod = 'category';
 +        opt.loop = true;
 +        // categories is ordinal, so do not set opt.categories.
 +    }
 +    else {
 +        opt.mappingMethod = 'linear';
 +    }
 +
 +    const mapping = new VisualMapping(opt);
 +    inner(mapping).drColorMappingBy = colorMappingBy;
 +
 +    return mapping;
 +}
 +
 +// Notice: If we dont have the attribute 'colorRange', but only use
 +// attribute 'color' to represent both concepts of 'colorRange' and 'color',
 +// (It means 'colorRange' when 'color' is Array, means 'color' when not array),
 +// this problem will be encountered:
 +// If a level-1 node dont have children, and its siblings has children,
 +// and colorRange is set on level-1, then the node can not be colored.
 +// So we separate 'colorRange' and 'color' to different attributes.
 +function getRangeVisual(nodeModel: NodeModel, name: keyof TreemapVisual) {
 +    // 'colorRange', 'colorARange', 'colorSRange'.
 +    // If not exsits on this node, fetch from levels and series.
 +    const range = nodeModel.get(name);
 +    return (isArray(range) && range.length) ? {
 +        name: name,
 +        range: range
 +    } : null;
 +}
 +
 +function mapVisual(
 +    nodeModel: NodeModel,
 +    visuals: TreemapVisual,
 +    child: TreemapLayoutNode,
 +    index: number,
 +    mapping: VisualMapping,
 +    seriesModel: TreemapSeriesModel
 +) {
 +    const childVisuals = extend({}, visuals);
 +
 +    if (mapping) {
 +        // Only support color, colorAlpha, colorSaturation.
 +        const mappingType = mapping.type as keyof TreemapVisual;
 +        const colorMappingBy = mappingType === 'color' && inner(mapping).drColorMappingBy;
 +        const value = colorMappingBy === 'index'
 +            ? index
 +            : colorMappingBy === 'id'
 +            ? seriesModel.mapIdToIndex(child.getId())
 +            : child.getValue(nodeModel.get('visualDimension'));
 +
 +        (childVisuals as any)[mappingType] = mapping.mapValueToVisual(value);
 +    }
 +
 +    return childVisuals;
 +}
diff --cc src/component/axisPointer/axisTrigger.ts
index 0bc672f,0000000..27aa8c5
mode 100644,000000..100644
--- a/src/component/axisPointer/axisTrigger.ts
+++ b/src/component/axisPointer/axisTrigger.ts
@@@ -1,529 -1,0 +1,529 @@@
 +/*
 +* 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 {makeInner, ModelFinderObject} from '../../util/model';
 +import * as modelHelper from './modelHelper';
 +import findPointFromSeries from './findPointFromSeries';
 +import GlobalModel from '../../model/Global';
 +import ExtensionAPI from '../../ExtensionAPI';
 +import { Dictionary, Payload, CommonAxisPointerOption, HighlightPayload, DownplayPayload } from '../../util/types';
 +import AxisPointerModel, { AxisPointerOption } from './AxisPointerModel';
 +import { each, curry, bind, extend, Curry1 } from 'zrender/src/core/util';
 +import { ZRenderType } from 'zrender/src/zrender';
 +
 +const inner = makeInner<{
 +    axisPointerLastHighlights: Dictionary<BatchItem>
 +}, ZRenderType>();
 +
 +type AxisValue = CommonAxisPointerOption['value'];
 +
 +interface DataIndex {
 +    seriesIndex: number
 +    dataIndex: number
 +    dataIndexInside: number
 +}
 +
 +type BatchItem = DataIndex;
 +
 +export interface DataByAxis {
 +    // TODO: TYPE Value type
 +    value: string | number
 +    axisIndex: number
 +    axisDim: string
 +    axisType: string
 +    axisId: string
 +
 +    seriesDataIndices: DataIndex[]
 +
 +    valueLabelOpt: {
 +        precision: AxisPointerOption['label']['precision']
 +        formatter: AxisPointerOption['label']['formatter']
 +    }
 +}
 +export interface DataByCoordSys {
 +    coordSysId: string
 +    coordSysIndex: number
 +    coordSysType: string
 +    coordSysMainType: string
 +    dataByAxis: DataByAxis[]
 +}
 +interface DataByCoordSysCollection {
 +    list: DataByCoordSys[]
 +    map: Dictionary<DataByCoordSys>
 +}
 +
 +type CollectedCoordInfo = ReturnType<typeof modelHelper['collect']>;
 +type CollectedAxisInfo = CollectedCoordInfo['axesInfo'][string];
 +
 +interface AxisTriggerPayload extends Payload {
 +    currTrigger?: 'click' | 'mousemove' | 'leave'
 +    /**
 +     * x and y, which are mandatory, specify a point to trigger axisPointer and tooltip.
 +     */
 +    x?: number
 +    /**
 +     * x and y, which are mandatory, specify a point to trigger axisPointer and tooltip.
 +     */
 +    y?: number
 +    /**
 +     * finder, optional, restrict target axes.
 +     */
 +    seriesIndex?: number
 +    dataIndex: number
 +
 +    axesInfo?: {
 +        // 'x'|'y'|'angle'
 +        axisDim?: string
 +        axisIndex?: number
 +        value?: AxisValue
 +    }[]
 +
 +    dispatchAction: ExtensionAPI['dispatchAction']
 +}
 +
 +type ShowValueMap = Dictionary<{
 +    value: AxisValue
 +    payloadBatch: BatchItem[]
 +}>;
 +
 +/**
 + * Basic logic: check all axis, if they do not demand show/highlight,
 + * then hide/downplay them.
 + *
 + * @return content of event obj for echarts.connect.
 + */
 +export default function (
 +    payload: AxisTriggerPayload,
 +    ecModel: GlobalModel,
 +    api: ExtensionAPI
 +) {
 +    const currTrigger = payload.currTrigger;
 +    let point = [payload.x, payload.y];
 +    const finder = payload;
 +    const dispatchAction = payload.dispatchAction || bind(api.dispatchAction, api);
 +    const coordSysAxesInfo = (ecModel.getComponent('axisPointer') as AxisPointerModel)
 +        .coordSysAxesInfo as CollectedCoordInfo;
 +
 +    // Pending
 +    // See #6121. But we are not able to reproduce it yet.
 +    if (!coordSysAxesInfo) {
 +        return;
 +    }
 +
 +    if (illegalPoint(point)) {
 +        // Used in the default behavior of `connection`: use the sample seriesIndex
 +        // and dataIndex. And also used in the tooltipView trigger.
 +        point = findPointFromSeries({
 +            seriesIndex: finder.seriesIndex,
 +            // Do not use dataIndexInside from other ec instance.
 +            // FIXME: auto detect it?
 +            dataIndex: finder.dataIndex
 +        }, ecModel).point;
 +    }
 +    const isIllegalPoint = illegalPoint(point);
 +
 +    // Axis and value can be specified when calling dispatchAction({type: 'updateAxisPointer'}).
 +    // Notice: In this case, it is difficult to get the `point` (which is necessary to show
 +    // tooltip, so if point is not given, we just use the point found by sample seriesIndex
 +    // and dataIndex.
 +    const inputAxesInfo = finder.axesInfo;
 +
 +    const axesInfo = coordSysAxesInfo.axesInfo;
 +    const shouldHide = currTrigger === 'leave' || illegalPoint(point);
 +    const outputPayload = {} as AxisTriggerPayload;
 +
 +    const showValueMap: ShowValueMap = {};
 +    const dataByCoordSys: DataByCoordSysCollection = {
 +        list: [],
 +        map: {}
 +    };
 +    const updaters = {
 +        showPointer: curry(showPointer, showValueMap),
 +        showTooltip: curry(showTooltip, dataByCoordSys)
 +    };
 +
 +    // Process for triggered axes.
 +    each(coordSysAxesInfo.coordSysMap, function (coordSys, coordSysKey) {
 +        // If a point given, it must be contained by the coordinate system.
 +        const coordSysContainsPoint = isIllegalPoint || coordSys.containPoint(point);
 +
 +        each(coordSysAxesInfo.coordSysAxesInfo[coordSysKey], function (axisInfo, key) {
 +            const axis = axisInfo.axis;
 +            const inputAxisInfo = findInputAxisInfo(inputAxesInfo, axisInfo);
 +            // If no inputAxesInfo, no axis is restricted.
 +            if (!shouldHide && coordSysContainsPoint && (!inputAxesInfo || inputAxisInfo)) {
 +                let val = inputAxisInfo && inputAxisInfo.value;
 +                if (val == null && !isIllegalPoint) {
 +                    val = axis.pointToData(point);
 +                }
 +                val != null && processOnAxis(axisInfo, val, updaters, false, outputPayload);
 +            }
 +        });
 +    });
 +
 +    // Process for linked axes.
 +    const linkTriggers: Dictionary<AxisValue> = {};
 +    each(axesInfo, function (tarAxisInfo, tarKey) {
 +        const linkGroup = tarAxisInfo.linkGroup;
 +
 +        // If axis has been triggered in the previous stage, it should not be triggered by link.
 +        if (linkGroup && !showValueMap[tarKey]) {
 +            each(linkGroup.axesInfo, function (srcAxisInfo, srcKey) {
 +                const srcValItem = showValueMap[srcKey];
 +                // If srcValItem exist, source axis is triggered, so link to target axis.
 +                if (srcAxisInfo !== tarAxisInfo && srcValItem) {
 +                    let val = srcValItem.value;
 +                    linkGroup.mapper && (val = tarAxisInfo.axis.scale.parse(linkGroup.mapper(
 +                        val, makeMapperParam(srcAxisInfo), makeMapperParam(tarAxisInfo)
 +                    )));
 +                    linkTriggers[tarAxisInfo.key] = val;
 +                }
 +            });
 +        }
 +    });
 +    each(linkTriggers, function (val, tarKey) {
 +        processOnAxis(axesInfo[tarKey], val, updaters, true, outputPayload);
 +    });
 +
 +    updateModelActually(showValueMap, axesInfo, outputPayload);
 +    dispatchTooltipActually(dataByCoordSys, point, payload, dispatchAction);
 +    dispatchHighDownActually(axesInfo, dispatchAction, api);
 +
 +    return outputPayload;
 +}
 +
 +function processOnAxis(
 +    axisInfo: CollectedCoordInfo['axesInfo'][string],
 +    newValue: AxisValue,
 +    updaters: {
 +        showPointer: Curry1<typeof showPointer, ShowValueMap>
 +        showTooltip: Curry1<typeof showTooltip, DataByCoordSysCollection>
 +    },
 +    noSnap: boolean,
 +    outputFinder: ModelFinderObject
 +) {
 +    const axis = axisInfo.axis;
 +
 +    if (axis.scale.isBlank() || !axis.containData(newValue)) {
 +        return;
 +    }
 +
 +    if (!axisInfo.involveSeries) {
 +        updaters.showPointer(axisInfo, newValue);
 +        return;
 +    }
 +
 +    // Heavy calculation. So put it after axis.containData checking.
 +    const payloadInfo = buildPayloadsBySeries(newValue, axisInfo);
 +    const payloadBatch = payloadInfo.payloadBatch;
 +    const snapToValue = payloadInfo.snapToValue;
 +
 +    // Fill content of event obj for echarts.connect.
-     // By defualt use the first involved series data as a sample to connect.
++    // By default use the first involved series data as a sample to connect.
 +    if (payloadBatch[0] && outputFinder.seriesIndex == null) {
 +        extend(outputFinder, payloadBatch[0]);
 +    }
 +
 +    // If no linkSource input, this process is for collecting link
 +    // target, where snap should not be accepted.
 +    if (!noSnap && axisInfo.snap) {
 +        if (axis.containData(snapToValue) && snapToValue != null) {
 +            newValue = snapToValue;
 +        }
 +    }
 +
 +    updaters.showPointer(axisInfo, newValue, payloadBatch);
 +    // Tooltip should always be snapToValue, otherwise there will be
 +    // incorrect "axis value ~ series value" mapping displayed in tooltip.
 +    updaters.showTooltip(axisInfo, payloadInfo, snapToValue);
 +}
 +
 +function buildPayloadsBySeries(value: AxisValue, axisInfo: CollectedAxisInfo) {
 +    const axis = axisInfo.axis;
 +    const dim = axis.dim;
 +    let snapToValue = value;
 +    const payloadBatch: BatchItem[] = [];
 +    let minDist = Number.MAX_VALUE;
 +    let minDiff = -1;
 +
 +    each(axisInfo.seriesModels, function (series, idx) {
 +        const dataDim = series.getData().mapDimensionsAll(dim);
 +        let seriesNestestValue;
 +        let dataIndices;
 +
 +        if (series.getAxisTooltipData) {
 +            const result = series.getAxisTooltipData(dataDim, value, axis);
 +            dataIndices = result.dataIndices;
 +            seriesNestestValue = result.nestestValue;
 +        }
 +        else {
 +            dataIndices = series.getData().indicesOfNearest(
 +                dataDim[0],
 +                value as number,
 +                // Add a threshold to avoid find the wrong dataIndex
 +                // when data length is not same.
 +                // false,
 +                axis.type === 'category' ? 0.5 : null
 +            );
 +            if (!dataIndices.length) {
 +                return;
 +            }
 +            seriesNestestValue = series.getData().get(dataDim[0], dataIndices[0]);
 +        }
 +
 +        if (seriesNestestValue == null || !isFinite(seriesNestestValue)) {
 +            return;
 +        }
 +
 +        const diff = value as number - seriesNestestValue;
 +        const dist = Math.abs(diff);
 +        // Consider category case
 +        if (dist <= minDist) {
 +            if (dist < minDist || (diff >= 0 && minDiff < 0)) {
 +                minDist = dist;
 +                minDiff = diff;
 +                snapToValue = seriesNestestValue;
 +                payloadBatch.length = 0;
 +            }
 +            each(dataIndices, function (dataIndex) {
 +                payloadBatch.push({
 +                    seriesIndex: series.seriesIndex,
 +                    dataIndexInside: dataIndex,
 +                    dataIndex: series.getData().getRawIndex(dataIndex)
 +                });
 +            });
 +        }
 +    });
 +
 +    return {
 +        payloadBatch: payloadBatch,
 +        snapToValue: snapToValue
 +    };
 +}
 +
 +function showPointer(
 +    showValueMap: ShowValueMap,
 +    axisInfo: CollectedAxisInfo,
 +    value: AxisValue,
 +    payloadBatch?: BatchItem[]
 +) {
 +    showValueMap[axisInfo.key] = {
 +        value: value,
 +        payloadBatch: payloadBatch
 +    };
 +}
 +
 +function showTooltip(
 +    dataByCoordSys: DataByCoordSysCollection,
 +    axisInfo: CollectedCoordInfo['axesInfo'][string],
 +    payloadInfo: { payloadBatch: BatchItem[] },
 +    value: AxisValue
 +) {
 +    const payloadBatch = payloadInfo.payloadBatch;
 +    const axis = axisInfo.axis;
 +    const axisModel = axis.model;
 +    const axisPointerModel = axisInfo.axisPointerModel;
 +
 +    // If no data, do not create anything in dataByCoordSys,
 +    // whose length will be used to judge whether dispatch action.
 +    if (!axisInfo.triggerTooltip || !payloadBatch.length) {
 +        return;
 +    }
 +
 +    const coordSysModel = axisInfo.coordSys.model;
 +    const coordSysKey = modelHelper.makeKey(coordSysModel);
 +    let coordSysItem = dataByCoordSys.map[coordSysKey];
 +    if (!coordSysItem) {
 +        coordSysItem = dataByCoordSys.map[coordSysKey] = {
 +            coordSysId: coordSysModel.id,
 +            coordSysIndex: coordSysModel.componentIndex,
 +            coordSysType: coordSysModel.type,
 +            coordSysMainType: coordSysModel.mainType,
 +            dataByAxis: []
 +        };
 +        dataByCoordSys.list.push(coordSysItem);
 +    }
 +
 +    coordSysItem.dataByAxis.push({
 +        axisDim: axis.dim,
 +        axisIndex: axisModel.componentIndex,
 +        axisType: axisModel.type,
 +        axisId: axisModel.id,
 +        value: value as number,
 +        // Caustion: viewHelper.getValueLabel is actually on "view stage", which
 +        // depends that all models have been updated. So it should not be performed
 +        // here. Considering axisPointerModel used here is volatile, which is hard
 +        // to be retrieve in TooltipView, we prepare parameters here.
 +        valueLabelOpt: {
 +            precision: axisPointerModel.get(['label', 'precision']),
 +            formatter: axisPointerModel.get(['label', 'formatter'])
 +        },
 +        seriesDataIndices: payloadBatch.slice()
 +    });
 +}
 +
 +function updateModelActually(
 +    showValueMap: ShowValueMap,
 +    axesInfo: Dictionary<CollectedAxisInfo>,
 +    outputPayload: AxisTriggerPayload
 +) {
 +    const outputAxesInfo: AxisTriggerPayload['axesInfo'] = outputPayload.axesInfo = [];
 +    // Basic logic: If no 'show' required, 'hide' this axisPointer.
 +    each(axesInfo, function (axisInfo, key) {
 +        const option = axisInfo.axisPointerModel.option;
 +        const valItem = showValueMap[key];
 +
 +        if (valItem) {
 +            !axisInfo.useHandle && (option.status = 'show');
 +            option.value = valItem.value;
 +            // For label formatter param and highlight.
 +            option.seriesDataIndices = (valItem.payloadBatch || []).slice();
 +        }
 +        // When always show (e.g., handle used), remain
 +        // original value and status.
 +        else {
 +            // If hide, value still need to be set, consider
 +            // click legend to toggle axis blank.
 +            !axisInfo.useHandle && (option.status = 'hide');
 +        }
 +
 +        // If status is 'hide', should be no info in payload.
 +        option.status === 'show' && outputAxesInfo.push({
 +            axisDim: axisInfo.axis.dim,
 +            axisIndex: axisInfo.axis.model.componentIndex,
 +            value: option.value
 +        });
 +    });
 +}
 +
 +function dispatchTooltipActually(
 +    dataByCoordSys: DataByCoordSysCollection,
 +    point: number[],
 +    payload: AxisTriggerPayload,
 +    dispatchAction: ExtensionAPI['dispatchAction']
 +) {
 +    // Basic logic: If no showTip required, hideTip will be dispatched.
 +    if (illegalPoint(point) || !dataByCoordSys.list.length) {
 +        dispatchAction({type: 'hideTip'});
 +        return;
 +    }
 +
 +    // In most case only one axis (or event one series is used). It is
 +    // convinient to fetch payload.seriesIndex and payload.dataIndex
 +    // dirtectly. So put the first seriesIndex and dataIndex of the first
 +    // axis on the payload.
 +    const sampleItem = ((dataByCoordSys.list[0].dataByAxis[0] || {}).seriesDataIndices || [])[0] || {} as DataIndex;
 +
 +    dispatchAction({
 +        type: 'showTip',
 +        escapeConnect: true,
 +        x: point[0],
 +        y: point[1],
 +        tooltipOption: payload.tooltipOption,
 +        position: payload.position,
 +        dataIndexInside: sampleItem.dataIndexInside,
 +        dataIndex: sampleItem.dataIndex,
 +        seriesIndex: sampleItem.seriesIndex,
 +        dataByCoordSys: dataByCoordSys.list
 +    });
 +}
 +
 +function dispatchHighDownActually(
 +    axesInfo: Dictionary<CollectedAxisInfo>,
 +    dispatchAction: ExtensionAPI['dispatchAction'],
 +    api: ExtensionAPI
 +) {
 +    // FIXME
 +    // highlight status modification shoule be a stage of main process?
 +    // (Consider confilct (e.g., legend and axisPointer) and setOption)
 +
 +    const zr = api.getZr();
 +    const highDownKey = 'axisPointerLastHighlights' as const;
 +    const lastHighlights = inner(zr)[highDownKey] || {};
 +    const newHighlights: Dictionary<BatchItem> = inner(zr)[highDownKey] = {};
 +
 +    // Update highlight/downplay status according to axisPointer model.
 +    // Build hash map and remove duplicate incidentally.
 +    each(axesInfo, function (axisInfo, key) {
 +        const option = axisInfo.axisPointerModel.option;
 +        option.status === 'show' && each(option.seriesDataIndices, function (batchItem) {
 +            const key = batchItem.seriesIndex + ' | ' + batchItem.dataIndex;
 +            newHighlights[key] = batchItem;
 +        });
 +    });
 +
 +    // Diff.
 +    const toHighlight: BatchItem[] = [];
 +    const toDownplay: BatchItem[] = [];
 +    each(lastHighlights, function (batchItem, key) {
 +        !newHighlights[key] && toDownplay.push(batchItem);
 +    });
 +    each(newHighlights, function (batchItem, key) {
 +        !lastHighlights[key] && toHighlight.push(batchItem);
 +    });
 +
 +    toDownplay.length && api.dispatchAction({
 +        type: 'downplay',
 +        escapeConnect: true,
 +        // Not blur others when highlight in axisPointer.
 +        notBlur: true,
 +        batch: toDownplay
 +    } as DownplayPayload);
 +    toHighlight.length && api.dispatchAction({
 +        type: 'highlight',
 +        escapeConnect: true,
 +        // Not blur others when highlight in axisPointer.
 +        notBlur: true,
 +        batch: toHighlight
 +    } as HighlightPayload);
 +}
 +
 +function findInputAxisInfo(
 +    inputAxesInfo: AxisTriggerPayload['axesInfo'],
 +    axisInfo: CollectedAxisInfo
 +) {
 +    for (let i = 0; i < (inputAxesInfo || []).length; i++) {
 +        const inputAxisInfo = inputAxesInfo[i];
 +        if (axisInfo.axis.dim === inputAxisInfo.axisDim
 +            && axisInfo.axis.model.componentIndex === inputAxisInfo.axisIndex
 +        ) {
 +            return inputAxisInfo;
 +        }
 +    }
 +}
 +
 +function makeMapperParam(axisInfo: CollectedAxisInfo) {
 +    const axisModel = axisInfo.axis.model;
 +    const item = {} as {
 +        axisDim: string
 +        axisIndex: number
 +        axisId: string
 +        axisName: string
 +        // TODO `dim`AxisIndex, `dim`AxisName, `dim`AxisId?
 +    };
 +    const dim = item.axisDim = axisInfo.axis.dim;
 +    item.axisIndex = (item as any)[dim + 'AxisIndex'] = axisModel.componentIndex;
 +    item.axisName = (item as any)[dim + 'AxisName'] = axisModel.name;
 +    item.axisId = (item as any)[dim + 'AxisId'] = axisModel.id;
 +    return item;
 +}
 +
 +function illegalPoint(point?: number[]) {
 +    return !point || point[0] == null || isNaN(point[0]) || point[1] == null || isNaN(point[1]);
 +}
diff --cc src/component/legend/ScrollableLegendView.ts
index 3a90998,0000000..0345b87
mode 100644,000000..100644
--- a/src/component/legend/ScrollableLegendView.ts
+++ b/src/component/legend/ScrollableLegendView.ts
@@@ -1,553 -1,0 +1,553 @@@
 +/*
 +* 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.
 +*/
 +
 +/**
 + * Separate legend and scrollable legend to reduce package size.
 + */
 +
 +import * as zrUtil from 'zrender/src/core/util';
 +import * as graphic from '../../util/graphic';
 +import * as layoutUtil from '../../util/layout';
 +import LegendView from './LegendView';
 +import { LegendSelectorButtonOption } from './LegendModel';
 +import ExtensionAPI from '../../ExtensionAPI';
 +import GlobalModel from '../../model/Global';
 +import ScrollableLegendModel, {ScrollableLegendOption} from './ScrollableLegendModel';
 +import Displayable from 'zrender/src/graphic/Displayable';
 +import ComponentView from '../../view/Component';
 +import Element from 'zrender/src/Element';
 +import { ZRRectLike } from '../../util/types';
 +
 +const Group = graphic.Group;
 +
 +const WH = ['width', 'height'] as const;
 +const XY = ['x', 'y'] as const;
 +
 +interface PageInfo {
 +    contentPosition: number[]
 +    pageCount: number
 +    pageIndex: number
 +    pagePrevDataIndex: number
 +    pageNextDataIndex: number
 +}
 +
 +interface ItemInfo {
 +    /**
 +     * Start
 +     */
 +    s: number
 +    /**
 +     * End
 +     */
 +    e: number
 +    /**
 +     * Index
 +     */
 +    i: number
 +}
 +
 +type LegendGroup = graphic.Group & {
 +    __rectSize: number
 +};
 +
 +type LegendItemElement = Element & {
 +    __legendDataIndex: number
 +};
 +
 +class ScrollableLegendView extends LegendView {
 +
 +    static type = 'legend.scroll' as const;
 +    type = ScrollableLegendView.type;
 +
 +    newlineDisabled = true;
 +
 +    private _containerGroup: LegendGroup;
 +    private _controllerGroup: graphic.Group;
 +
 +    private _currentIndex: number = 0;
 +
 +    private _showController: boolean;
 +
 +    init() {
 +
 +        super.init();
 +
 +        this.group.add(this._containerGroup = new Group() as LegendGroup);
 +        this._containerGroup.add(this.getContentGroup());
 +
 +        this.group.add(this._controllerGroup = new Group());
 +    }
 +
 +    /**
 +     * @override
 +     */
 +    resetInner() {
 +        super.resetInner();
 +
 +        this._controllerGroup.removeAll();
 +        this._containerGroup.removeClipPath();
 +        this._containerGroup.__rectSize = null;
 +    }
 +
 +    /**
 +     * @override
 +     */
 +    renderInner(
 +        itemAlign: ScrollableLegendOption['align'],
 +        legendModel: ScrollableLegendModel,
 +        ecModel: GlobalModel,
 +        api: ExtensionAPI,
 +        selector: LegendSelectorButtonOption[],
 +        orient: ScrollableLegendOption['orient'],
 +        selectorPosition: ScrollableLegendOption['selectorPosition']
 +    ) {
 +        const self = this;
 +
 +        // Render content items.
 +        super.renderInner(itemAlign, legendModel, ecModel, api, selector, orient, selectorPosition);
 +
 +        const controllerGroup = this._controllerGroup;
 +
 +        // FIXME: support be 'auto' adapt to size number text length,
 +        // e.g., '3/12345' should not overlap with the control arrow button.
 +        const pageIconSize = legendModel.get('pageIconSize', true);
 +        const pageIconSizeArr: number[] = zrUtil.isArray(pageIconSize)
 +            ? pageIconSize : [pageIconSize, pageIconSize];
 +
 +        createPageButton('pagePrev', 0);
 +
 +        const pageTextStyleModel = legendModel.getModel('pageTextStyle');
 +        controllerGroup.add(new graphic.Text({
 +            name: 'pageText',
 +            style: {
 +                // Placeholder to calculate a proper layout.
 +                text: 'xx/xx',
 +                fill: pageTextStyleModel.getTextColor(),
 +                font: pageTextStyleModel.getFont(),
 +                verticalAlign: 'middle',
 +                align: 'center'
 +            },
 +            silent: true
 +        }));
 +
 +        createPageButton('pageNext', 1);
 +
 +        function createPageButton(name: string, iconIdx: number) {
 +            const pageDataIndexName = (name + 'DataIndex') as 'pagePrevDataIndex' | 'pageNextDataIndex';
 +            const icon = graphic.createIcon(
 +                legendModel.get('pageIcons', true)[legendModel.getOrient().name][iconIdx],
 +                {
 +                    // Buttons will be created in each render, so we do not need
 +                    // to worry about avoiding using legendModel kept in scope.
 +                    onclick: zrUtil.bind(
 +                        self._pageGo, self, pageDataIndexName, legendModel, api
 +                    )
 +                },
 +                {
 +                    x: -pageIconSizeArr[0] / 2,
 +                    y: -pageIconSizeArr[1] / 2,
 +                    width: pageIconSizeArr[0],
 +                    height: pageIconSizeArr[1]
 +                }
 +            );
 +            icon.name = name;
 +            controllerGroup.add(icon);
 +        }
 +    }
 +
 +    /**
 +     * @override
 +     */
 +    layoutInner(
 +        legendModel: ScrollableLegendModel,
 +        itemAlign: ScrollableLegendOption['align'],
 +        maxSize: { width: number, height: number },
 +        isFirstRender: boolean,
 +        selector: LegendSelectorButtonOption[],
 +        selectorPosition: ScrollableLegendOption['selectorPosition']
 +    ) {
 +        const selectorGroup = this.getSelectorGroup();
 +
 +        const orientIdx = legendModel.getOrient().index;
 +        const wh = WH[orientIdx];
 +        const xy = XY[orientIdx];
 +        const hw = WH[1 - orientIdx];
 +        const yx = XY[1 - orientIdx];
 +
 +        selector && layoutUtil.box(
 +            // Buttons in selectorGroup always layout horizontally
 +            'horizontal',
 +            selectorGroup,
 +            legendModel.get('selectorItemGap', true)
 +        );
 +
 +        const selectorButtonGap = legendModel.get('selectorButtonGap', true);
 +        const selectorRect = selectorGroup.getBoundingRect();
 +        const selectorPos = [-selectorRect.x, -selectorRect.y];
 +
 +        const processMaxSize = zrUtil.clone(maxSize);
 +        selector && (processMaxSize[wh] = maxSize[wh] - selectorRect[wh] - selectorButtonGap);
 +
 +        const mainRect = this._layoutContentAndController(legendModel, isFirstRender,
 +            processMaxSize, orientIdx, wh, hw, yx
 +        );
 +
 +        if (selector) {
 +            if (selectorPosition === 'end') {
 +                selectorPos[orientIdx] += mainRect[wh] + selectorButtonGap;
 +            }
 +            else {
 +                const offset = selectorRect[wh] + selectorButtonGap;
 +                selectorPos[orientIdx] -= offset;
 +                mainRect[xy] -= offset;
 +            }
 +            mainRect[wh] += selectorRect[wh] + selectorButtonGap;
 +
 +            selectorPos[1 - orientIdx] += mainRect[yx] + mainRect[hw] / 2 - selectorRect[hw] / 2;
 +            mainRect[hw] = Math.max(mainRect[hw], selectorRect[hw]);
 +            mainRect[yx] = Math.min(mainRect[yx], selectorRect[yx] + selectorPos[1 - orientIdx]);
 +
 +            selectorGroup.x = selectorPos[0];
 +            selectorGroup.y = selectorPos[1];
 +            selectorGroup.markRedraw();
 +        }
 +
 +        return mainRect;
 +    }
 +
 +    _layoutContentAndController(
 +        legendModel: ScrollableLegendModel,
 +        isFirstRender: boolean,
 +        maxSize: { width: number, height: number },
 +        orientIdx: 0 | 1,
 +        wh: 'width' | 'height',
 +        hw: 'width' | 'height',
 +        yx: 'x' | 'y'
 +    ) {
 +        const contentGroup = this.getContentGroup();
 +        const containerGroup = this._containerGroup;
 +        const controllerGroup = this._controllerGroup;
 +
 +        // Place items in contentGroup.
 +        layoutUtil.box(
 +            legendModel.get('orient'),
 +            contentGroup,
 +            legendModel.get('itemGap'),
 +            !orientIdx ? null : maxSize.width,
 +            orientIdx ? null : maxSize.height
 +        );
 +
 +        layoutUtil.box(
 +            // Buttons in controller are layout always horizontally.
 +            'horizontal',
 +            controllerGroup,
 +            legendModel.get('pageButtonItemGap', true)
 +        );
 +
 +        const contentRect = contentGroup.getBoundingRect();
 +        const controllerRect = controllerGroup.getBoundingRect();
 +        const showController = this._showController = contentRect[wh] > maxSize[wh];
 +
 +        const contentPos = [-contentRect.x, -contentRect.y];
 +        // Remain contentPos when scroll animation perfroming.
 +        // If first rendering, `contentGroup.position` is [0, 0], which
 +        // does not make sense and may cause unexepcted animation if adopted.
 +        if (!isFirstRender) {
 +            contentPos[orientIdx] = contentGroup[yx];
 +        }
 +
 +        // Layout container group based on 0.
 +        const containerPos = [0, 0];
 +        const controllerPos = [-controllerRect.x, -controllerRect.y];
 +        const pageButtonGap = zrUtil.retrieve2(
 +            legendModel.get('pageButtonGap', true), legendModel.get('itemGap', true)
 +        );
 +
 +        // Place containerGroup and controllerGroup and contentGroup.
 +        if (showController) {
 +            const pageButtonPosition = legendModel.get('pageButtonPosition', true);
 +            // controller is on the right / bottom.
 +            if (pageButtonPosition === 'end') {
 +                controllerPos[orientIdx] += maxSize[wh] - controllerRect[wh];
 +            }
 +            // controller is on the left / top.
 +            else {
 +                containerPos[orientIdx] += controllerRect[wh] + pageButtonGap;
 +            }
 +        }
 +
 +        // Always align controller to content as 'middle'.
 +        controllerPos[1 - orientIdx] += contentRect[hw] / 2 - controllerRect[hw] / 2;
 +
 +        contentGroup.setPosition(contentPos);
 +        containerGroup.setPosition(containerPos);
 +        controllerGroup.setPosition(controllerPos);
 +
 +        // Calculate `mainRect` and set `clipPath`.
 +        // mainRect should not be calculated by `this.group.getBoundingRect()`
 +        // for sake of the overflow.
 +        const mainRect = {x: 0, y: 0} as ZRRectLike;
 +
 +        // Consider content may be overflow (should be clipped).
 +        mainRect[wh] = showController ? maxSize[wh] : contentRect[wh];
 +        mainRect[hw] = Math.max(contentRect[hw], controllerRect[hw]);
 +
 +        // `containerRect[yx] + containerPos[1 - orientIdx]` is 0.
 +        mainRect[yx] = Math.min(0, controllerRect[yx] + controllerPos[1 - orientIdx]);
 +
 +        containerGroup.__rectSize = maxSize[wh];
 +        if (showController) {
 +            const clipShape = {x: 0, y: 0} as graphic.Rect['shape'];
 +            clipShape[wh] = Math.max(maxSize[wh] - controllerRect[wh] - pageButtonGap, 0);
 +            clipShape[hw] = mainRect[hw];
 +            containerGroup.setClipPath(new graphic.Rect({shape: clipShape}));
 +            // Consider content may be larger than container, container rect
 +            // can not be obtained from `containerGroup.getBoundingRect()`.
 +            containerGroup.__rectSize = clipShape[wh];
 +        }
 +        else {
 +            // Do not remove or ignore controller. Keep them set as placeholders.
 +            controllerGroup.eachChild(function (child: Displayable) {
 +                child.attr({
 +                    invisible: true,
 +                    silent: true
 +                });
 +            });
 +        }
 +
 +        // Content translate animation.
 +        const pageInfo = this._getPageInfo(legendModel);
 +        pageInfo.pageIndex != null && graphic.updateProps(
 +            contentGroup,
 +            { x: pageInfo.contentPosition[0], y: pageInfo.contentPosition[1] },
 +            // When switch from "show controller" to "not show controller", view should be
 +            // updated immediately without animation, otherwise causes weird effect.
 +            showController ? legendModel : null
 +        );
 +
 +        this._updatePageInfoView(legendModel, pageInfo);
 +
 +        return mainRect;
 +    }
 +
 +    _pageGo(
 +        to: 'pagePrevDataIndex' | 'pageNextDataIndex',
 +        legendModel: ScrollableLegendModel,
 +        api: ExtensionAPI
 +    ) {
 +        const scrollDataIndex = this._getPageInfo(legendModel)[to];
 +
 +        scrollDataIndex != null && api.dispatchAction({
 +            type: 'legendScroll',
 +            scrollDataIndex: scrollDataIndex,
 +            legendId: legendModel.id
 +        });
 +    }
 +
 +    _updatePageInfoView(
 +        legendModel: ScrollableLegendModel,
 +        pageInfo: PageInfo
 +    ) {
 +        const controllerGroup = this._controllerGroup;
 +
 +        zrUtil.each(['pagePrev', 'pageNext'], function (name) {
 +            const key = (name + 'DataIndex') as'pagePrevDataIndex' | 'pageNextDataIndex';
 +            const canJump = pageInfo[key] != null;
 +            const icon = controllerGroup.childOfName(name) as graphic.Path;
 +            if (icon) {
 +                icon.setStyle(
 +                    'fill',
 +                    canJump
 +                        ? legendModel.get('pageIconColor', true)
 +                        : legendModel.get('pageIconInactiveColor', true)
 +                );
 +                icon.cursor = canJump ? 'pointer' : 'default';
 +            }
 +        });
 +
 +        const pageText = controllerGroup.childOfName('pageText') as graphic.Text;
 +        const pageFormatter = legendModel.get('pageFormatter');
 +        const pageIndex = pageInfo.pageIndex;
 +        const current = pageIndex != null ? pageIndex + 1 : 0;
 +        const total = pageInfo.pageCount;
 +
 +        pageText && pageFormatter && pageText.setStyle(
 +            'text',
 +            zrUtil.isString(pageFormatter)
 +                ? pageFormatter.replace('{current}', current == null ? '' : current + '')
 +                    .replace('{total}', total == null ? '' : total + '')
 +                : pageFormatter({current: current, total: total})
 +        );
 +    }
 +
 +    /**
 +     *  contentPosition: Array.<number>, null when data item not found.
 +     *  pageIndex: number, null when data item not found.
 +     *  pageCount: number, always be a number, can be 0.
 +     *  pagePrevDataIndex: number, null when no previous page.
 +     *  pageNextDataIndex: number, null when no next page.
 +     * }
 +     */
 +    _getPageInfo(legendModel: ScrollableLegendModel): PageInfo {
 +        const scrollDataIndex = legendModel.get('scrollDataIndex', true);
 +        const contentGroup = this.getContentGroup();
 +        const containerRectSize = this._containerGroup.__rectSize;
 +        const orientIdx = legendModel.getOrient().index;
 +        const wh = WH[orientIdx];
 +        const xy = XY[orientIdx];
 +
 +        const targetItemIndex = this._findTargetItemIndex(scrollDataIndex);
 +        const children = contentGroup.children();
 +        const targetItem = children[targetItemIndex];
 +        const itemCount = children.length;
 +        const pCount = !itemCount ? 0 : 1;
 +
 +        const result: PageInfo = {
 +            contentPosition: [contentGroup.x, contentGroup.y],
 +            pageCount: pCount,
 +            pageIndex: pCount - 1,
 +            pagePrevDataIndex: null,
 +            pageNextDataIndex: null
 +        };
 +
 +        if (!targetItem) {
 +            return result;
 +        }
 +
 +        const targetItemInfo = getItemInfo(targetItem);
 +        result.contentPosition[orientIdx] = -targetItemInfo.s;
 +
 +        // Strategy:
 +        // (1) Always align based on the left/top most item.
 +        // (2) It is user-friendly that the last item shown in the
 +        // current window is shown at the begining of next window.
 +        // Otherwise if half of the last item is cut by the window,
 +        // it will have no chance to display entirely.
 +        // (3) Consider that item size probably be different, we
 +        // have calculate pageIndex by size rather than item index,
 +        // and we can not get page index directly by division.
 +        // (4) The window is to narrow to contain more than
 +        // one item, we should make sure that the page can be fliped.
 +
 +        for (let i = targetItemIndex + 1,
 +            winStartItemInfo = targetItemInfo,
 +            winEndItemInfo = targetItemInfo,
 +            currItemInfo = null;
 +            i <= itemCount;
 +            ++i
 +        ) {
 +            currItemInfo = getItemInfo(children[i]);
 +            if (
 +                // Half of the last item is out of the window.
 +                (!currItemInfo && winEndItemInfo.e > winStartItemInfo.s + containerRectSize)
 +                // If the current item does not intersect with the window, the new page
 +                // can be started at the current item or the last item.
 +                || (currItemInfo && !intersect(currItemInfo, winStartItemInfo.s))
 +            ) {
 +                if (winEndItemInfo.i > winStartItemInfo.i) {
 +                    winStartItemInfo = winEndItemInfo;
 +                }
 +                else { // e.g., when page size is smaller than item size.
 +                    winStartItemInfo = currItemInfo;
 +                }
 +                if (winStartItemInfo) {
 +                    if (result.pageNextDataIndex == null) {
 +                        result.pageNextDataIndex = winStartItemInfo.i;
 +                    }
 +                    ++result.pageCount;
 +                }
 +            }
 +            winEndItemInfo = currItemInfo;
 +        }
 +
 +        for (let i = targetItemIndex - 1,
 +            winStartItemInfo = targetItemInfo,
 +            winEndItemInfo = targetItemInfo,
 +            currItemInfo = null;
 +            i >= -1;
 +            --i
 +        ) {
 +            currItemInfo = getItemInfo(children[i]);
 +            if (
 +                // If the the end item does not intersect with the window started
 +                // from the current item, a page can be settled.
 +                (!currItemInfo || !intersect(winEndItemInfo, currItemInfo.s))
 +                // e.g., when page size is smaller than item size.
 +                && winStartItemInfo.i < winEndItemInfo.i
 +            ) {
 +                winEndItemInfo = winStartItemInfo;
 +                if (result.pagePrevDataIndex == null) {
 +                    result.pagePrevDataIndex = winStartItemInfo.i;
 +                }
 +                ++result.pageCount;
 +                ++result.pageIndex;
 +            }
 +            winStartItemInfo = currItemInfo;
 +        }
 +
 +        return result;
 +
 +        function getItemInfo(el: Element): ItemInfo {
 +            if (el) {
 +                const itemRect = el.getBoundingRect();
 +                const start = itemRect[xy] + el[xy];
 +                return {
 +                    s: start,
 +                    e: start + itemRect[wh],
 +                    i: (el as LegendItemElement).__legendDataIndex
 +                };
 +            }
 +        }
 +
 +        function intersect(itemInfo: ItemInfo, winStart: number) {
 +            return itemInfo.e >= winStart && itemInfo.s <= winStart + containerRectSize;
 +        }
 +    }
 +
 +    _findTargetItemIndex(targetDataIndex: number) {
 +        if (!this._showController) {
 +            return 0;
 +        }
 +
 +        let index;
 +        const contentGroup = this.getContentGroup();
 +        let defaultIndex: number;
 +
 +        contentGroup.eachChild(function (child, idx) {
 +            const legendDataIdx = (child as LegendItemElement).__legendDataIndex;
 +            // FIXME
 +            // If the given targetDataIndex (from model) is illegal,
-             // we use defualtIndex. But the index on the legend model and
++            // we use defaultIndex. But the index on the legend model and
 +            // action payload is still illegal. That case will not be
 +            // changed until some scenario requires.
 +            if (defaultIndex == null && legendDataIdx != null) {
 +                defaultIndex = idx;
 +            }
 +            if (legendDataIdx === targetDataIndex) {
 +                index = idx;
 +            }
 +        });
 +
 +        return index != null ? index : defaultIndex;
 +    }
 +}
 +
 +ComponentView.registerClass(ScrollableLegendView);
 +
 +export default ScrollableLegendView;
diff --cc src/component/marker/MarkAreaView.ts
index cd992fc,0000000..2b12284
mode 100644,000000..100644
--- a/src/component/marker/MarkAreaView.ts
+++ b/src/component/marker/MarkAreaView.ts
@@@ -1,382 -1,0 +1,421 @@@
 +/*
 +* 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.
 +*/
 +
 +// TODO Optimize on polar
 +
 +import * as colorUtil from 'zrender/src/tool/color';
 +import List from '../../data/List';
 +import * as numberUtil from '../../util/number';
 +import * as graphic from '../../util/graphic';
 +import { enableHoverEmphasis, setStatesStylesFromModel } from '../../util/states';
 +import * as markerHelper from './markerHelper';
 +import MarkerView from './MarkerView';
- import { retrieve, mergeAll, map, defaults, curry, filter, HashMap } from 'zrender/src/core/util';
++import { retrieve, mergeAll, map, defaults, curry, filter, HashMap, each } from 'zrender/src/core/util';
 +import { ScaleDataValue, ParsedValue, ZRColor } from '../../util/types';
 +import { CoordinateSystem, isCoordinateSystemType } from '../../coord/CoordinateSystem';
 +import MarkAreaModel, { MarkArea2DDataItemOption } from './MarkAreaModel';
 +import SeriesModel from '../../model/Series';
 +import Cartesian2D from '../../coord/cartesian/Cartesian2D';
 +import DataDimensionInfo from '../../data/DataDimensionInfo';
 +import ComponentView from '../../view/Component';
 +import GlobalModel from '../../model/Global';
 +import ExtensionAPI from '../../ExtensionAPI';
 +import MarkerModel from './MarkerModel';
 +import { makeInner } from '../../util/model';
 +import { getVisualFromData } from '../../visual/helper';
 +import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle';
 +import { getECData } from '../../util/innerStore';
 +
 +interface MarkAreaDrawGroup {
 +    group: graphic.Group
 +}
 +
 +const inner = makeInner<{
 +    data: List<MarkAreaModel>
 +}, MarkAreaDrawGroup>();
 +
 +// Merge two ends option into one.
 +type MarkAreaMergedItemOption = Omit<MarkArea2DDataItemOption[number], 'coord'> & {
 +    coord: MarkArea2DDataItemOption[number]['coord'][]
 +    x0: number | string
 +    y0: number | string
 +    x1: number | string
 +    y1: number | string
 +};
 +
 +const markAreaTransform = function (
 +    seriesModel: SeriesModel,
 +    coordSys: CoordinateSystem,
 +    maModel: MarkAreaModel,
 +    item: MarkArea2DDataItemOption
 +): MarkAreaMergedItemOption {
 +    const lt = markerHelper.dataTransform(seriesModel, item[0]);
 +    const rb = markerHelper.dataTransform(seriesModel, item[1]);
 +
 +    // FIXME make sure lt is less than rb
 +    const ltCoord = lt.coord;
 +    const rbCoord = rb.coord;
 +    ltCoord[0] = retrieve(ltCoord[0], -Infinity);
 +    ltCoord[1] = retrieve(ltCoord[1], -Infinity);
 +
 +    rbCoord[0] = retrieve(rbCoord[0], Infinity);
 +    rbCoord[1] = retrieve(rbCoord[1], Infinity);
 +
 +    // Merge option into one
 +    const result: MarkAreaMergedItemOption = mergeAll([{}, lt, rb]);
 +
 +    result.coord = [
 +        lt.coord, rb.coord
 +    ];
 +    result.x0 = lt.x;
 +    result.y0 = lt.y;
 +    result.x1 = rb.x;
 +    result.y1 = rb.y;
 +    return result;
 +};
 +
 +function isInifinity(val: ScaleDataValue) {
 +    return !isNaN(val as number) && !isFinite(val as number);
 +}
 +
 +// If a markArea has one dim
 +function ifMarkAreaHasOnlyDim(
 +    dimIndex: number,
 +    fromCoord: ScaleDataValue[],
 +    toCoord: ScaleDataValue[],
 +    coordSys: CoordinateSystem
 +) {
 +    const otherDimIndex = 1 - dimIndex;
 +    return isInifinity(fromCoord[otherDimIndex]) && isInifinity(toCoord[otherDimIndex]);
 +}
 +
 +function markAreaFilter(coordSys: CoordinateSystem, item: MarkAreaMergedItemOption) {
 +    const fromCoord = item.coord[0];
 +    const toCoord = item.coord[1];
 +    if (isCoordinateSystemType<Cartesian2D>(coordSys, 'cartesian2d')) {
 +        // In case
 +        // {
 +        //  markArea: {
 +        //    data: [{ yAxis: 2 }]
 +        //  }
 +        // }
 +        if (
 +            fromCoord && toCoord
 +            && (ifMarkAreaHasOnlyDim(1, fromCoord, toCoord, coordSys)
 +            || ifMarkAreaHasOnlyDim(0, fromCoord, toCoord, coordSys))
 +        ) {
 +            return true;
 +        }
 +    }
 +    return markerHelper.dataFilter(coordSys, {
 +            coord: fromCoord,
 +            x: item.x0,
 +            y: item.y0
 +        })
 +        || markerHelper.dataFilter(coordSys, {
 +            coord: toCoord,
 +            x: item.x1,
 +            y: item.y1
 +        });
 +}
 +
 +// dims can be ['x0', 'y0'], ['x1', 'y1'], ['x0', 'y1'], ['x1', 'y0']
 +function getSingleMarkerEndPoint(
 +    data: List<MarkAreaModel>,
 +    idx: number,
 +    dims: typeof dimPermutations[number],
 +    seriesModel: SeriesModel,
 +    api: ExtensionAPI
 +) {
 +    const coordSys = seriesModel.coordinateSystem;
 +    const itemModel = data.getItemModel<MarkAreaMergedItemOption>(idx);
 +
 +    let point;
 +    const xPx = numberUtil.parsePercent(itemModel.get(dims[0]), api.getWidth());
 +    const yPx = numberUtil.parsePercent(itemModel.get(dims[1]), api.getHeight());
 +    if (!isNaN(xPx) && !isNaN(yPx)) {
 +        point = [xPx, yPx];
 +    }
 +    else {
 +        // Chart like bar may have there own marker positioning logic
 +        if (seriesModel.getMarkerPosition) {
 +            // Use the getMarkerPoisition
 +            point = seriesModel.getMarkerPosition(
 +                data.getValues(dims, idx)
 +            );
 +        }
 +        else {
 +            const x = data.get(dims[0], idx) as number;
 +            const y = data.get(dims[1], idx) as number;
 +            const pt = [x, y];
 +            coordSys.clampData && coordSys.clampData(pt, pt);
 +            point = coordSys.dataToPoint(pt, true);
 +        }
 +        if (isCoordinateSystemType<Cartesian2D>(coordSys, 'cartesian2d')) {
 +            const xAxis = coordSys.getAxis('x');
 +            const yAxis = coordSys.getAxis('y');
 +            const x = data.get(dims[0], idx) as number;
 +            const y = data.get(dims[1], idx) as number;
 +            if (isInifinity(x)) {
 +                point[0] = xAxis.toGlobalCoord(xAxis.getExtent()[dims[0] === 'x0' ? 0 : 1]);
 +            }
 +            else if (isInifinity(y)) {
 +                point[1] = yAxis.toGlobalCoord(yAxis.getExtent()[dims[1] === 'y0' ? 0 : 1]);
 +            }
 +        }
 +
 +        // Use x, y if has any
 +        if (!isNaN(xPx)) {
 +            point[0] = xPx;
 +        }
 +        if (!isNaN(yPx)) {
 +            point[1] = yPx;
 +        }
 +    }
 +
 +    return point;
 +}
 +
 +const dimPermutations = [['x0', 'y0'], ['x1', 'y0'], ['x1', 'y1'], ['x0', 'y1']] as const;
 +
 +class MarkAreaView extends MarkerView {
 +
 +    static type = 'markArea';
 +    type = MarkAreaView.type;
 +
 +    markerGroupMap: HashMap<MarkAreaDrawGroup>;
 +
 +    updateTransform(markAreaModel: MarkAreaModel, ecModel: GlobalModel, api: ExtensionAPI) {
 +        ecModel.eachSeries(function (seriesModel) {
 +            const maModel = MarkerModel.getMarkerModelFromSeries(seriesModel, 'markArea');
 +            if (maModel) {
 +                const areaData = maModel.getData();
 +                areaData.each(function (idx) {
 +                    const points = map(dimPermutations, function (dim) {
 +                        return getSingleMarkerEndPoint(areaData, idx, dim, seriesModel, api);
 +                    });
 +                    // Layout
 +                    areaData.setItemLayout(idx, points);
 +                    const el = areaData.getItemGraphicEl(idx) as graphic.Polygon;
 +                    el.setShape('points', points);
 +                });
 +            }
 +        }, this);
 +    }
 +
 +    renderSeries(
 +        seriesModel: SeriesModel,
 +        maModel: MarkAreaModel,
 +        ecModel: GlobalModel,
 +        api: ExtensionAPI
 +    ) {
 +        const coordSys = seriesModel.coordinateSystem;
 +        const seriesId = seriesModel.id;
 +        const seriesData = seriesModel.getData();
 +
 +        const areaGroupMap = this.markerGroupMap;
 +        const polygonGroup = areaGroupMap.get(seriesId)
 +            || areaGroupMap.set(seriesId, {group: new graphic.Group()});
 +
 +        this.group.add(polygonGroup.group);
 +        this.markKeep(polygonGroup);
 +
 +        const areaData = createList(coordSys, seriesModel, maModel);
 +
 +        // Line data for tooltip and formatter
 +        maModel.setData(areaData);
 +
 +        // Update visual and layout of line
 +        areaData.each(function (idx) {
 +            // Layout
-             areaData.setItemLayout(idx, map(dimPermutations, function (dim) {
++            const points = map(dimPermutations, function (dim) {
 +                return getSingleMarkerEndPoint(areaData, idx, dim, seriesModel, api);
-             }));
++            });
++            // If none of the area is inside coordSys, allClipped is set to be true
++            // in layout so that label will not be displayed. See #12591
++            let allClipped = true;
++            each(dimPermutations, function (dim) {
++                if (!allClipped) {
++                    return;
++                }
++                const xValue = areaData.get(dim[0], idx);
++                const yValue = areaData.get(dim[1], idx);
++                // If is infinity, the axis should be considered not clipped
++                if ((isInifinity(xValue) || coordSys.getAxis('x').containData(xValue))
++                    && (isInifinity(yValue) || coordSys.getAxis('y').containData(yValue))
++                ) {
++                    allClipped = false;
++                }
++            });
++            areaData.setItemLayout(idx, {
++                points: points,
++                allClipped: allClipped
++            });
++
 +
 +            const style = areaData.getItemModel<MarkAreaMergedItemOption>(idx).getModel('itemStyle').getItemStyle();
 +            const color = getVisualFromData(seriesData, 'color') as ZRColor;
 +            if (!style.fill) {
 +                style.fill = color;
 +                if (typeof style.fill === 'string') {
 +                    style.fill = colorUtil.modifyAlpha(style.fill, 0.4);
 +                }
 +            }
 +            if (!style.stroke) {
 +                style.stroke = color;
 +            }
 +            // Visual
 +            areaData.setItemVisual(idx, 'style', style);
 +        });
 +
 +
 +        areaData.diff(inner(polygonGroup).data)
 +            .add(function (idx) {
-                 const polygon = new graphic.Polygon({
-                     shape: {
-                         points: areaData.getItemLayout(idx)
-                     }
-                 });
-                 areaData.setItemGraphicEl(idx, polygon);
-                 polygonGroup.group.add(polygon);
++                const layout = areaData.getItemLayout(idx);
++                if (!layout.allClipped) {
++                    const polygon = new graphic.Polygon({
++                        shape: {
++                            points: layout.points
++                        }
++                    });
++                    areaData.setItemGraphicEl(idx, polygon);
++                    polygonGroup.group.add(polygon);
++                }
 +            })
 +            .update(function (newIdx, oldIdx) {
-                 const polygon = inner(polygonGroup).data.getItemGraphicEl(oldIdx) as graphic.Polygon;
-                 graphic.updateProps(polygon, {
-                     shape: {
-                         points: areaData.getItemLayout(newIdx)
++                let polygon = inner(polygonGroup).data.getItemGraphicEl(oldIdx) as graphic.Polygon;
++                const layout = areaData.getItemLayout(newIdx);
++                if (!layout.allClipped) {
++                    if (polygon) {
++                        graphic.updateProps(polygon, {
++                            shape: {
++                                points: layout.points
++                            }
++                        }, maModel, newIdx);
++                    }
++                    else {
++                        polygon = new graphic.Polygon({
++                            shape: {
++                                points: layout.points
++                            }
++                        });
 +                    }
-                 }, maModel, newIdx);
-                 polygonGroup.group.add(polygon);
-                 areaData.setItemGraphicEl(newIdx, polygon);
++                    areaData.setItemGraphicEl(newIdx, polygon);
++                    polygonGroup.group.add(polygon);
++                }
++                else if (polygon) {
++                    polygonGroup.group.remove(polygon);
++                }
 +            })
 +            .remove(function (idx) {
 +                const polygon = inner(polygonGroup).data.getItemGraphicEl(idx);
 +                polygonGroup.group.remove(polygon);
 +            })
 +            .execute();
 +
 +        areaData.eachItemGraphicEl(function (polygon: graphic.Polygon, idx) {
 +            const itemModel = areaData.getItemModel<MarkAreaMergedItemOption>(idx);
 +            const style = areaData.getItemVisual(idx, 'style');
 +            polygon.useStyle(areaData.getItemVisual(idx, 'style'));
 +
 +            setLabelStyle(
 +                polygon, getLabelStatesModels(itemModel),
 +                {
 +                    labelFetcher: maModel,
 +                    labelDataIndex: idx,
 +                    defaultText: areaData.getName(idx) || '',
 +                    inheritColor: typeof style.fill === 'string'
 +                        ? colorUtil.modifyAlpha(style.fill, 1) : '#000'
 +                }
 +            );
 +
 +            setStatesStylesFromModel(polygon, itemModel);
 +
 +            enableHoverEmphasis(polygon);
 +
 +            getECData(polygon).dataModel = maModel;
 +        });
 +
 +        inner(polygonGroup).data = areaData;
 +
 +        polygonGroup.group.silent = maModel.get('silent') || seriesModel.get('silent');
 +    }
 +}
 +
 +function createList(
 +    coordSys: CoordinateSystem,
 +    seriesModel: SeriesModel,
 +    maModel: MarkAreaModel
 +) {
 +
 +    let coordDimsInfos: DataDimensionInfo[];
 +    let areaData: List<MarkAreaModel>;
 +    const dims = ['x0', 'y0', 'x1', 'y1'];
 +    if (coordSys) {
 +        coordDimsInfos = map(coordSys && coordSys.dimensions, function (coordDim) {
 +            const data = seriesModel.getData();
 +            const info = data.getDimensionInfo(
 +                data.mapDimension(coordDim)
 +            ) || {};
 +            // In map series data don't have lng and lat dimension. Fallback to same with coordSys
 +            return defaults({
 +                name: coordDim
 +            }, info);
 +        });
 +        areaData = new List(map(dims, function (dim, idx) {
 +            return {
 +                name: dim,
 +                type: coordDimsInfos[idx % 2].type
 +            };
 +        }), maModel);
 +    }
 +    else {
 +        coordDimsInfos = [{
 +            name: 'value',
 +            type: 'float'
 +        }];
 +        areaData = new List(coordDimsInfos, maModel);
 +    }
 +
 +    let optData = map(maModel.get('data'), curry(
 +        markAreaTransform, seriesModel, coordSys, maModel
 +    ));
 +    if (coordSys) {
 +        optData = filter(
 +            optData, curry(markAreaFilter, coordSys)
 +        );
 +    }
 +
 +    const dimValueGetter = coordSys ? function (
 +        item: MarkAreaMergedItemOption,
 +        dimName: string,
 +        dataIndex: number,
 +        dimIndex: number
 +    ) {
 +        // TODO should convert to ParsedValue?
 +        return item.coord[Math.floor(dimIndex / 2)][dimIndex % 2] as ParsedValue;
 +    } : function (item: MarkAreaMergedItemOption) {
 +        return item.value;
 +    };
 +    areaData.initData(optData, null, dimValueGetter);
 +    areaData.hasItemOption = true;
 +    return areaData;
 +}
 +
 +ComponentView.registerClass(MarkAreaView);
diff --cc src/component/marker/MarkLineView.ts
index 6f890ac,0000000..56e6b65
mode 100644,000000..100644
--- a/src/component/marker/MarkLineView.ts
+++ b/src/component/marker/MarkLineView.ts
@@@ -1,461 -1,0 +1,464 @@@
 +/*
 +* 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 List from '../../data/List';
 +import * as numberUtil from '../../util/number';
 +import * as markerHelper from './markerHelper';
 +import LineDraw from '../../chart/helper/LineDraw';
 +import MarkerView from './MarkerView';
 +import {getStackedDimension} from '../../data/helper/dataStackHelper';
 +import { CoordinateSystem, isCoordinateSystemType } from '../../coord/CoordinateSystem';
 +import MarkLineModel, { MarkLine2DDataItemOption, MarkLineOption } from './MarkLineModel';
 +import { ScaleDataValue, ColorString } from '../../util/types';
 +import SeriesModel from '../../model/Series';
 +import { getECData } from '../../util/innerStore';
 +import ExtensionAPI from '../../ExtensionAPI';
 +import Cartesian2D from '../../coord/cartesian/Cartesian2D';
 +import GlobalModel from '../../model/Global';
 +import MarkerModel from './MarkerModel';
 +import {
 +    isArray,
 +    retrieve,
 +    clone,
 +    extend,
 +    logError,
 +    merge,
 +    map,
 +    defaults,
 +    curry,
 +    filter,
 +    HashMap
 +} from 'zrender/src/core/util';
 +import ComponentView from '../../view/Component';
 +import { makeInner } from '../../util/model';
 +import { LineDataVisual } from '../../visual/commonVisualTypes';
 +import { getVisualFromData } from '../../visual/helper';
 +
 +// Item option for configuring line and each end of symbol.
 +// Line option. be merged from configuration of two ends.
 +type MarkLineMergedItemOption = MarkLine2DDataItemOption[number];
 +
 +const inner = makeInner<{
 +    // from data
 +    from: List<MarkLineModel>
 +    // to data
 +    to: List<MarkLineModel>
 +}, MarkLineModel>();
 +
 +const markLineTransform = function (
 +    seriesModel: SeriesModel,
 +    coordSys: CoordinateSystem,
 +    mlModel: MarkLineModel,
 +    item: MarkLineOption['data'][number]
 +) {
 +    const data = seriesModel.getData();
 +
 +    let itemArray: MarkLineMergedItemOption[];
 +    if (!isArray(item)) {
 +        // Special type markLine like 'min', 'max', 'average', 'median'
 +        const mlType = item.type;
 +        if (
 +            mlType === 'min' || mlType === 'max' || mlType === 'average' || mlType === 'median'
 +            // In case
 +            // data: [{
 +            //   yAxis: 10
 +            // }]
 +            || (item.xAxis != null || item.yAxis != null)
 +        ) {
 +
 +            let valueAxis;
 +            let value;
 +
 +            if (item.yAxis != null || item.xAxis != null) {
 +                valueAxis = coordSys.getAxis(item.yAxis != null ? 'y' : 'x');
 +                value = retrieve(item.yAxis, item.xAxis);
 +            }
 +            else {
 +                const axisInfo = markerHelper.getAxisInfo(item, data, coordSys, seriesModel);
 +                valueAxis = axisInfo.valueAxis;
 +                const valueDataDim = getStackedDimension(data, axisInfo.valueDataDim);
 +                value = markerHelper.numCalculate(data, valueDataDim, mlType);
 +            }
 +            const valueIndex = valueAxis.dim === 'x' ? 0 : 1;
 +            const baseIndex = 1 - valueIndex;
 +
 +            // Normized to 2d data with start and end point
 +            const mlFrom = clone(item) as MarkLine2DDataItemOption[number];
 +            const mlTo = {
 +                coord: []
 +            } as MarkLine2DDataItemOption[number];
 +
 +            mlFrom.type = null;
 +
 +            mlFrom.coord = [];
 +            mlFrom.coord[baseIndex] = -Infinity;
 +            mlTo.coord[baseIndex] = Infinity;
 +
 +            const precision = mlModel.get('precision');
 +            if (precision >= 0 && typeof value === 'number') {
 +                value = +value.toFixed(Math.min(precision, 20));
 +            }
 +
 +            mlFrom.coord[valueIndex] = mlTo.coord[valueIndex] = value;
 +
 +            itemArray = [mlFrom, mlTo, { // Extra option for tooltip and label
 +                type: mlType,
 +                valueIndex: item.valueIndex,
 +                // Force to use the value of calculated value.
 +                value: value
 +            }];
 +        }
 +        else {
 +            // Invalid data
 +            if (__DEV__) {
 +                logError('Invalid markLine data.');
 +            }
 +            itemArray = [];
 +        }
 +    }
 +    else {
 +        itemArray = item;
 +    }
 +
 +    const normalizedItem = [
 +        markerHelper.dataTransform(seriesModel, itemArray[0]),
 +        markerHelper.dataTransform(seriesModel, itemArray[1]),
 +        extend({}, itemArray[2])
 +    ];
 +
 +    // Avoid line data type is extended by from(to) data type
 +    normalizedItem[2].type = normalizedItem[2].type || null;
 +
 +    // Merge from option and to option into line option
 +    merge(normalizedItem[2], normalizedItem[0]);
 +    merge(normalizedItem[2], normalizedItem[1]);
 +
 +    return normalizedItem;
 +};
 +
 +function isInifinity(val: ScaleDataValue) {
 +    return !isNaN(val as number) && !isFinite(val as number);
 +}
 +
 +// If a markLine has one dim
 +function ifMarkLineHasOnlyDim(
 +    dimIndex: number,
 +    fromCoord: ScaleDataValue[],
 +    toCoord: ScaleDataValue[],
 +    coordSys: CoordinateSystem
 +) {
 +    const otherDimIndex = 1 - dimIndex;
 +    const dimName = coordSys.dimensions[dimIndex];
 +    return isInifinity(fromCoord[otherDimIndex]) && isInifinity(toCoord[otherDimIndex])
 +        && fromCoord[dimIndex] === toCoord[dimIndex] && coordSys.getAxis(dimName).containData(fromCoord[dimIndex]);
 +}
 +
 +function markLineFilter(
 +    coordSys: CoordinateSystem,
 +    item: MarkLine2DDataItemOption
 +) {
 +    if (coordSys.type === 'cartesian2d') {
 +        const fromCoord = item[0].coord;
 +        const toCoord = item[1].coord;
 +        // In case
 +        // {
 +        //  markLine: {
 +        //    data: [{ yAxis: 2 }]
 +        //  }
 +        // }
 +        if (
 +            fromCoord && toCoord
 +            && (ifMarkLineHasOnlyDim(1, fromCoord, toCoord, coordSys)
 +            || ifMarkLineHasOnlyDim(0, fromCoord, toCoord, coordSys))
 +        ) {
 +            return true;
 +        }
 +    }
 +    return markerHelper.dataFilter(coordSys, item[0])
 +        && markerHelper.dataFilter(coordSys, item[1]);
 +}
 +
 +function updateSingleMarkerEndLayout(
 +    data: List<MarkLineModel>,
 +    idx: number,
 +    isFrom: boolean,
 +    seriesModel: SeriesModel,
 +    api: ExtensionAPI
 +) {
 +    const coordSys = seriesModel.coordinateSystem;
 +    const itemModel = data.getItemModel<MarkLine2DDataItemOption[number]>(idx);
 +
 +    let point;
 +    const xPx = numberUtil.parsePercent(itemModel.get('x'), api.getWidth());
 +    const yPx = numberUtil.parsePercent(itemModel.get('y'), api.getHeight());
 +    if (!isNaN(xPx) && !isNaN(yPx)) {
 +        point = [xPx, yPx];
 +    }
 +    else {
 +        // Chart like bar may have there own marker positioning logic
 +        if (seriesModel.getMarkerPosition) {
 +            // Use the getMarkerPoisition
 +            point = seriesModel.getMarkerPosition(
 +                data.getValues(data.dimensions, idx)
 +            );
 +        }
 +        else {
 +            const dims = coordSys.dimensions;
 +            const x = data.get(dims[0], idx);
 +            const y = data.get(dims[1], idx);
 +            point = coordSys.dataToPoint([x, y]);
 +        }
 +        // Expand line to the edge of grid if value on one axis is Inifnity
 +        // In case
 +        //  markLine: {
 +        //    data: [{
 +        //      yAxis: 2
 +        //      // or
 +        //      type: 'average'
 +        //    }]
 +        //  }
 +        if (isCoordinateSystemType<Cartesian2D>(coordSys, 'cartesian2d')) {
 +            const xAxis = coordSys.getAxis('x');
 +            const yAxis = coordSys.getAxis('y');
 +            const dims = coordSys.dimensions;
 +            if (isInifinity(data.get(dims[0], idx))) {
 +                point[0] = xAxis.toGlobalCoord(xAxis.getExtent()[isFrom ? 0 : 1]);
 +            }
 +            else if (isInifinity(data.get(dims[1], idx))) {
 +                point[1] = yAxis.toGlobalCoord(yAxis.getExtent()[isFrom ? 0 : 1]);
 +            }
 +        }
 +
 +        // Use x, y if has any
 +        if (!isNaN(xPx)) {
 +            point[0] = xPx;
 +        }
 +        if (!isNaN(yPx)) {
 +            point[1] = yPx;
 +        }
 +    }
 +
 +    data.setItemLayout(idx, point);
 +}
 +
 +class MarkLineView extends MarkerView {
 +
 +    static type = 'markLine';
 +    type = MarkLineView.type;
 +
 +    markerGroupMap: HashMap<LineDraw>;
 +
 +    updateTransform(markLineModel: MarkLineModel, ecModel: GlobalModel, api: ExtensionAPI) {
 +        ecModel.eachSeries(function (seriesModel) {
 +            const mlModel = MarkerModel.getMarkerModelFromSeries(seriesModel, 'markLine') as MarkLineModel;
 +            if (mlModel) {
 +                const mlData = mlModel.getData();
 +                const fromData = inner(mlModel).from;
 +                const toData = inner(mlModel).to;
 +                // Update visual and layout of from symbol and to symbol
 +                fromData.each(function (idx) {
 +                    updateSingleMarkerEndLayout(fromData, idx, true, seriesModel, api);
 +                    updateSingleMarkerEndLayout(toData, idx, false, seriesModel, api);
 +                });
 +                // Update layout of line
 +                mlData.each(function (idx) {
 +                    mlData.setItemLayout(idx, [
 +                        fromData.getItemLayout(idx),
 +                        toData.getItemLayout(idx)
 +                    ]);
 +                });
 +
 +                this.markerGroupMap.get(seriesModel.id).updateLayout();
 +
 +            }
 +        }, this);
 +    }
 +
 +    renderSeries(
 +        seriesModel: SeriesModel,
 +        mlModel: MarkLineModel,
 +        ecModel: GlobalModel,
 +        api: ExtensionAPI
 +    ) {
 +        const coordSys = seriesModel.coordinateSystem;
 +        const seriesId = seriesModel.id;
 +        const seriesData = seriesModel.getData();
 +
 +        const lineDrawMap = this.markerGroupMap;
 +        const lineDraw = lineDrawMap.get(seriesId)
 +            || lineDrawMap.set(seriesId, new LineDraw());
 +        this.group.add(lineDraw.group);
 +
 +        const mlData = createList(coordSys, seriesModel, mlModel);
 +
 +        const fromData = mlData.from;
 +        const toData = mlData.to;
 +        const lineData = mlData.line as List<MarkLineModel, LineDataVisual>;
 +
 +        inner(mlModel).from = fromData;
 +        inner(mlModel).to = toData;
 +        // Line data for tooltip and formatter
 +        mlModel.setData(lineData);
 +
 +        let symbolType = mlModel.get('symbol');
 +        let symbolSize = mlModel.get('symbolSize');
 +        if (!isArray(symbolType)) {
 +            symbolType = [symbolType, symbolType];
 +        }
 +        if (!isArray(symbolSize)) {
 +            symbolSize = [symbolSize, symbolSize];
 +        }
 +
 +        // Update visual and layout of from symbol and to symbol
 +        mlData.from.each(function (idx) {
 +            updateDataVisualAndLayout(fromData, idx, true);
 +            updateDataVisualAndLayout(toData, idx, false);
 +        });
 +
 +        // Update visual and layout of line
 +        lineData.each(function (idx) {
 +            const lineStyle = lineData.getItemModel<MarkLineMergedItemOption>(idx)
 +                .getModel('lineStyle').getLineStyle();
 +            // lineData.setItemVisual(idx, {
 +            //     color: lineColor || fromData.getItemVisual(idx, 'color')
 +            // });
 +            lineData.setItemLayout(idx, [
 +                fromData.getItemLayout(idx),
 +                toData.getItemLayout(idx)
 +            ]);
 +
 +            if (lineStyle.stroke == null) {
 +                lineStyle.stroke = fromData.getItemVisual(idx, 'style').fill;
 +            }
 +
 +            lineData.setItemVisual(idx, {
++                fromSymbolRotate: fromData.getItemVisual(idx, 'symbolRotate'),
 +                fromSymbolSize: fromData.getItemVisual(idx, 'symbolSize') as number,
 +                fromSymbol: fromData.getItemVisual(idx, 'symbol'),
++                toSymbolRotate: toData.getItemVisual(idx, 'symbolRotate'),
 +                toSymbolSize: toData.getItemVisual(idx, 'symbolSize') as number,
 +                toSymbol: toData.getItemVisual(idx, 'symbol'),
 +                style: lineStyle
 +            });
 +        });
 +
 +        lineDraw.updateData(lineData);
 +
 +        // Set host model for tooltip
 +        // FIXME
 +        mlData.line.eachItemGraphicEl(function (el, idx) {
 +            el.traverse(function (child) {
 +                getECData(child).dataModel = mlModel;
 +            });
 +        });
 +
 +        function updateDataVisualAndLayout(
 +            data: List<MarkLineModel>,
 +            idx: number,
 +            isFrom: boolean
 +        ) {
 +            const itemModel = data.getItemModel<MarkLineMergedItemOption>(idx);
 +
 +            updateSingleMarkerEndLayout(
 +                data, idx, isFrom, seriesModel, api
 +            );
 +
 +            const style = itemModel.getModel('itemStyle').getItemStyle();
 +            if (style.fill == null) {
 +                style.fill = getVisualFromData(seriesData, 'color') as ColorString;
 +            }
 +
 +            data.setItemVisual(idx, {
++                symbolRotate: itemModel.get('symbolRotate'),
 +                symbolSize: itemModel.get('symbolSize') || (symbolSize as number[])[isFrom ? 0 : 1],
 +                symbol: itemModel.get('symbol', true) || (symbolType as string[])[isFrom ? 0 : 1],
 +                style
 +            });
 +        }
 +
 +        this.markKeep(lineDraw);
 +
 +        lineDraw.group.silent = mlModel.get('silent') || seriesModel.get('silent');
 +    }
 +}
 +
 +function createList(coordSys: CoordinateSystem, seriesModel: SeriesModel, mlModel: MarkLineModel) {
 +
 +    let coordDimsInfos;
 +    if (coordSys) {
 +        coordDimsInfos = map(coordSys && coordSys.dimensions, function (coordDim) {
 +            const info = seriesModel.getData().getDimensionInfo(
 +                seriesModel.getData().mapDimension(coordDim)
 +            ) || {};
 +            // In map series data don't have lng and lat dimension. Fallback to same with coordSys
 +            return defaults({name: coordDim}, info);
 +        });
 +    }
 +    else {
 +        coordDimsInfos = [{
 +            name: 'value',
 +            type: 'float'
 +        }];
 +    }
 +
 +    const fromData = new List(coordDimsInfos, mlModel);
 +    const toData = new List(coordDimsInfos, mlModel);
 +    // No dimensions
 +    const lineData = new List([], mlModel);
 +
 +    let optData = map(mlModel.get('data'), curry(
 +        markLineTransform, seriesModel, coordSys, mlModel
 +    ));
 +    if (coordSys) {
 +        optData = filter(
 +            optData, curry(markLineFilter, coordSys)
 +        );
 +    }
 +    const dimValueGetter = coordSys ? markerHelper.dimValueGetter : function (item: MarkLineMergedItemOption) {
 +        return item.value;
 +    };
 +    fromData.initData(
 +        map(optData, function (item) {
 +            return item[0];
 +        }),
 +        null,
 +        dimValueGetter
 +    );
 +    toData.initData(
 +        map(optData, function (item) {
 +            return item[1];
 +        }),
 +        null,
 +        dimValueGetter
 +    );
 +    lineData.initData(
 +        map(optData, function (item) {
 +            return item[2];
 +        })
 +    );
 +    lineData.hasItemOption = true;
 +
 +    return {
 +        from: fromData,
 +        to: toData,
 +        line: lineData
 +    };
 +}
 +
 +ComponentView.registerClass(MarkLineView);
diff --cc src/component/marker/MarkPointView.ts
index 503e855,0000000..db5bc14
mode 100644,000000..100644
--- a/src/component/marker/MarkPointView.ts
+++ b/src/component/marker/MarkPointView.ts
@@@ -1,208 -1,0 +1,213 @@@
 +/*
 +* 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 SymbolDraw from '../../chart/helper/SymbolDraw';
 +import * as numberUtil from '../../util/number';
 +import List from '../../data/List';
 +import * as markerHelper from './markerHelper';
 +import MarkerView from './MarkerView';
 +import ComponentView from '../../view/Component';
 +import { CoordinateSystem } from '../../coord/CoordinateSystem';
 +import SeriesModel from '../../model/Series';
 +import MarkPointModel, {MarkPointDataItemOption} from './MarkPointModel';
 +import GlobalModel from '../../model/Global';
 +import MarkerModel from './MarkerModel';
 +import ExtensionAPI from '../../ExtensionAPI';
 +import { HashMap, isFunction, map, defaults, filter, curry } from 'zrender/src/core/util';
 +import { getECData } from '../../util/innerStore';
 +import { getVisualFromData } from '../../visual/helper';
 +import { ZRColor } from '../../util/types';
 +
 +function updateMarkerLayout(
 +    mpData: List<MarkPointModel>,
 +    seriesModel: SeriesModel,
 +    api: ExtensionAPI
 +) {
 +    const coordSys = seriesModel.coordinateSystem;
 +    mpData.each(function (idx: number) {
 +        const itemModel = mpData.getItemModel<MarkPointDataItemOption>(idx);
 +        let point;
 +        const xPx = numberUtil.parsePercent(itemModel.get('x'), api.getWidth());
 +        const yPx = numberUtil.parsePercent(itemModel.get('y'), api.getHeight());
 +        if (!isNaN(xPx) && !isNaN(yPx)) {
 +            point = [xPx, yPx];
 +        }
 +        // Chart like bar may have there own marker positioning logic
 +        else if (seriesModel.getMarkerPosition) {
 +            // Use the getMarkerPoisition
 +            point = seriesModel.getMarkerPosition(
 +                mpData.getValues(mpData.dimensions, idx)
 +            );
 +        }
 +        else if (coordSys) {
 +            const x = mpData.get(coordSys.dimensions[0], idx);
 +            const y = mpData.get(coordSys.dimensions[1], idx);
 +            point = coordSys.dataToPoint([x, y]);
 +
 +        }
 +
 +        // Use x, y if has any
 +        if (!isNaN(xPx)) {
 +            point[0] = xPx;
 +        }
 +        if (!isNaN(yPx)) {
 +            point[1] = yPx;
 +        }
 +
 +        mpData.setItemLayout(idx, point);
 +    });
 +}
 +
 +class MarkPointView extends MarkerView {
 +
 +    static type = 'markPoint';
 +    type = MarkPointView.type;
 +
 +    markerGroupMap: HashMap<SymbolDraw>;
 +
 +    updateTransform(markPointModel: MarkPointModel, ecModel: GlobalModel, api: ExtensionAPI) {
 +        ecModel.eachSeries(function (seriesModel) {
 +            const mpModel = MarkerModel.getMarkerModelFromSeries(seriesModel, 'markPoint') as MarkPointModel;
 +            if (mpModel) {
 +                updateMarkerLayout(
 +                    mpModel.getData(),
 +                    seriesModel, api
 +                );
 +                this.markerGroupMap.get(seriesModel.id).updateLayout();
 +            }
 +        }, this);
 +    }
 +
 +    renderSeries(
 +        seriesModel: SeriesModel,
 +        mpModel: MarkPointModel,
 +        ecModel: GlobalModel,
 +        api: ExtensionAPI
 +    ) {
 +        const coordSys = seriesModel.coordinateSystem;
 +        const seriesId = seriesModel.id;
 +        const seriesData = seriesModel.getData();
 +
 +        const symbolDrawMap = this.markerGroupMap;
 +        const symbolDraw = symbolDrawMap.get(seriesId)
 +            || symbolDrawMap.set(seriesId, new SymbolDraw());
 +
 +        const mpData = createList(coordSys, seriesModel, mpModel);
 +
 +        // FIXME
 +        mpModel.setData(mpData);
 +
 +        updateMarkerLayout(mpModel.getData(), seriesModel, api);
 +
 +        mpData.each(function (idx) {
 +            const itemModel = mpData.getItemModel<MarkPointDataItemOption>(idx);
 +            let symbol = itemModel.getShallow('symbol');
 +            let symbolSize = itemModel.getShallow('symbolSize');
++            let symbolRotate = itemModel.getShallow('symbolRotate');
 +
-             if (isFunction(symbol) || isFunction(symbolSize)) {
++            if (isFunction(symbol) || isFunction(symbolSize) || isFunction(symbolRotate)) {
 +                const rawIdx = mpModel.getRawValue(idx);
 +                const dataParams = mpModel.getDataParams(idx);
 +                if (isFunction(symbol)) {
 +                    symbol = symbol(rawIdx, dataParams);
 +                }
 +                if (isFunction(symbolSize)) {
 +                    // FIXME 这里不兼容 ECharts 2.x,2.x 貌似参数是整个数据?
 +                    symbolSize = symbolSize(rawIdx, dataParams);
 +                }
++                if (isFunction(symbolRotate)) {
++                    symbolRotate = symbolRotate(rawIdx, dataParams);
++                }
 +            }
 +
 +            const style = itemModel.getModel('itemStyle').getItemStyle();
 +            const color = getVisualFromData(seriesData, 'color') as ZRColor;
 +            if (!style.fill) {
 +                style.fill = color;
 +            }
 +
 +            mpData.setItemVisual(idx, {
 +                symbol: symbol,
 +                symbolSize: symbolSize,
++                symbolRotate: symbolRotate,
 +                style
 +            });
 +        });
 +
 +        // TODO Text are wrong
 +        symbolDraw.updateData(mpData);
 +        this.group.add(symbolDraw.group);
 +
 +        // Set host model for tooltip
 +        // FIXME
 +        mpData.eachItemGraphicEl(function (el) {
 +            el.traverse(function (child) {
 +                getECData(child).dataModel = mpModel;
 +            });
 +        });
 +
 +        this.markKeep(symbolDraw);
 +
 +        symbolDraw.group.silent = mpModel.get('silent') || seriesModel.get('silent');
 +    }
 +}
 +
 +function createList(
 +    coordSys: CoordinateSystem,
 +    seriesModel: SeriesModel,
 +    mpModel: MarkPointModel
 +) {
 +    let coordDimsInfos;
 +    if (coordSys) {
 +        coordDimsInfos = map(coordSys && coordSys.dimensions, function (coordDim) {
 +            const info = seriesModel.getData().getDimensionInfo(
 +                seriesModel.getData().mapDimension(coordDim)
 +            ) || {};
 +            // In map series data don't have lng and lat dimension. Fallback to same with coordSys
 +            return defaults({name: coordDim}, info);
 +        });
 +    }
 +    else {
 +        coordDimsInfos = [{
 +            name: 'value',
 +            type: 'float'
 +        }];
 +    }
 +
 +    const mpData = new List(coordDimsInfos, mpModel);
 +    let dataOpt = map(mpModel.get('data'), curry(
 +            markerHelper.dataTransform, seriesModel
 +        ));
 +    if (coordSys) {
 +        dataOpt = filter(
 +            dataOpt, curry(markerHelper.dataFilter, coordSys)
 +        );
 +    }
 +
 +    mpData.initData(dataOpt, null,
 +        coordSys ? markerHelper.dimValueGetter : function (item: MarkPointDataItemOption) {
 +            return item.value;
 +        }
 +    );
 +
 +    return mpData;
 +}
 +
 +ComponentView.registerClass(MarkPointView);
diff --cc src/component/timeline/SliderTimelineView.ts
index b382e5a,0000000..0df2fff
mode 100644,000000..100644
--- a/src/component/timeline/SliderTimelineView.ts
+++ b/src/component/timeline/SliderTimelineView.ts
@@@ -1,893 -1,0 +1,900 @@@
 +/*
 +* 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 BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect';
 +import * as matrix from 'zrender/src/core/matrix';
 +import * as graphic from '../../util/graphic';
 +import { createTextStyle } from '../../label/labelStyle';
 +import * as layout from '../../util/layout';
 +import TimelineView from './TimelineView';
 +import TimelineAxis from './TimelineAxis';
 +import {createSymbol} from '../../util/symbol';
 +import * as numberUtil from '../../util/number';
 +import GlobalModel from '../../model/Global';
 +import ExtensionAPI from '../../ExtensionAPI';
 +import { merge, each, extend, clone, isString, bind, defaults, retrieve2 } from 'zrender/src/core/util';
 +import SliderTimelineModel from './SliderTimelineModel';
 +import ComponentView from '../../view/Component';
 +import { LayoutOrient, ZRTextAlign, ZRTextVerticalAlign, ZRElementEvent, ScaleTick } from '../../util/types';
 +import TimelineModel, { TimelineDataItemOption, TimelineCheckpointStyle } from './TimelineModel';
 +import { TimelineChangePayload, TimelinePlayChangePayload } from './timelineAction';
 +import Model from '../../model/Model';
 +import { PathProps, PathStyleProps } from 'zrender/src/graphic/Path';
 +import Scale from '../../scale/Scale';
 +import OrdinalScale from '../../scale/Ordinal';
 +import TimeScale from '../../scale/Time';
 +import IntervalScale from '../../scale/Interval';
 +import { VectorArray } from 'zrender/src/core/vector';
 +import { parsePercent } from 'zrender/src/contain/text';
 +import { makeInner } from '../../util/model';
 +import { getECData } from '../../util/innerStore';
 +import { enableHoverEmphasis } from '../../util/states';
 +import { createTooltipMarkup } from '../tooltip/tooltipMarkup';
++import Displayable from 'zrender/src/graphic/Displayable';
 +
 +const PI = Math.PI;
 +
 +type TimelineSymbol = ReturnType<typeof createSymbol>;
 +
 +type RenderMethodName = '_renderAxisLine' | '_renderAxisTick' | '_renderControl' | '_renderCurrentPointer';
 +
 +type ControlName = 'play' | 'stop' | 'next' | 'prev';
 +type ControlIconName = 'playIcon' | 'stopIcon' | 'nextIcon' | 'prevIcon';
 +
 +const labelDataIndexStore = makeInner<{
 +    dataIndex: number
 +}, graphic.Text>();
 +
 +interface LayoutInfo {
 +    viewRect: BoundingRect
 +    mainLength: number
 +    orient: LayoutOrient
 +
 +    rotation: number
 +    labelRotation: number
 +    labelPosOpt: number | '+' | '-'
 +    labelAlign: ZRTextAlign
 +    labelBaseline: ZRTextVerticalAlign
 +
 +    playPosition: number[]
 +    prevBtnPosition: number[]
 +    nextBtnPosition: number[]
 +    axisExtent: number[]
 +
 +    controlSize: number
 +    controlGap: number
 +}
 +
 +class SliderTimelineView extends TimelineView {
 +
 +    static type = 'timeline.slider';
 +    type = SliderTimelineView.type;
 +
 +    api: ExtensionAPI;
 +    model: SliderTimelineModel;
 +    ecModel: GlobalModel;
 +
 +    private _axis: TimelineAxis;
 +
 +    private _viewRect: BoundingRect;
 +
 +    private _timer: number;
 +
 +    private _currentPointer: TimelineSymbol;
 +
 +    private _progressLine: graphic.Line;
 +
 +    private _mainGroup: graphic.Group;
 +
 +    private _labelGroup: graphic.Group;
 +
 +    private _tickSymbols: graphic.Path[];
 +    private _tickLabels: graphic.Text[];
 +
 +    init(ecModel: GlobalModel, api: ExtensionAPI) {
 +        this.api = api;
 +    }
 +
 +    /**
 +     * @override
 +     */
 +    render(timelineModel: SliderTimelineModel, ecModel: GlobalModel, api: ExtensionAPI) {
 +        this.model = timelineModel;
 +        this.api = api;
 +        this.ecModel = ecModel;
 +
 +        this.group.removeAll();
 +
 +        if (timelineModel.get('show', true)) {
 +
 +            const layoutInfo = this._layout(timelineModel, api);
 +            const mainGroup = this._createGroup('_mainGroup');
 +            const labelGroup = this._createGroup('_labelGroup');
 +
 +            const axis = this._axis = this._createAxis(layoutInfo, timelineModel);
 +
 +            timelineModel.formatTooltip = function (dataIndex: number) {
 +                const name = axis.scale.getLabel({value: dataIndex});
 +                return createTooltipMarkup('nameValue', { noName: true, value: name });
 +            };
 +
 +            each(
 +                ['AxisLine', 'AxisTick', 'Control', 'CurrentPointer'] as const,
 +                function (name) {
 +                    this['_render' + name as RenderMethodName](layoutInfo, mainGroup, axis, timelineModel);
 +                },
 +                this
 +            );
 +
 +            this._renderAxisLabel(layoutInfo, labelGroup, axis, timelineModel);
 +            this._position(layoutInfo, timelineModel);
 +        }
 +
 +        this._doPlayStop();
 +
 +        this._updateTicksStatus();
 +    }
 +
 +    /**
 +     * @override
 +     */
 +    remove() {
 +        this._clearTimer();
 +        this.group.removeAll();
 +    }
 +
 +    /**
 +     * @override
 +     */
 +    dispose() {
 +        this._clearTimer();
 +    }
 +
 +    private _layout(timelineModel: SliderTimelineModel, api: ExtensionAPI): LayoutInfo {
 +        const labelPosOpt = timelineModel.get(['label', 'position']);
 +        const orient = timelineModel.get('orient');
 +        const viewRect = getViewRect(timelineModel, api);
 +        let parsedLabelPos: number | '+' | '-';
 +        // Auto label offset.
 +        if (labelPosOpt == null || labelPosOpt === 'auto') {
 +            parsedLabelPos = orient === 'horizontal'
 +                ? ((viewRect.y + viewRect.height / 2) < api.getHeight() / 2 ? '-' : '+')
 +                : ((viewRect.x + viewRect.width / 2) < api.getWidth() / 2 ? '+' : '-');
 +        }
 +        else if (isString(labelPosOpt)) {
 +            parsedLabelPos = ({
 +                horizontal: {top: '-', bottom: '+'},
 +                vertical: {left: '-', right: '+'}
 +            } as const)[orient][labelPosOpt];
 +        }
 +        else {
 +            // is number
 +            parsedLabelPos = labelPosOpt;
 +        }
 +
 +        const labelAlignMap = {
 +            horizontal: 'center',
 +            vertical: (parsedLabelPos >= 0 || parsedLabelPos === '+') ? 'left' : 'right'
 +        };
 +
 +        const labelBaselineMap = {
 +            horizontal: (parsedLabelPos >= 0 || parsedLabelPos === '+') ? 'top' : 'bottom',
 +            vertical: 'middle'
 +        };
 +        const rotationMap = {
 +            horizontal: 0,
 +            vertical: PI / 2
 +        };
 +
 +        // Position
 +        const mainLength = orient === 'vertical' ? viewRect.height : viewRect.width;
 +
 +        const controlModel = timelineModel.getModel('controlStyle');
 +        const showControl = controlModel.get('show', true);
 +        const controlSize = showControl ? controlModel.get('itemSize') : 0;
 +        const controlGap = showControl ? controlModel.get('itemGap') : 0;
 +        const sizePlusGap = controlSize + controlGap;
 +
 +        // Special label rotate.
 +        let labelRotation = timelineModel.get(['label', 'rotate']) || 0;
 +        labelRotation = labelRotation * PI / 180; // To radian.
 +
 +        let playPosition: number[];
 +        let prevBtnPosition: number[];
 +        let nextBtnPosition: number[];
 +        const controlPosition = controlModel.get('position', true);
 +        const showPlayBtn = showControl && controlModel.get('showPlayBtn', true);
 +        const showPrevBtn = showControl && controlModel.get('showPrevBtn', true);
 +        const showNextBtn = showControl && controlModel.get('showNextBtn', true);
 +        let xLeft = 0;
 +        let xRight = mainLength;
 +
 +        // position[0] means left, position[1] means middle.
 +        if (controlPosition === 'left' || controlPosition === 'bottom') {
 +            showPlayBtn && (playPosition = [0, 0], xLeft += sizePlusGap);
 +            showPrevBtn && (prevBtnPosition = [xLeft, 0], xLeft += sizePlusGap);
 +            showNextBtn && (nextBtnPosition = [xRight - controlSize, 0], xRight -= sizePlusGap);
 +        }
 +        else { // 'top' 'right'
 +            showPlayBtn && (playPosition = [xRight - controlSize, 0], xRight -= sizePlusGap);
 +            showPrevBtn && (prevBtnPosition = [0, 0], xLeft += sizePlusGap);
 +            showNextBtn && (nextBtnPosition = [xRight - controlSize, 0], xRight -= sizePlusGap);
 +        }
 +        const axisExtent = [xLeft, xRight];
 +
 +        if (timelineModel.get('inverse')) {
 +            axisExtent.reverse();
 +        }
 +
 +        return {
 +            viewRect: viewRect,
 +            mainLength: mainLength,
 +            orient: orient,
 +
 +            rotation: rotationMap[orient],
 +            labelRotation: labelRotation,
 +            labelPosOpt: parsedLabelPos,
 +            labelAlign: timelineModel.get(['label', 'align']) || labelAlignMap[orient] as ZRTextAlign,
 +            labelBaseline: timelineModel.get(['label', 'verticalAlign'])
 +                || timelineModel.get(['label', 'baseline'])
 +                || labelBaselineMap[orient] as ZRTextVerticalAlign,
 +
 +            // Based on mainGroup.
 +            playPosition: playPosition,
 +            prevBtnPosition: prevBtnPosition,
 +            nextBtnPosition: nextBtnPosition,
 +            axisExtent: axisExtent,
 +
 +            controlSize: controlSize,
 +            controlGap: controlGap
 +        };
 +    }
 +
 +    private _position(layoutInfo: LayoutInfo, timelineModel: SliderTimelineModel) {
 +        // Position is be called finally, because bounding rect is needed for
 +        // adapt content to fill viewRect (auto adapt offset).
 +
 +        // Timeline may be not all in the viewRect when 'offset' is specified
 +        // as a number, because it is more appropriate that label aligns at
 +        // 'offset' but not the other edge defined by viewRect.
 +
 +        const mainGroup = this._mainGroup;
 +        const labelGroup = this._labelGroup;
 +
 +        let viewRect = layoutInfo.viewRect;
 +        if (layoutInfo.orient === 'vertical') {
 +            // transform to horizontal, inverse rotate by left-top point.
 +            const m = matrix.create();
 +            const rotateOriginX = viewRect.x;
 +            const rotateOriginY = viewRect.y + viewRect.height;
 +            matrix.translate(m, m, [-rotateOriginX, -rotateOriginY]);
 +            matrix.rotate(m, m, -PI / 2);
 +            matrix.translate(m, m, [rotateOriginX, rotateOriginY]);
 +            viewRect = viewRect.clone();
 +            viewRect.applyTransform(m);
 +        }
 +
 +        const viewBound = getBound(viewRect);
 +        const mainBound = getBound(mainGroup.getBoundingRect());
 +        const labelBound = getBound(labelGroup.getBoundingRect());
 +
 +        const mainPosition = [mainGroup.x, mainGroup.y];
 +        const labelsPosition = [labelGroup.x, labelGroup.y];
 +
 +        labelsPosition[0] = mainPosition[0] = viewBound[0][0];
 +
 +        const labelPosOpt = layoutInfo.labelPosOpt;
 +
 +        if (labelPosOpt == null || isString(labelPosOpt)) { // '+' or '-'
 +            const mainBoundIdx = labelPosOpt === '+' ? 0 : 1;
 +            toBound(mainPosition, mainBound, viewBound, 1, mainBoundIdx);
 +            toBound(labelsPosition, labelBound, viewBound, 1, 1 - mainBoundIdx);
 +        }
 +        else {
 +            const mainBoundIdx = labelPosOpt >= 0 ? 0 : 1;
 +            toBound(mainPosition, mainBound, viewBound, 1, mainBoundIdx);
 +            labelsPosition[1] = mainPosition[1] + labelPosOpt;
 +        }
 +
 +        mainGroup.setPosition(mainPosition);
 +        labelGroup.setPosition(labelsPosition);
 +        mainGroup.rotation = labelGroup.rotation = layoutInfo.rotation;
 +
 +        setOrigin(mainGroup);
 +        setOrigin(labelGroup);
 +
 +        function setOrigin(targetGroup: graphic.Group) {
 +            targetGroup.originX = viewBound[0][0] - targetGroup.x;
 +            targetGroup.originY = viewBound[1][0] - targetGroup.y;
 +        }
 +
 +        function getBound(rect: RectLike) {
 +            // [[xmin, xmax], [ymin, ymax]]
 +            return [
 +                [rect.x, rect.x + rect.width],
 +                [rect.y, rect.y + rect.height]
 +            ];
 +        }
 +
 +        function toBound(fromPos: VectorArray, from: number[][], to: number[][], dimIdx: number, boundIdx: number) {
 +            fromPos[dimIdx] += to[dimIdx][boundIdx] - from[dimIdx][boundIdx];
 +        }
 +    }
 +
 +    private _createAxis(layoutInfo: LayoutInfo, timelineModel: SliderTimelineModel) {
 +        const data = timelineModel.getData();
 +        const axisType = timelineModel.get('axisType');
 +
 +        const scale = createScaleByModel(timelineModel, axisType);
 +
 +        // Customize scale. The `tickValue` is `dataIndex`.
 +        scale.getTicks = function () {
 +            return data.mapArray(['value'], function (value: number) {
 +                return {value};
 +            });
 +        };
 +
 +        const dataExtent = data.getDataExtent('value');
 +        scale.setExtent(dataExtent[0], dataExtent[1]);
 +        scale.niceTicks();
 +
 +        const axis = new TimelineAxis('value', scale, layoutInfo.axisExtent as [number, number], axisType);
 +        axis.model = timelineModel;
 +
 +        return axis;
 +    }
 +
 +    private _createGroup(key: '_mainGroup' | '_labelGroup') {
 +        const newGroup = this[key] = new graphic.Group();
 +        this.group.add(newGroup);
 +        return newGroup;
 +    }
 +
 +    private _renderAxisLine(
 +        layoutInfo: LayoutInfo,
 +        group: graphic.Group,
 +        axis: TimelineAxis,
 +        timelineModel: SliderTimelineModel
 +    ) {
 +        const axisExtent = axis.getExtent();
 +
 +        if (!timelineModel.get(['lineStyle', 'show'])) {
 +            return;
 +        }
 +
 +        const line = new graphic.Line({
 +            shape: {
 +                x1: axisExtent[0], y1: 0,
 +                x2: axisExtent[1], y2: 0
 +            },
 +            style: extend(
 +                {lineCap: 'round'},
 +                timelineModel.getModel('lineStyle').getLineStyle()
 +            ),
 +            silent: true,
 +            z2: 1
 +        });
 +        group.add(line);
 +
 +        const progressLine = this._progressLine = new graphic.Line({
 +            shape: {
 +                x1: axisExtent[0],
 +                x2: this._currentPointer
 +                    ? this._currentPointer.x : axisExtent[0],
 +                y1: 0, y2: 0
 +            },
 +            style: defaults(
 +                { lineCap: 'round', lineWidth: line.style.lineWidth } as PathStyleProps,
 +                timelineModel.getModel(['progress', 'lineStyle']).getLineStyle()
 +            ),
 +            silent: true,
 +            z2: 1
 +        });
 +        group.add(progressLine);
 +    }
 +
 +    private _renderAxisTick(
 +        layoutInfo: LayoutInfo,
 +        group: graphic.Group,
 +        axis: TimelineAxis,
 +        timelineModel: SliderTimelineModel
 +    ) {
 +        const data = timelineModel.getData();
 +        // Show all ticks, despite ignoring strategy.
 +        const ticks = axis.scale.getTicks();
 +
 +        this._tickSymbols = [];
 +
 +        // The value is dataIndex, see the costomized scale.
 +        each(ticks, (tick: ScaleTick) => {
 +            const tickCoord = axis.dataToCoord(tick.value);
 +            const itemModel = data.getItemModel<TimelineDataItemOption>(tick.value);
 +            const itemStyleModel = itemModel.getModel('itemStyle');
 +            const hoverStyleModel = itemModel.getModel(['emphasis', 'itemStyle']);
 +            const progressStyleModel = itemModel.getModel(['progress', 'itemStyle']);
 +
 +            const symbolOpt = {
 +                position: [tickCoord, 0],
 +                onclick: bind(this._changeTimeline, this, tick.value)
 +            };
 +            const el = giveSymbol(itemModel, itemStyleModel, group, symbolOpt);
 +            el.ensureState('emphasis').style = hoverStyleModel.getItemStyle();
 +            el.ensureState('progress').style = progressStyleModel.getItemStyle();
 +
 +            enableHoverEmphasis(el);
 +
 +            const ecData = getECData(el);
 +            if (itemModel.get('tooltip')) {
 +                ecData.dataIndex = tick.value;
 +                ecData.dataModel = timelineModel;
 +            }
 +            else {
 +                ecData.dataIndex = ecData.dataModel = null;
 +            }
 +
 +            this._tickSymbols.push(el);
 +        });
 +    }
 +
 +    private _renderAxisLabel(
 +        layoutInfo: LayoutInfo,
 +        group: graphic.Group,
 +        axis: TimelineAxis,
 +        timelineModel: SliderTimelineModel
 +    ) {
 +        const labelModel = axis.getLabelModel();
 +
 +        if (!labelModel.get('show')) {
 +            return;
 +        }
 +
 +        const data = timelineModel.getData();
 +        const labels = axis.getViewLabels();
 +
 +        this._tickLabels = [];
 +
 +        each(labels, (labelItem) => {
 +            // The tickValue is dataIndex, see the costomized scale.
 +            const dataIndex = labelItem.tickValue;
 +
 +            const itemModel = data.getItemModel<TimelineDataItemOption>(dataIndex);
 +            const normalLabelModel = itemModel.getModel('label');
 +            const hoverLabelModel = itemModel.getModel(['emphasis', 'label']);
 +            const progressLabelModel = itemModel.getModel(['progress', 'label']);
 +
 +            const tickCoord = axis.dataToCoord(labelItem.tickValue);
 +            const textEl = new graphic.Text({
 +                x: tickCoord,
 +                y: 0,
 +                rotation: layoutInfo.labelRotation - layoutInfo.rotation,
 +                onclick: bind(this._changeTimeline, this, dataIndex),
 +                silent: false,
 +                style: createTextStyle(normalLabelModel, {
 +                    text: labelItem.formattedLabel,
 +                    align: layoutInfo.labelAlign,
 +                    verticalAlign: layoutInfo.labelBaseline
 +                })
 +            });
 +
 +            textEl.ensureState('emphasis').style = createTextStyle(hoverLabelModel);
 +            textEl.ensureState('progress').style = createTextStyle(progressLabelModel);
 +
 +            group.add(textEl);
 +            enableHoverEmphasis(textEl);
 +
 +            labelDataIndexStore(textEl).dataIndex = dataIndex;
 +
 +            this._tickLabels.push(textEl);
 +
 +        });
 +    }
 +
 +    private _renderControl(
 +        layoutInfo: LayoutInfo,
 +        group: graphic.Group,
 +        axis: TimelineAxis,
 +        timelineModel: SliderTimelineModel
 +    ) {
 +        const controlSize = layoutInfo.controlSize;
 +        const rotation = layoutInfo.rotation;
 +
 +        const itemStyle = timelineModel.getModel('controlStyle').getItemStyle();
 +        const hoverStyle = timelineModel.getModel(['emphasis', 'controlStyle']).getItemStyle();
 +        const playState = timelineModel.getPlayState();
 +        const inverse = timelineModel.get('inverse', true);
 +
 +        makeBtn(
 +            layoutInfo.nextBtnPosition,
 +            'next',
 +            bind(this._changeTimeline, this, inverse ? '-' : '+')
 +        );
 +        makeBtn(
 +            layoutInfo.prevBtnPosition,
 +            'prev',
 +            bind(this._changeTimeline, this, inverse ? '+' : '-')
 +        );
 +        makeBtn(
 +            layoutInfo.playPosition,
 +            (playState ? 'stop' : 'play'),
 +            bind(this._handlePlayClick, this, !playState),
 +            true
 +        );
 +
 +        function makeBtn(
 +            position: number[],
 +            iconName: ControlName,
 +            onclick: () => void,
 +            willRotate?: boolean
 +        ) {
 +            if (!position) {
 +                return;
 +            }
 +            const iconSize = parsePercent(
 +                retrieve2(timelineModel.get(['controlStyle', iconName + 'BtnSize' as any]), controlSize),
 +                controlSize
 +            );
 +            const rect = [0, -iconSize / 2, iconSize, iconSize];
 +            const opt = {
 +                position: position,
 +                origin: [controlSize / 2, 0],
 +                rotation: willRotate ? -rotation : 0,
 +                rectHover: true,
 +                style: itemStyle,
 +                onclick: onclick
 +            };
 +            const btn = makeControlIcon(timelineModel, iconName + 'Icon' as ControlIconName, rect, opt);
 +            btn.ensureState('emphasis').style = hoverStyle;
 +            group.add(btn);
 +            enableHoverEmphasis(btn);
 +        }
 +    }
 +
 +    private _renderCurrentPointer(
 +        layoutInfo: LayoutInfo,
 +        group: graphic.Group,
 +        axis: TimelineAxis,
 +        timelineModel: SliderTimelineModel
 +    ) {
 +        const data = timelineModel.getData();
 +        const currentIndex = timelineModel.getCurrentIndex();
 +        const pointerModel = data.getItemModel<TimelineDataItemOption>(currentIndex)
 +            .getModel('checkpointStyle');
 +        const me = this;
 +
 +        const callback = {
 +            onCreate(pointer: TimelineSymbol) {
 +                pointer.draggable = true;
 +                pointer.drift = bind(me._handlePointerDrag, me);
 +                pointer.ondragend = bind(me._handlePointerDragend, me);
 +                pointerMoveTo(pointer, me._progressLine, currentIndex, axis, timelineModel, true);
 +            },
 +            onUpdate(pointer: TimelineSymbol) {
 +                pointerMoveTo(pointer, me._progressLine, currentIndex, axis, timelineModel);
 +            }
 +        };
 +
 +        // Reuse when exists, for animation and drag.
 +        this._currentPointer = giveSymbol(
 +            pointerModel, pointerModel, this._mainGroup, {}, this._currentPointer, callback
 +        );
 +    }
 +
 +    private _handlePlayClick(nextState: boolean) {
 +        this._clearTimer();
 +        this.api.dispatchAction({
 +            type: 'timelinePlayChange',
 +            playState: nextState,
 +            from: this.uid
 +        } as TimelinePlayChangePayload);
 +    }
 +
 +    private _handlePointerDrag(dx: number, dy: number, e: ZRElementEvent) {
 +        this._clearTimer();
 +        this._pointerChangeTimeline([e.offsetX, e.offsetY]);
 +    }
 +
 +    private _handlePointerDragend(e: ZRElementEvent) {
 +        this._pointerChangeTimeline([e.offsetX, e.offsetY], true);
 +    }
 +
 +    private _pointerChangeTimeline(mousePos: number[], trigger?: boolean) {
 +        let toCoord = this._toAxisCoord(mousePos)[0];
 +
 +        const axis = this._axis;
 +        const axisExtent = numberUtil.asc(axis.getExtent().slice());
 +
 +        toCoord > axisExtent[1] && (toCoord = axisExtent[1]);
 +        toCoord < axisExtent[0] && (toCoord = axisExtent[0]);
 +
 +        this._currentPointer.x = toCoord;
 +        this._currentPointer.markRedraw();
 +
 +        this._progressLine.shape.x2 = toCoord;
 +        this._progressLine.dirty();
 +
 +        const targetDataIndex = this._findNearestTick(toCoord);
 +        const timelineModel = this.model;
 +
 +        if (trigger || (
 +            targetDataIndex !== timelineModel.getCurrentIndex()
 +            && timelineModel.get('realtime')
 +        )) {
 +            this._changeTimeline(targetDataIndex);
 +        }
 +    }
 +
 +    private _doPlayStop() {
 +        this._clearTimer();
 +
 +        if (this.model.getPlayState()) {
 +            this._timer = setTimeout(
 +                () => {
 +                    // Do not cache
 +                    const timelineModel = this.model;
 +                    this._changeTimeline(
 +                        timelineModel.getCurrentIndex()
 +                        + (timelineModel.get('rewind', true) ? -1 : 1)
 +                    );
 +                },
 +                this.model.get('playInterval')
 +            ) as any;
... 10170 lines suppressed ...


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