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/03/18 12:14:03 UTC

[incubator-echarts] branch typescript updated (704c1d1 -> ede3ac4)

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

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


    from 704c1d1  ts: bug fixes
     add b1657f3  test: reduce screenshots when mousewheel in replay
     add 91ee3b5  Merge pull request #12177 from apache/visual-test-improve-replay
     add 5d93759  fix(radar): optimize ticks calculation when min/max is not defined.
     add c935811  test: add radar axis test
     add 1e54495  Merge pull request #12180 from apache/fix-radar-axis
     add f5a8a35  fix: fix treemap throw brought by a9ee949d5172c3283e7de8400f59f35bfa448705
     add a6489e5  Merge pull request #12186 from apache/fix/treemap-highdown
     add 09e14f6  fix: keep the original effect: graph line label has distance to its line.
     add ca4c391  Merge pull request #12194 from apache/fix/graph-line-label
     add 879b895  fix: fix stacked bar not drawn because of NaN value brought in #11951
     add 3ae8187  release: build 4.7.0
     add 4f3748d  update dep to zrender@4.3.0
     new ede3ac4  Merge branch 'release' into typescript

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 dist/echarts-en.common.js        |  984 +++++++++++++++++--------
 dist/echarts-en.common.min.js    |    2 +-
 dist/echarts-en.js               | 1477 +++++++++++++++++++++++++++-----------
 dist/echarts-en.js.map           |    2 +-
 dist/echarts-en.min.js           |    2 +-
 dist/echarts-en.simple.js        |  721 +++++++++++++------
 dist/echarts-en.simple.min.js    |    2 +-
 dist/echarts.common.js           |  984 +++++++++++++++++--------
 dist/echarts.common.min.js       |    2 +-
 dist/echarts.js                  | 1477 +++++++++++++++++++++++++++-----------
 dist/echarts.js.map              |    2 +-
 dist/echarts.min.js              |    2 +-
 dist/echarts.simple.js           |  721 +++++++++++++------
 dist/echarts.simple.min.js       |    2 +-
 package-lock.json                |    8 +-
 package.json                     |    4 +-
 src/chart/bar/BarView.ts         |    1 +
 src/chart/graph/GraphSeries.ts   |    3 +-
 src/chart/treemap/TreemapView.ts |    4 +-
 src/layout/barGrid.ts            |   10 +-
 test/bar.html                    |    2 +-
 test/radar-axis.html             |  113 +++
 test/runTest/Timeline.js         |   18 +-
 test/ut/core/extendExpect.js     |   20 +
 24 files changed, 4684 insertions(+), 1879 deletions(-)
 create mode 100644 test/radar-axis.html


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


[incubator-echarts] 01/01: Merge branch 'release' into typescript

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

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

commit ede3ac4821f79c3bc658ed6bbb50d70b40204c33
Merge: 704c1d1 4f3748d
Author: pissang <bm...@gmail.com>
AuthorDate: Wed Mar 18 20:13:25 2020 +0800

    Merge branch 'release' into typescript
    
    # Conflicts:
    #	src/coord/radar/Radar.js
    #	src/echarts.js

 dist/echarts-en.common.js        |  984 +++++++++++++++++--------
 dist/echarts-en.common.min.js    |    2 +-
 dist/echarts-en.js               | 1477 +++++++++++++++++++++++++++-----------
 dist/echarts-en.js.map           |    2 +-
 dist/echarts-en.min.js           |    2 +-
 dist/echarts-en.simple.js        |  721 +++++++++++++------
 dist/echarts-en.simple.min.js    |    2 +-
 dist/echarts.common.js           |  984 +++++++++++++++++--------
 dist/echarts.common.min.js       |    2 +-
 dist/echarts.js                  | 1477 +++++++++++++++++++++++++++-----------
 dist/echarts.js.map              |    2 +-
 dist/echarts.min.js              |    2 +-
 dist/echarts.simple.js           |  721 +++++++++++++------
 dist/echarts.simple.min.js       |    2 +-
 package-lock.json                |    8 +-
 package.json                     |    4 +-
 src/chart/bar/BarView.ts         |    1 +
 src/chart/graph/GraphSeries.ts   |    3 +-
 src/chart/treemap/TreemapView.ts |    4 +-
 src/layout/barGrid.ts            |   10 +-
 test/bar.html                    |    2 +-
 test/radar-axis.html             |  113 +++
 test/runTest/Timeline.js         |   18 +-
 test/ut/core/extendExpect.js     |   20 +
 24 files changed, 4684 insertions(+), 1879 deletions(-)

diff --cc package-lock.json
index 94dc284,071ad60..ebb71d9
--- a/package-lock.json
+++ b/package-lock.json
@@@ -8408,22 -7264,10 +8408,22 @@@
        "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=",
        "dev": true
      },
 +    "z-schema": {
 +      "version": "3.18.4",
 +      "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.18.4.tgz",
 +      "integrity": "sha512-DUOKC/IhbkdLKKiV89gw9DUauTV8U/8yJl1sjf6MtDmzevLKOF2duNJ495S3MFVjqZarr+qNGCPbkg4mu4PpLw==",
 +      "dev": true,
 +      "requires": {
 +        "commander": "^2.7.1",
 +        "lodash.get": "^4.0.0",
 +        "lodash.isequal": "^4.0.0",
 +        "validator": "^8.0.0"
 +      }
 +    },
      "zrender": {
-       "version": "4.2.0",
-       "resolved": "https://registry.npmjs.org/zrender/-/zrender-4.2.0.tgz",
-       "integrity": "sha512-YJ9hxt5uFincYYU3KK31+Ce+B6PJmYYK0Q9fQ6jOUAoC/VHbe4kCKAPkxKeT7jGTxrK5wYu18R0TLGqj2zbEOA=="
+       "version": "4.3.0",
+       "resolved": "https://registry.npmjs.org/zrender/-/zrender-4.3.0.tgz",
+       "integrity": "sha512-Dii6j2bDsPkxQayuVf2DXJeruIB/mKVxxcGRZQ9GExiBd4c3w7+oBuvo1O/JGHeFeA1nCmSDVDs/S7yKZG1nrA=="
      }
    }
  }
diff --cc src/chart/bar/BarView.ts
index 602bc5c,0000000..7515478
mode 100644,000000..100644
--- a/src/chart/bar/BarView.ts
+++ b/src/chart/bar/BarView.ts
@@@ -1,784 -1,0 +1,785 @@@
 +/*
 +* 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 {__DEV__} from '../../config';
 +import * as zrUtil from 'zrender/src/core/util';
 +import {Rect, Sector, getECData, updateProps, initProps, setHoverStyle} from '../../util/graphic';
 +import {setLabel} from './helper';
 +import {getBarItemStyle} from './barItemStyle';
 +import Path, { PathProps } from 'zrender/src/graphic/Path';
 +import Group from 'zrender/src/container/Group';
 +import {throttle} from '../../util/throttle';
 +import {createClipPath} from '../helper/createClipPathFromCoordSys';
 +import Sausage from '../../util/shape/sausage';
 +import ChartView from '../../view/Chart';
 +import List from '../../data/List';
 +import GlobalModel from '../../model/Global';
 +import ExtensionAPI from '../../ExtensionAPI';
 +import { StageHandlerProgressParams, ZRElementEvent } 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 { RectLike } from 'zrender/src/core/BoundingRect';
 +import type Model from '../../model/Model';
 +import { isCoordinateSystemType } from '../../coord/CoordinateSystem';
 +
 +const BAR_BORDER_WIDTH_QUERY = ['itemStyle', 'borderWidth'] 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
 +
 +
 +function getClipArea(coord: CoordSysOfBar, data: List) {
 +    if (isCoordinateSystemType<Cartesian2D>(coord, 'cartesian2d')) {
 +        var coordSysClipArea = coord.getArea && coord.getArea();
 +        var 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) {
 +            var expandWidth = data.getLayout('bandWidth');
 +            if (baseAxis.isHorizontal()) {
 +                coordSysClipArea.x -= expandWidth;
 +                coordSysClipArea.width += expandWidth * 2;
 +            }
 +            else {
 +                coordSysClipArea.y -= expandWidth;
 +                coordSysClipArea.height += expandWidth * 2;
 +            }
 +        }
 +    }
 +
 +    return coordSysClipArea;
 +}
 +
 +
 +class BarView extends ChartView {
 +    static type = 'bar' as const
 +    type = BarView.type
 +
 +    _data: List
 +
 +    _isLargeDraw: boolean
 +
 +    _backgroundGroup: Group
 +
 +    _backgroundEls: (Rect | Sector)[]
 +
 +    render(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) {
 +        this._updateDrawMode(seriesModel);
 +
 +        var coordinateSystemType = seriesModel.get('coordinateSystem');
 +
 +        if (coordinateSystemType === 'cartesian2d'
 +            || coordinateSystemType === 'polar'
 +        ) {
 +            this._isLargeDraw
 +                ? this._renderLarge(seriesModel, ecModel, api)
 +                : this._renderNormal(seriesModel, ecModel, api);
 +        }
 +        else if (__DEV__) {
 +            console.warn('Only cartesian2d and polar supported for bar.');
 +        }
 +
 +        return this.group;
 +    }
 +
 +    incrementalPrepareRender(seriesModel: BarSeriesModel) {
 +        this._clear();
 +        this._updateDrawMode(seriesModel);
 +    }
 +
 +    incrementalRender(
 +        params: StageHandlerProgressParams, seriesModel: BarSeriesModel) {
 +        // Do not support progressive in normal mode.
 +        this._incrementalRenderLarge(params, seriesModel);
 +    }
 +
 +    _updateDrawMode(seriesModel: BarSeriesModel) {
 +        var isLargeDraw = seriesModel.pipelineContext.large;
 +        if (this._isLargeDraw == null || isLargeDraw !== this._isLargeDraw) {
 +            this._isLargeDraw = isLargeDraw;
 +            this._clear();
 +        }
 +    }
 +
 +    _renderNormal(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) {
 +        var group = this.group;
 +        var data = seriesModel.getData();
 +        var oldData = this._data;
 +
 +        var coord = seriesModel.coordinateSystem;
 +        var baseAxis = coord.getBaseAxis();
 +        var isHorizontalOrRadial: boolean;
 +
 +        if (coord.type === 'cartesian2d') {
 +            isHorizontalOrRadial = (baseAxis as Axis2D).isHorizontal();
 +        }
 +        else if (coord.type === 'polar') {
 +            isHorizontalOrRadial = baseAxis.dim === 'angle';
 +        }
 +
 +        var animationModel = seriesModel.isAnimationEnabled() ? seriesModel : null;
 +
 +        var needsClip = seriesModel.get('clip', true);
 +        var 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.
 +
 +        var roundCap = seriesModel.get('roundCap', true);
 +
 +        var drawBackground = seriesModel.get('showBackground', true);
 +        var backgroundModel = seriesModel.getModel('backgroundStyle');
 +
 +        var bgEls: BarView['_backgroundEls'] = [];
 +        var oldBgEls = this._backgroundEls;
 +
 +        data.diff(oldData)
 +            .add(function (dataIndex) {
 +                var itemModel = data.getItemModel(dataIndex);
 +                var layout = getLayout[coord.type](data, dataIndex, itemModel);
 +
 +                if (drawBackground) {
 +                    var bgEl = createBackgroundEl(
 +                        coord, isHorizontalOrRadial, layout
 +                    );
 +                    bgEl.useStyle(getBarItemStyle(backgroundModel));
 +                    bgEls[dataIndex] = bgEl;
 +                }
 +
++                // If dataZoom in filteMode: 'empty', the baseValue can be set as NaN in "axisProxy".
 +                if (!data.hasValue(dataIndex)) {
 +                    return;
 +                }
 +
 +                if (needsClip) {
 +                    // Clip will modify the layout params.
 +                    // And return a boolean to determine if the shape are fully clipped.
 +                    var isClipped = clip[coord.type](coordSysClipArea, layout);
 +                    if (isClipped) {
 +                        group.remove(el);
 +                        return;
 +                    }
 +                }
 +
 +                var el = elementCreator[coord.type](
 +                    dataIndex, layout, isHorizontalOrRadial, animationModel, false, roundCap
 +                );
 +                data.setItemGraphicEl(dataIndex, el);
 +                group.add(el);
 +
 +                updateStyle(
 +                    el, data, dataIndex, itemModel, layout,
 +                    seriesModel, isHorizontalOrRadial, coord.type === 'polar'
 +                );
 +            })
 +            .update(function (newIndex, oldIndex) {
 +                var itemModel = data.getItemModel(newIndex);
 +                var layout = getLayout[coord.type](data, newIndex, itemModel);
 +
 +                if (drawBackground) {
 +                    var bgEl = oldBgEls[oldIndex];
 +                    bgEl.useStyle(getBarItemStyle(backgroundModel));
 +                    bgEls[newIndex] = bgEl;
 +
 +                    var shape = createBackgroundShape(isHorizontalOrRadial, layout, coord);
 +                    updateProps(
 +                        bgEl as Path, { shape: shape }, animationModel, newIndex
 +                    );
 +                }
 +
 +                var el = oldData.getItemGraphicEl(oldIndex) as BarPossiblePath;
 +                if (!data.hasValue(newIndex)) {
 +                    group.remove(el);
 +                    return;
 +                }
 +
 +                if (needsClip) {
 +                    var isClipped = clip[coord.type](coordSysClipArea, layout);
 +                    if (isClipped) {
 +                        group.remove(el);
 +                        return;
 +                    }
 +                }
 +
 +                if (el) {
 +                    updateProps(el as Path, {
 +                        shape: layout
 +                    }, animationModel, newIndex);
 +                }
 +                else {
 +                    el = elementCreator[coord.type](
 +                        newIndex, layout, isHorizontalOrRadial, animationModel, true, roundCap
 +                    );
 +                }
 +
 +                data.setItemGraphicEl(newIndex, el);
 +                // Add back
 +                group.add(el);
 +
 +                updateStyle(
 +                    el, data, newIndex, itemModel, layout,
 +                    seriesModel, isHorizontalOrRadial, coord.type === 'polar'
 +                );
 +            })
 +            .remove(function (dataIndex) {
 +                var el = oldData.getItemGraphicEl(dataIndex);
 +                if (coord.type === 'cartesian2d') {
 +                    el && removeRect(dataIndex, animationModel, el as Rect);
 +                }
 +                else {
 +                    el && removeSector(dataIndex, animationModel, el as Sector);
 +                }
 +            })
 +            .execute();
 +
 +        var bgGroup = this._backgroundGroup || (this._backgroundGroup = new Group());
 +        bgGroup.removeAll();
 +
 +        for (var i = 0; i < bgEls.length; ++i) {
 +            bgGroup.add(bgEls[i]);
 +        }
 +        group.add(bgGroup);
 +        this._backgroundEls = bgEls;
 +
 +        this._data = data;
 +    }
 +
 +    _renderLarge(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) {
 +        this._clear();
 +        createLarge(seriesModel, this.group);
 +
 +        // Use clipPath in large mode.
 +        var clipPath = seriesModel.get('clip', true)
 +            ? createClipPath(seriesModel.coordinateSystem, false, seriesModel)
 +            : null;
 +        if (clipPath) {
 +            this.group.setClipPath(clipPath);
 +        }
 +        else {
 +            this.group.removeClipPath();
 +        }
 +    }
 +
 +    _incrementalRenderLarge(params: StageHandlerProgressParams, seriesModel: BarSeriesModel) {
 +        this._removeBackground();
 +        createLarge(seriesModel, this.group, true);
 +    }
 +
 +    remove(ecModel?: GlobalModel) {
 +        this._clear(ecModel);
 +    }
 +
 +    _clear(ecModel?: GlobalModel) {
 +        var group = this.group;
 +        var data = this._data;
 +        if (ecModel && ecModel.get('animation') && data && !this._isLargeDraw) {
 +            this._removeBackground();
 +            this._backgroundEls = [];
 +
 +            data.eachItemGraphicEl(function (el: Sector | Rect) {
 +                if (el.type === 'sector') {
 +                    removeSector(getECData(el).dataIndex, ecModel, el as (Sector));
 +                }
 +                else {
 +                    removeRect(getECData(el).dataIndex, ecModel, el as (Rect));
 +                }
 +            });
 +        }
 +        else {
 +            group.removeAll();
 +        }
 +        this._data = null;
 +    }
 +
 +    _removeBackground() {
 +        this.group.remove(this._backgroundGroup);
 +        this._backgroundGroup = null;
 +    }
 +}
 +
 +interface Clipper {
 +    (coordSysBoundingRect: RectLike, layout: RectLayout | SectorLayout): boolean
 +}
 +var clip: {
 +    [key in 'cartesian2d' | 'polar']: Clipper
 +} = {
 +    cartesian2d(coordSysBoundingRect: RectLike, layout: Rect['shape']) {
 +        var signWidth = layout.width < 0 ? -1 : 1;
 +        var 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;
 +        }
 +
 +        var x = mathMax(layout.x, coordSysBoundingRect.x);
 +        var x2 = mathMin(layout.x + layout.width, coordSysBoundingRect.x + coordSysBoundingRect.width);
 +        var y = mathMax(layout.y, coordSysBoundingRect.y);
 +        var y2 = mathMin(layout.y + layout.height, coordSysBoundingRect.y + coordSysBoundingRect.height);
 +
 +        layout.x = x;
 +        layout.y = y;
 +        layout.width = x2 - x;
 +        layout.height = y2 - y;
 +
 +        var 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;
 +    }
 +};
 +
 +interface ElementCreator {
 +    (
 +        dataIndex: number, layout: RectLayout | SectorLayout, isHorizontalOrRadial: boolean,
 +        animationModel: BarSeriesModel, isUpdate: boolean, roundCap?: boolean
 +    ): BarPossiblePath
 +}
 +
 +var elementCreator: {
 +    [key in 'polar' | 'cartesian2d']: ElementCreator
 +} = {
 +
 +    cartesian2d(
 +        dataIndex, layout: RectLayout, isHorizontal,
 +        animationModel, isUpdate
 +    ) {
 +        var rect = new Rect({
 +            shape: zrUtil.extend({}, layout),
 +            z2: 1
 +        });
 +
 +        rect.name = 'item';
 +
 +        // Animation
 +        if (animationModel) {
 +            var rectShape = rect.shape;
 +            var animateProperty = isHorizontal ? 'height' : 'width' as 'width' | 'height';
 +            var animateTarget = {} as RectShape;
 +            rectShape[animateProperty] = 0;
 +            animateTarget[animateProperty] = layout[animateProperty];
 +            (isUpdate ? updateProps : initProps)(rect, {
 +                shape: animateTarget
 +            }, animationModel, dataIndex);
 +        }
 +
 +        return rect;
 +    },
 +
 +    polar(
 +        dataIndex: number, layout: SectorLayout, isRadial: boolean,
 +        animationModel, 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.
 +        var clockwise = layout.startAngle < layout.endAngle;
 +
 +        var ShapeClass = (!isRadial && roundCap) ? Sausage : Sector;
 +
 +        var sector = new ShapeClass({
 +            shape: zrUtil.defaults({clockwise: clockwise}, layout),
 +            z2: 1
 +        });
 +
 +        sector.name = 'item';
 +
 +        // Animation
 +        if (animationModel) {
 +            var sectorShape = sector.shape;
 +            var animateProperty = isRadial ? 'r' : 'endAngle' as 'r' | 'endAngle';
 +            var animateTarget = {} as SectorShape;
 +            sectorShape[animateProperty] = isRadial ? 0 : layout.startAngle;
 +            animateTarget[animateProperty] = layout[animateProperty];
 +            (isUpdate ? updateProps : initProps)(sector, {
 +                shape: animateTarget
 +            }, animationModel, dataIndex);
 +        }
 +
 +        return sector;
 +    }
 +};
 +
 +function removeRect(
 +    dataIndex: number,
 +    animationModel: BarSeriesModel | GlobalModel,
 +    el: Rect
 +) {
 +    // Not show text when animating
 +    el.style.text = null;
 +    updateProps(el, {
 +        shape: {
 +            width: 0
 +        }
 +    }, animationModel, dataIndex, function () {
 +        el.parent && el.parent.remove(el);
 +    });
 +}
 +
 +function removeSector(
 +    dataIndex: number,
 +    animationModel: BarSeriesModel | GlobalModel,
 +    el: Sector
 +) {
 +    // Not show text when animating
 +    el.style.text = null;
 +    updateProps(el, {
 +        shape: {
 +            r: el.shape.r0
 +        }
 +    }, animationModel, dataIndex, function () {
 +        el.parent && el.parent.remove(el);
 +    });
 +}
 +
 +interface GetLayout {
 +    (data: List, dataIndex: number, itemModel: Model<BarDataItemOption>): RectLayout | SectorLayout
 +}
 +var getLayout: {
 +    [key in 'cartesian2d' | 'polar']: GetLayout
 +} = {
 +    cartesian2d(data, dataIndex, itemModel): RectLayout {
 +        var layout = data.getItemLayout(dataIndex) as RectLayout;
 +        var fixedLineWidth = getLineWidth(itemModel, layout);
 +
 +        // fix layout with lineWidth
 +        var signX = layout.width > 0 ? 1 : -1;
 +        var 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 {
 +        var 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
 +) {
 +    var color = data.getItemVisual(dataIndex, 'color');
 +    var opacity = data.getItemVisual(dataIndex, 'opacity');
 +    var stroke = data.getVisual('borderColor');
 +    var itemStyleModel = itemModel.getModel('itemStyle');
 +    var hoverStyle = getBarItemStyle(itemModel.getModel(['emphasis', 'itemStyle']));
 +
 +    if (!isPolar) {
 +        el.setShape('r', itemStyleModel.get('barBorderRadius') || 0);
 +    }
 +
 +    el.useStyle(zrUtil.defaults(
 +        {
 +            stroke: isZeroOnPolar(layout as SectorLayout) ? 'none' : stroke,
 +            fill: isZeroOnPolar(layout as SectorLayout) ? 'none' : color,
 +            opacity: opacity
 +        },
 +        getBarItemStyle(itemStyleModel)
 +    ));
 +
 +    var cursorStyle = itemModel.getShallow('cursor');
 +    cursorStyle && el.attr('cursor', cursorStyle);
 +
 +    if (!isPolar) {
 +        var labelPositionOutside = isHorizontal
 +            ? ((layout as RectLayout).height > 0 ? 'bottom' : 'top')
 +            : ((layout as RectLayout).width > 0 ? 'left' : 'right');
 +
 +        setLabel(
 +            el.style, hoverStyle, itemModel, color,
 +            seriesModel, dataIndex, labelPositionOutside
 +        );
 +    }
 +    if (isZeroOnPolar(layout as SectorLayout)) {
 +        hoverStyle.fill = hoverStyle.stroke = 'none';
 +    }
 +    setHoverStyle(el, hoverStyle);
 +}
 +
 +// In case width or height are too small.
 +function getLineWidth(
 +    itemModel: Model<BarSeriesOption>,
 +    rawLayout: RectLayout
 +) {
 +    var lineWidth = itemModel.get(BAR_BORDER_WIDTH_QUERY) || 0;
 +    // width or height may be NaN for empty data
 +    var width = isNaN(rawLayout.width) ? Number.MAX_VALUE : Math.abs(rawLayout.width);
 +    var 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 {
 +    type = 'largeBar'
 +
 +    shape: LagePathShape
 +
 +    __startPoint: number[]
 +    __baseDimIdx: number
 +    __largeDataIndices: ArrayLike<number>
 +    __barWidth: number
 +
 +    constructor(opts?: LargePathProps) {
 +        super(opts, null, 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 (var 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
 +    var data = seriesModel.getData();
 +    var startPoint = [];
 +    var baseDimIdx = data.getLayout('valueAxisHorizontal') ? 1 : 0;
 +    startPoint[1 - baseDimIdx] = data.getLayout('valueAxisStart');
 +
 +    var largeDataIndices = data.getLayout('largeDataIndices');
 +    var barWidth = data.getLayout('barWidth');
 +
 +    var backgroundModel = seriesModel.getModel('backgroundStyle');
 +    var 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);
 +    }
 +
 +    var 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.
 +var largePathUpdateDataIndex = throttle(function (this: LargePath, event: ZRElementEvent) {
 +    var largePath = this;
 +    var dataIndex = largePathFindDataIndex(largePath, event.offsetX, event.offsetY);
 +    getECData(largePath).dataIndex = dataIndex >= 0 ? dataIndex : null;
 +}, 30, false);
 +
 +function largePathFindDataIndex(largePath: LargePath, x: number, y: number) {
 +    var baseDimIdx = largePath.__baseDimIdx;
 +    var valueDimIdx = 1 - baseDimIdx;
 +    var points = largePath.shape.points;
 +    var largeDataIndices = largePath.__largeDataIndices;
 +    var barWidthHalf = Math.abs(largePath.__barWidth / 2);
 +    var startValueVal = largePath.__startPoint[valueDimIdx];
 +
 +    _eventPos[0] = x;
 +    _eventPos[1] = y;
 +    var pointerBaseVal = _eventPos[baseDimIdx];
 +    var pointerValueVal = _eventPos[1 - baseDimIdx];
 +    var baseLowerBound = pointerBaseVal - barWidthHalf;
 +    var baseUpperBound = pointerBaseVal + barWidthHalf;
 +
 +    for (var i = 0, len = points.length / 2; i < len; i++) {
 +        var ii = i * 2;
 +        var barBaseVal = points[ii + baseDimIdx];
 +        var 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
 +) {
 +    var borderColor = data.getVisual('borderColor') || data.getVisual('color');
 +    var itemStyle = seriesModel.getModel('itemStyle').getItemStyle(['color', 'borderColor']);
 +
 +    el.useStyle(itemStyle);
 +    el.style.fill = null;
 +    el.style.stroke = borderColor;
 +    el.style.lineWidth = data.getLayout('barWidth');
 +}
 +
 +function setLargeBackgroundStyle(
 +    el: LargePath,
 +    backgroundModel: Model<BarSeriesOption['backgroundStyle']>,
 +    data: List
 +) {
 +    var borderColor = backgroundModel.get('borderColor') || backgroundModel.get('color');
 +    var itemStyle = backgroundModel.getItemStyle(['color', 'borderColor']);
 +
 +    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 {
 +    var 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/graph/GraphSeries.ts
index b06353d,0000000..608b08b
mode 100644,000000..100644
--- a/src/chart/graph/GraphSeries.ts
+++ b/src/chart/graph/GraphSeries.ts
@@@ -1,477 -1,0 +1,478 @@@
 +/*
 +* 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 {encodeHTML} from '../../util/format';
 +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
 +} 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';
 +
 +type GraphDataValue = OptionDataValue | OptionDataValue[]
 +
 +interface GraphEdgeLineStyleOption extends LineStyleOption {
 +    curveness: number
 +}
 +export interface GraphNodeItemOption extends SymbolOptionMixin {
 +    id?: string
 +    name?: string
 +    value?: GraphDataValue
 +
 +    itemStyle?: ItemStyleOption
 +    label?: LabelOption
 +
 +    emphasis?: {
 +        itemStyle?: ItemStyleOption
 +        label?: LabelOption
 +    }
 +
 +    /**
 +     * 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
 +
 +    focusNodeAdjacency?: boolean
 +}
 +
 +export interface GraphEdgeItemOption {
 +    /**
 +     * Name or index of source node.
 +     */
 +    source?: string | number
 +    /**
 +     * Name or index of target node.
 +     */
 +    target?: string | number
 +
 +    value?: number
 +
 +    lineStyle?: GraphEdgeLineStyleOption
 +    label?: LabelOption
 +
 +    emphasis?: {
 +        lineStyle?: GraphEdgeLineStyleOption
 +        label?: LabelOption
 +    }
 +
 +    /**
 +     * Symbol of both line ends
 +     */
 +    symbol?: string | string[]
 +
 +    symbolSize?: number | number[]
 +
 +    ignoreForceLayout?: boolean
 +
 +    focusNodeAdjacency?: boolean
 +}
 +
 +export interface GraphCategoryItemOption extends SymbolOptionMixin {
 +    name?: string
 +
 +    value?: OptionDataValue
 +
 +    itemStyle?: ItemStyleOption
 +    label?: LabelOption
 +
 +    emphasis?: {
 +        itemStyle?: ItemStyleOption
 +        label?: LabelOption
 +    }
 +}
 +
 +interface GraphSeriesOption extends SeriesOption,
 +    SeriesOnCartesianOptionMixin, SeriesOnPolarOptionMixin, SeriesOnCalendarOptionMixin,
 +    SeriesOnGeoOptionMixin, SeriesOnSingleOptionMixin,
 +    SymbolOptionMixin,
 +    RoamOptionMixin,
 +    BoxLayoutOptionMixin {
 +
 +    type?: 'graph'
 +
 +    coordinateSystem?: string
 +
 +    hoverAnimation?: boolean
 +    legendHoverLink?: boolean
 +
 +    layout?: 'none' | 'force' | 'circular'
 +
 +    data?: GraphNodeItemOption[]
 +    nodes?: GraphNodeItemOption[]
 +
 +    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?: LabelOption & {
 +        formatter?: LabelFormatterCallback | string
 +    }
 +    label?: LabelOption & {
 +        formatter?: LabelFormatterCallback | string
 +    }
 +
 +    itemStyle?: ItemStyleOption
 +    lineStyle?: GraphEdgeLineStyleOption
 +
 +    emphasis?: {
 +        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
 +
 +    init(option: GraphSeriesOption) {
 +        super.init.apply(this, arguments as any);
 +
 +        var 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) {
 +        var edges = option.edges || option.links || [];
 +        var nodes = option.data || option.nodes || [];
 +        var self = this;
 +
 +        if (nodes && edges) {
 +            return createGraphFromNodeEdge(nodes, edges, this, true, beforeLink).data;
 +        }
 +
 +        function beforeLink(nodeData: List, edgeData: List) {
 +            // Overwrite nodeData.getItemModel to
 +            nodeData.wrapMethod('getItemModel', function (model) {
 +                var categoriesModels = self._categoriesModels;
 +                var categoryIdx = model.getShallow('category');
 +                var categoryModel = categoriesModels[categoryIdx];
 +                if (categoryModel) {
 +                    categoryModel.parentModel = model.parentModel;
 +                    model.parentModel = categoryModel;
 +                }
 +                return model;
 +            });
 +
 +            var edgeLabelModel = self.getModel('edgeLabel');
 +            // For option `edgeLabel` can be found by label.xxx.xxx on item mode.
 +            var fakeSeriesModel = new Model(
 +                {label: edgeLabelModel.option},
 +                edgeLabelModel.parentModel,
 +                ecModel
 +            );
 +            var emphasisEdgeLabelModel = self.getModel(['emphasis', 'edgeLabel']);
 +            var emphasisFakeSeriesModel = new Model(
 +                {emphasis: {label: emphasisEdgeLabelModel.option}},
 +                emphasisEdgeLabelModel.parentModel,
 +                ecModel
 +            );
 +
 +            edgeData.wrapMethod('getItemModel', function (model) {
 +                model.customizeGetParent(edgeGetParent);
 +                return model;
 +            });
 +
 +            function edgeGetParent(this: Model, path: string | string[]) {
 +                const pathArr = this.parsePath(path);
 +                return (pathArr && pathArr[0] === 'label')
 +                    ? fakeSeriesModel
 +                    : (pathArr && pathArr[0] === 'emphasis' && pathArr[1] === 'label')
 +                    ? emphasisFakeSeriesModel
 +                    : this.parentModel;
 +            }
 +        }
 +    }
 +
 +    getGraph(): Graph {
 +        return this.getData().graph;
 +    }
 +
 +    getEdgeData(): List {
 +        return this.getGraph().edgeData;
 +    }
 +
 +    getCategoriesData(): List {
 +        return this._categoriesData;
 +    }
 +
 +    /**
 +     * @override
 +     */
 +    formatTooltip(dataIndex: number, multipleSeries: boolean, dataType: string) {
 +        if (dataType === 'edge') {
 +            var nodeData = this.getData();
 +            var params = this.getDataParams(dataIndex, dataType);
 +            var edge = nodeData.graph.getEdgeByIndex(dataIndex);
 +            var sourceName = nodeData.getName(edge.node1.dataIndex);
 +            var targetName = nodeData.getName(edge.node2.dataIndex);
 +
 +            var html = [];
 +            sourceName != null && html.push(sourceName);
 +            targetName != null && html.push(targetName);
 +            var htmlStr = encodeHTML(html.join(' > '));
 +
 +            if (params.value) {
 +                htmlStr += ' : ' + encodeHTML(params.value);
 +            }
 +            return htmlStr;
 +        }
 +        else { // dataType === 'node' or empty
 +            return super.formatTooltip.apply(this, arguments as any);
 +        }
 +    }
 +
 +    _updateCategoriesData() {
 +        var categories = zrUtil.map(this.option.categories || [], function (category) {
 +            // Data must has value
 +            return category.value != null ? category : zrUtil.extend({
 +                value: 0
 +            }, category);
 +        });
 +        var 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,
 +
 +        hoverAnimation: 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'
++            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: {
 +            label: {
 +                show: true
 +            }
 +        }
 +    }
 +}
 +
 +SeriesModel.registerClass(GraphSeriesModel);
 +
 +export default GraphSeriesModel;
diff --cc src/chart/treemap/TreemapView.ts
index cfcd2e5,0000000..c7129ca
mode 100644,000000..100644
--- a/src/chart/treemap/TreemapView.ts
+++ b/src/chart/treemap/TreemapView.ts
@@@ -1,1064 -1,0 +1,1066 @@@
 +/*
 +* 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 {bind, each, indexOf, curry, extend, retrieve, clone} from 'zrender/src/core/util';
 +import * as graphic from '../../util/graphic';
 +import DataDiffer from '../../data/DataDiffer';
 +import * as helper from '../helper/treeHelper';
 +import Breadcrumb from './Breadcrumb';
 +import RoamController, { RoamEventParams } from '../../component/helper/RoamController';
 +import BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect';
 +import * as matrix from 'zrender/src/core/matrix';
 +import * as animationUtil from '../../util/animation';
 +import makeStyleMapper from '../../model/mixin/makeStyleMapper';
 +import ChartView from '../../view/Chart';
 +import Tree, { TreeNode } from '../../data/Tree';
 +import TreemapSeriesModel, { TreemapSeriesNodeItemOption } from './TreemapSeries';
 +import GlobalModel from '../../model/Global';
 +import ExtensionAPI from '../../ExtensionAPI';
 +import Model from '../../model/Model';
 +import { LayoutRect } from '../../util/layout';
 +import { TreemapLayoutNode } from './treemapLayout';
 +import Element from 'zrender/src/Element';
 +import Displayable from 'zrender/src/graphic/Displayable';
 +import { makeInner } from '../../util/model';
 +import { PathProps } from 'zrender/src/graphic/Path';
 +import { TreeSeriesNodeItemOption } from '../tree/TreeSeries';
 +import {
 +    TreemapRootToNodePayload,
 +    TreemapMovePayload,
 +    TreemapRenderPayload,
 +    TreemapZoomToNodePayload
 +} from './treemapAction';
 +import { StyleProps } from 'zrender/src/graphic/Style';
 +import { ColorString } from '../../util/types';
 +
 +const Group = graphic.Group;
 +const Rect = graphic.Rect;
 +
 +const DRAG_THRESHOLD = 3;
 +const PATH_LABEL_NOAMAL = ['label'] as const;
 +const PATH_LABEL_EMPHASIS = ['emphasis', 'label'] as const;
 +const PATH_UPPERLABEL_NORMAL = ['upperLabel'] as const;
 +const PATH_UPPERLABEL_EMPHASIS = ['emphasis', 'upperLabel'] as const;
 +const Z_BASE = 10; // Should bigger than every z.
 +const Z_BG = 1;
 +const Z_CONTENT = 2;
 +
 +const getItemStyleEmphasis = makeStyleMapper([
 +    ['fill', 'color'],
 +    // `borderColor` and `borderWidth` has been occupied,
 +    // so use `stroke` to indicate the stroke of the rect.
 +    ['stroke', 'strokeColor'],
 +    ['lineWidth', 'strokeWidth'],
 +    ['shadowBlur'],
 +    ['shadowOffsetX'],
 +    ['shadowOffsetY'],
 +    ['shadowColor']
 +]);
 +const getItemStyleNormal = function (model: Model<TreemapSeriesNodeItemOption['itemStyle']>): StyleProps {
 +    // Normal style props should include emphasis style props.
 +    var itemStyle = getItemStyleEmphasis(model) as StyleProps;
 +    // Clear styles set by emphasis.
 +    itemStyle.stroke = itemStyle.fill = itemStyle.lineWidth = null;
 +    return itemStyle;
 +};
 +
 +interface RenderElementStorage {
 +    nodeGroup: graphic.Group[]
 +    background: graphic.Rect[]
 +    content: graphic.Rect[]
 +}
 +
 +type LastCfgStorage = {
 +    [key in keyof RenderElementStorage]: LastCfg[]
 +    // nodeGroup: {
 +    //     old: Pick<graphic.Group, 'position'>[]
 +    //     fadein: boolean
 +    // }[]
 +    // background: {
 +    //     old: Pick<graphic.Rect, 'shape'>
 +    //     fadein: boolean
 +    // }[]
 +    // content: {
 +    //     old: Pick<graphic.Rect, 'shape'>
 +    //     fadein: boolean
 +    // }[]
 +}
 +
 +interface FoundTargetInfo {
 +    node: TreeNode
 +
 +    offsetX?: number
 +    offsetY?: number
 +}
 +
 +interface RenderResult {
 +    lastsForAnimation: LastCfgStorage
 +    willInvisibleEls?: graphic.Rect[]
 +    willDeleteEls: RenderElementStorage
 +    renderFinally: () => void
 +}
 +
 +interface ReRoot {
 +    rootNodeGroup: graphic.Group
 +    direction: 'drillDown' | 'rollUp'
 +}
 +
 +interface LastCfg {
 +    oldPos?: graphic.Group['position']
 +    oldShape?: graphic.Rect['shape']
 +    fadein: boolean
 +}
 +
 +const inner = makeInner<{
 +    nodeWidth: number
 +    nodeHeight: number
 +    willDelete: boolean
 +}, Element>();
 +
 +class TreemapView extends ChartView {
 +
 +    static type = 'treemap'
 +    type = TreemapView.type
 +
 +    private _containerGroup: graphic.Group
 +    private _breadcrumb: Breadcrumb
 +    private _controller: RoamController
 +
 +    private _oldTree: Tree
 +
 +    private _state: 'ready' | 'animating' = 'ready'
 +
 +    private _storage = createStorage() as RenderElementStorage;
 +
 +    seriesModel: TreemapSeriesModel
 +    api: ExtensionAPI
 +    ecModel: GlobalModel
 +
 +    /**
 +     * @override
 +     */
 +    render(
 +        seriesModel: TreemapSeriesModel,
 +        ecModel: GlobalModel,
 +        api: ExtensionAPI,
 +        payload: TreemapZoomToNodePayload | TreemapRenderPayload | TreemapMovePayload | TreemapRootToNodePayload
 +    ) {
 +
 +        var models = ecModel.findComponents({
 +            mainType: 'series', subType: 'treemap', query: payload
 +        });
 +        if (indexOf(models, seriesModel) < 0) {
 +            return;
 +        }
 +
 +        this.seriesModel = seriesModel;
 +        this.api = api;
 +        this.ecModel = ecModel;
 +
 +        var types = ['treemapZoomToNode', 'treemapRootToNode'];
 +        var targetInfo = helper
 +            .retrieveTargetInfo(payload, types, seriesModel);
 +        var payloadType = payload && payload.type;
 +        var layoutInfo = seriesModel.layoutInfo;
 +        var isInit = !this._oldTree;
 +        var thisStorage = this._storage;
 +
 +        // Mark new root when action is treemapRootToNode.
 +        var reRoot = (payloadType === 'treemapRootToNode' && targetInfo && thisStorage)
 +            ? {
 +                rootNodeGroup: thisStorage.nodeGroup[targetInfo.node.getRawIndex()],
 +                direction: (payload as TreemapRootToNodePayload).direction
 +            }
 +            : null;
 +
 +        var containerGroup = this._giveContainerGroup(layoutInfo);
 +
 +        var renderResult = this._doRender(containerGroup, seriesModel, reRoot);
 +        (
 +            !isInit && (
 +                !payloadType
 +                || payloadType === 'treemapZoomToNode'
 +                || payloadType === 'treemapRootToNode'
 +            )
 +        )
 +            ? this._doAnimation(containerGroup, renderResult, seriesModel, reRoot)
 +            : renderResult.renderFinally();
 +
 +        this._resetController(api);
 +
 +        this._renderBreadcrumb(seriesModel, api, targetInfo);
 +    }
 +
 +    /**
 +     * @private
 +     */
 +    _giveContainerGroup(layoutInfo: LayoutRect) {
 +        var containerGroup = this._containerGroup;
 +        if (!containerGroup) {
 +            // FIXME
 +            // 加一层containerGroup是为了clip,但是现在clip功能并没有实现。
 +            containerGroup = this._containerGroup = new Group();
 +            this._initEvents(containerGroup);
 +            this.group.add(containerGroup);
 +        }
 +        containerGroup.attr('position', [layoutInfo.x, layoutInfo.y]);
 +
 +        return containerGroup;
 +    }
 +
 +    /**
 +     * @private
 +     */
 +    _doRender(containerGroup: graphic.Group, seriesModel: TreemapSeriesModel, reRoot: ReRoot): RenderResult {
 +        var thisTree = seriesModel.getData().tree;
 +        var oldTree = this._oldTree;
 +
 +        // Clear last shape records.
 +        var lastsForAnimation = createStorage() as LastCfgStorage;
 +        var thisStorage = createStorage() as RenderElementStorage;
 +        var oldStorage = this._storage;
 +        var willInvisibleEls: RenderResult['willInvisibleEls'] = [];
 +
 +        function doRenderNode(thisNode: TreeNode, oldNode: TreeNode, parentGroup: graphic.Group, depth: number) {
 +            return renderNode(
 +                seriesModel,
 +                thisStorage, oldStorage, reRoot,
 +                lastsForAnimation, willInvisibleEls,
 +                thisNode, oldNode, parentGroup, depth
 +            );
 +        }
 +
 +        // Notice: when thisTree and oldTree are the same tree (see list.cloneShallow),
 +        // the oldTree is actually losted, so we can not find all of the old graphic
 +        // elements from tree. So we use this stragegy: make element storage, move
 +        // from old storage to new storage, clear old storage.
 +
 +        dualTravel(
 +            thisTree.root ? [thisTree.root] : [],
 +            (oldTree && oldTree.root) ? [oldTree.root] : [],
 +            containerGroup,
 +            thisTree === oldTree || !oldTree,
 +            0
 +        );
 +
 +        // Process all removing.
 +        var willDeleteEls = clearStorage(oldStorage) as RenderElementStorage;
 +
 +        this._oldTree = thisTree;
 +        this._storage = thisStorage;
 +
 +        return {
 +            lastsForAnimation,
 +            willDeleteEls,
 +            renderFinally
 +        };
 +
 +        function dualTravel(
 +            thisViewChildren: TreemapLayoutNode[],
 +            oldViewChildren: TreemapLayoutNode[],
 +            parentGroup: graphic.Group,
 +            sameTree: boolean,
 +            depth: number
 +        ) {
 +            // When 'render' is triggered by action,
 +            // 'this' and 'old' may be the same tree,
 +            // we use rawIndex in that case.
 +            if (sameTree) {
 +                oldViewChildren = thisViewChildren;
 +                each(thisViewChildren, function (child, index) {
 +                    !child.isRemoved() && processNode(index, index);
 +                });
 +            }
 +            // Diff hierarchically (diff only in each subtree, but not whole).
 +            // because, consistency of view is important.
 +            else {
 +                (new DataDiffer(oldViewChildren, thisViewChildren, getKey, getKey))
 +                    .add(processNode)
 +                    .update(processNode)
 +                    .remove(curry(processNode, null))
 +                    .execute();
 +            }
 +
 +            function getKey(node: TreeNode) {
 +                // Identify by name or raw index.
 +                return node.getId();
 +            }
 +
 +            function processNode(newIndex: number, oldIndex?: number) {
 +                var thisNode = newIndex != null ? thisViewChildren[newIndex] : null;
 +                var oldNode = oldIndex != null ? oldViewChildren[oldIndex] : null;
 +
 +                var group = doRenderNode(thisNode, oldNode, parentGroup, depth);
 +
 +                group && dualTravel(
 +                    thisNode && thisNode.viewChildren || [],
 +                    oldNode && oldNode.viewChildren || [],
 +                    group,
 +                    sameTree,
 +                    depth + 1
 +                );
 +            }
 +        }
 +
 +        function clearStorage(storage: RenderElementStorage) {
 +            var willDeleteEls = createStorage() as RenderElementStorage;
 +            storage && each(storage, function (store, storageName) {
 +                var delEls = willDeleteEls[storageName];
 +                each(store, function (el) {
 +                    el && (delEls.push(el as any), inner(el).willDelete = true);
 +                });
 +            });
 +            return willDeleteEls;
 +        }
 +
 +        function renderFinally() {
 +            each(willDeleteEls, function (els) {
 +                each(els, function (el) {
 +                    el.parent && el.parent.remove(el);
 +                });
 +            });
 +            each(willInvisibleEls, function (el) {
 +                el.invisible = true;
 +                // Setting invisible is for optimizing, so no need to set dirty,
 +                // just mark as invisible.
 +                el.dirty();
 +            });
 +        }
 +    }
 +
 +    /**
 +     * @private
 +     */
 +    _doAnimation(
 +        containerGroup: graphic.Group,
 +        renderResult: RenderResult,
 +        seriesModel: TreemapSeriesModel,
 +        reRoot: ReRoot
 +    ) {
 +        if (!seriesModel.get('animation')) {
 +            return;
 +        }
 +
 +        var duration = seriesModel.get('animationDurationUpdate');
 +        var easing = seriesModel.get('animationEasing');
 +        var animationWrap = animationUtil.createWrap();
 +
 +        // Make delete animations.
 +        each(renderResult.willDeleteEls, function (store, storageName) {
 +            each(store, function (el, rawIndex) {
 +                if ((el as Displayable).invisible) {
 +                    return;
 +                }
 +
 +                var parent = el.parent; // Always has parent, and parent is nodeGroup.
 +                var target: PathProps;
 +                var innerStore = inner(parent);
 +
 +                if (reRoot && reRoot.direction === 'drillDown') {
 +                    target = parent === reRoot.rootNodeGroup
 +                        // This is the content element of view root.
 +                        // Only `content` will enter this branch, because
 +                        // `background` and `nodeGroup` will not be deleted.
 +                        ? {
 +                            shape: {
 +                                x: 0,
 +                                y: 0,
 +                                width: innerStore.nodeWidth,
 +                                height: innerStore.nodeHeight
 +                            },
 +                            style: {
 +                                opacity: 0
 +                            }
 +                        }
 +                        // Others.
 +                        : {style: {opacity: 0}};
 +                }
 +                else {
 +                    var targetX = 0;
 +                    var targetY = 0;
 +
 +                    if (!innerStore.willDelete) {
 +                        // Let node animate to right-bottom corner, cooperating with fadeout,
 +                        // which is appropriate for user understanding.
 +                        // Divided by 2 for reRoot rolling up effect.
 +                        targetX = innerStore.nodeWidth / 2;
 +                        targetY = innerStore.nodeHeight / 2;
 +                    }
 +
 +                    target = storageName === 'nodeGroup'
 +                        ? {position: [targetX, targetY], style: {opacity: 0}}
 +                        : {
 +                            shape: {x: targetX, y: targetY, width: 0, height: 0},
 +                            style: {opacity: 0}
 +                        };
 +                }
 +                // @ts-ignore
 +                target && animationWrap.add(el, target, duration, easing);
 +            });
 +        });
 +
 +        // Make other animations
 +        each(this._storage, function (store, storageName) {
 +            each(store, function (el, rawIndex) {
 +                var last = renderResult.lastsForAnimation[storageName][rawIndex];
 +                var target: PathProps = {};
 +
 +                if (!last) {
 +                    return;
 +                }
 +
 +                if (el instanceof graphic.Group) {
 +                    if (last.oldPos) {
 +                        target.position = el.position.slice();
 +                        el.attr('position', last.oldPos);
 +                    }
 +                }
 +                else {
 +                    if (last.oldShape) {
 +                        target.shape = extend({}, el.shape);
 +                        el.setShape(last.oldShape);
 +                    }
 +
 +                    if (last.fadein) {
 +                        el.setStyle('opacity', 0);
 +                        target.style = {opacity: 1};
 +                    }
 +                    // When animation is stopped for succedent animation starting,
 +                    // el.style.opacity might not be 1
 +                    else if (el.style.opacity !== 1) {
 +                        target.style = {opacity: 1};
 +                    }
 +                }
 +
 +                // @ts-ignore
 +                animationWrap.add(el, target, duration, easing);
 +            });
 +        }, this);
 +
 +        this._state = 'animating';
 +
 +        animationWrap
 +            .done(bind(function () {
 +                this._state = 'ready';
 +                renderResult.renderFinally();
 +            }, this))
 +            .start();
 +    }
 +
 +    /**
 +     * @private
 +     */
 +    _resetController(api: ExtensionAPI) {
 +        var controller = this._controller;
 +
 +        // Init controller.
 +        if (!controller) {
 +            controller = this._controller = new RoamController(api.getZr());
 +            controller.enable(this.seriesModel.get('roam'));
 +            controller.on('pan', bind(this._onPan, this));
 +            controller.on('zoom', bind(this._onZoom, this));
 +        }
 +
 +        var rect = new BoundingRect(0, 0, api.getWidth(), api.getHeight());
 +        controller.setPointerChecker(function (e, x, y) {
 +            return rect.contain(x, y);
 +        });
 +    }
 +
 +    /**
 +     * @private
 +     */
 +    _clearController() {
 +        var controller = this._controller;
 +        if (controller) {
 +            controller.dispose();
 +            controller = null;
 +        }
 +    }
 +
 +    /**
 +     * @private
 +     */
 +    _onPan(e: RoamEventParams['pan']) {
 +        if (this._state !== 'animating'
 +            && (Math.abs(e.dx) > DRAG_THRESHOLD || Math.abs(e.dy) > DRAG_THRESHOLD)
 +        ) {
 +            // These param must not be cached.
 +            var root = this.seriesModel.getData().tree.root;
 +
 +            if (!root) {
 +                return;
 +            }
 +
 +            var rootLayout = root.getLayout();
 +
 +            if (!rootLayout) {
 +                return;
 +            }
 +
 +            this.api.dispatchAction({
 +                type: 'treemapMove',
 +                from: this.uid,
 +                seriesId: this.seriesModel.id,
 +                rootRect: {
 +                    x: rootLayout.x + e.dx, y: rootLayout.y + e.dy,
 +                    width: rootLayout.width, height: rootLayout.height
 +                }
 +            });
 +        }
 +    }
 +
 +    /**
 +     * @private
 +     */
 +    _onZoom(e: RoamEventParams['zoom']) {
 +        var mouseX = e.originX;
 +        var mouseY = e.originY;
 +
 +        if (this._state !== 'animating') {
 +            // These param must not be cached.
 +            var root = this.seriesModel.getData().tree.root;
 +
 +            if (!root) {
 +                return;
 +            }
 +
 +            var rootLayout = root.getLayout();
 +
 +            if (!rootLayout) {
 +                return;
 +            }
 +
 +            var rect = new BoundingRect(
 +                rootLayout.x, rootLayout.y, rootLayout.width, rootLayout.height
 +            );
 +            var layoutInfo = this.seriesModel.layoutInfo;
 +
 +            // Transform mouse coord from global to containerGroup.
 +            mouseX -= layoutInfo.x;
 +            mouseY -= layoutInfo.y;
 +
 +            // Scale root bounding rect.
 +            var m = matrix.create();
 +            matrix.translate(m, m, [-mouseX, -mouseY]);
 +            matrix.scale(m, m, [e.scale, e.scale]);
 +            matrix.translate(m, m, [mouseX, mouseY]);
 +
 +            rect.applyTransform(m);
 +
 +            this.api.dispatchAction({
 +                type: 'treemapRender',
 +                from: this.uid,
 +                seriesId: this.seriesModel.id,
 +                rootRect: {
 +                    x: rect.x, y: rect.y,
 +                    width: rect.width, height: rect.height
 +                }
 +            });
 +        }
 +    }
 +
 +    /**
 +     * @private
 +     */
 +    _initEvents(containerGroup: graphic.Group) {
 +        containerGroup.on('click', (e) => {
 +            if (this._state !== 'ready') {
 +                return;
 +            }
 +
 +            var nodeClick = this.seriesModel.get('nodeClick', true);
 +
 +            if (!nodeClick) {
 +                return;
 +            }
 +
 +            var targetInfo = this.findTarget(e.offsetX, e.offsetY);
 +
 +            if (!targetInfo) {
 +                return;
 +            }
 +
 +            var node = targetInfo.node;
 +            if (node.getLayout().isLeafRoot) {
 +                this._rootToNode(targetInfo);
 +            }
 +            else {
 +                if (nodeClick === 'zoomToNode') {
 +                    this._zoomToNode(targetInfo);
 +                }
 +                else if (nodeClick === 'link') {
 +                    var itemModel = node.hostTree.data.getItemModel<TreeSeriesNodeItemOption>(node.dataIndex);
 +                    var link = itemModel.get('link', true);
 +                    var linkTarget = itemModel.get('target', true) || 'blank';
 +                    link && window.open(link, linkTarget);
 +                }
 +            }
 +
 +        }, this);
 +    }
 +
 +    /**
 +     * @private
 +     */
 +    _renderBreadcrumb(seriesModel: TreemapSeriesModel, api: ExtensionAPI, targetInfo: FoundTargetInfo) {
 +        if (!targetInfo) {
 +            targetInfo = seriesModel.get('leafDepth', true) != null
 +                ? {node: seriesModel.getViewRoot()}
 +                // FIXME
 +                // better way?
 +                // Find breadcrumb tail on center of containerGroup.
 +                : this.findTarget(api.getWidth() / 2, api.getHeight() / 2);
 +
 +            if (!targetInfo) {
 +                targetInfo = {node: seriesModel.getData().tree.root};
 +            }
 +        }
 +
 +        (this._breadcrumb || (this._breadcrumb = new Breadcrumb(this.group)))
 +            .render(seriesModel, api, targetInfo.node, (node) => {
 +                if (this._state !== 'animating') {
 +                    helper.aboveViewRoot(seriesModel.getViewRoot(), node)
 +                        ? this._rootToNode({node: node})
 +                        : this._zoomToNode({node: node});
 +                }
 +            });
 +    }
 +
 +    /**
 +     * @override
 +     */
 +    remove() {
 +        this._clearController();
 +        this._containerGroup && this._containerGroup.removeAll();
 +        this._storage = createStorage() as RenderElementStorage;
 +        this._state = 'ready';
 +        this._breadcrumb && this._breadcrumb.remove();
 +    }
 +
 +    dispose() {
 +        this._clearController();
 +    }
 +
 +    /**
 +     * @private
 +     */
 +    _zoomToNode(targetInfo: FoundTargetInfo) {
 +        this.api.dispatchAction({
 +            type: 'treemapZoomToNode',
 +            from: this.uid,
 +            seriesId: this.seriesModel.id,
 +            targetNode: targetInfo.node
 +        });
 +    }
 +
 +    /**
 +     * @private
 +     */
 +    _rootToNode(targetInfo: FoundTargetInfo) {
 +        this.api.dispatchAction({
 +            type: 'treemapRootToNode',
 +            from: this.uid,
 +            seriesId: this.seriesModel.id,
 +            targetNode: targetInfo.node
 +        });
 +    }
 +
 +    /**
 +     * @public
 +     * @param {number} x Global coord x.
 +     * @param {number} y Global coord y.
 +     * @return {Object} info If not found, return undefined;
 +     * @return {number} info.node Target node.
 +     * @return {number} info.offsetX x refer to target node.
 +     * @return {number} info.offsetY y refer to target node.
 +     */
 +    findTarget(x: number, y: number): FoundTargetInfo {
 +        var targetInfo;
 +        var viewRoot = this.seriesModel.getViewRoot();
 +
 +        viewRoot.eachNode({attr: 'viewChildren', order: 'preorder'}, function (node) {
 +            var bgEl = this._storage.background[node.getRawIndex()];
 +            // If invisible, there might be no element.
 +            if (bgEl) {
 +                var point = bgEl.transformCoordToLocal(x, y);
 +                var shape = bgEl.shape;
 +
 +                // For performance consideration, dont use 'getBoundingRect'.
 +                if (shape.x <= point[0]
 +                    && point[0] <= shape.x + shape.width
 +                    && shape.y <= point[1]
 +                    && point[1] <= shape.y + shape.height
 +                ) {
 +                    targetInfo = {
 +                        node: node,
 +                        offsetX: point[0],
 +                        offsetY: point[1]
 +                    };
 +                }
 +                else {
 +                    return false; // Suppress visit subtree.
 +                }
 +            }
 +        }, this);
 +
 +        return targetInfo;
 +    }
 +}
 +
 +/**
 + * @inner
 + */
 +function createStorage(): RenderElementStorage | LastCfgStorage {
 +    return {
 +        nodeGroup: [],
 +        background: [],
 +        content: []
 +    };
 +}
 +
 +/**
 + * @inner
 + * @return Return undefined means do not travel further.
 + */
 +function renderNode(
 +    seriesModel: TreemapSeriesModel,
 +    thisStorage: RenderElementStorage,
 +    oldStorage: RenderElementStorage,
 +    reRoot: ReRoot,
 +    lastsForAnimation: RenderResult['lastsForAnimation'],
 +    willInvisibleEls: RenderResult['willInvisibleEls'],
 +    thisNode: TreeNode,
 +    oldNode: TreeNode,
 +    parentGroup: graphic.Group,
 +    depth: number
 +) {
 +    // Whether under viewRoot.
 +    if (!thisNode) {
 +        // Deleting nodes will be performed finally. This method just find
 +        // element from old storage, or create new element, set them to new
 +        // storage, and set styles.
 +        return;
 +    }
 +
 +    // -------------------------------------------------------------------
 +    // Start of closure variables available in "Procedures in renderNode".
 +
 +    var thisLayout = thisNode.getLayout();
 +    var data = seriesModel.getData();
 +    var nodeModel = thisNode.getModel<TreemapSeriesNodeItemOption>();
 +
 +    // Only for enabling highlight/downplay. Clear firstly.
 +    // Because some node will not be rendered.
 +    data.setItemGraphicEl(thisNode.dataIndex, null);
 +
 +    if (!thisLayout || !thisLayout.isInView) {
 +        return;
 +    }
 +
 +    var thisWidth = thisLayout.width;
 +    var thisHeight = thisLayout.height;
 +    var borderWidth = thisLayout.borderWidth;
 +    var thisInvisible = thisLayout.invisible;
 +
 +    var thisRawIndex = thisNode.getRawIndex();
 +    var oldRawIndex = oldNode && oldNode.getRawIndex();
 +
 +    var thisViewChildren = thisNode.viewChildren;
 +    var upperHeight = thisLayout.upperHeight;
 +    var isParent = thisViewChildren && thisViewChildren.length;
 +    var itemStyleNormalModel = nodeModel.getModel('itemStyle');
 +    var itemStyleEmphasisModel = nodeModel.getModel(['emphasis', 'itemStyle']);
 +
 +    // End of closure ariables available in "Procedures in renderNode".
 +    // -----------------------------------------------------------------
 +
 +    // Node group
 +    var group = giveGraphic('nodeGroup', Group);
 +
 +    if (!group) {
 +        return;
 +    }
 +
 +    parentGroup.add(group);
 +    // x,y are not set when el is above view root.
 +    group.attr('position', [thisLayout.x || 0, thisLayout.y || 0]);
 +    inner(group).nodeWidth = thisWidth;
 +    inner(group).nodeHeight = thisHeight;
 +
 +    if (thisLayout.isAboveViewRoot) {
 +        return group;
 +    }
 +
 +    // Background
 +    var bg = giveGraphic('background', Rect, depth, Z_BG);
 +    bg && renderBackground(group, bg, isParent && thisLayout.upperHeight);
 +
 +    // No children, render content.
 +    if (isParent) {
 +        // Because of the implementation about "traverse" in graphic hover style, we
 +        // can not set hover listener on the "group" of non-leaf node. Otherwise the
 +        // hover event from the descendents will be listenered.
 +        if (graphic.isHighDownDispatcher(group)) {
 +            graphic.setAsHighDownDispatcher(group, false);
 +        }
 +        if (bg) {
 +            graphic.setAsHighDownDispatcher(bg, true);
 +            // Only for enabling highlight/downplay.
 +            data.setItemGraphicEl(thisNode.dataIndex, bg);
 +        }
 +    }
 +    else {
 +        var content = giveGraphic('content', Rect, depth, Z_CONTENT);
 +        content && renderContent(group, content);
 +
 +        if (bg && graphic.isHighDownDispatcher(bg)) {
 +            graphic.setAsHighDownDispatcher(bg, false);
 +        }
 +        graphic.setAsHighDownDispatcher(group, true);
 +        // Only for enabling highlight/downplay.
 +        data.setItemGraphicEl(thisNode.dataIndex, group);
 +    }
 +
 +    return group;
 +
 +    // ----------------------------
 +    // | Procedures in renderNode |
 +    // ----------------------------
 +
 +    function renderBackground(group: graphic.Group, bg: graphic.Rect, useUpperLabel: boolean) {
 +        let ecData = graphic.getECData(bg);
 +        // For tooltip.
 +        ecData.dataIndex = thisNode.dataIndex;
 +        ecData.seriesIndex = seriesModel.seriesIndex;
 +
 +        bg.setShape({x: 0, y: 0, width: thisWidth, height: thisHeight});
 +
 +        if (thisInvisible) {
 +            // If invisible, do not set visual, otherwise the element will
 +            // change immediately before animation. We think it is OK to
 +            // remain its origin color when moving out of the view window.
-             processInvisible(content);
++            processInvisible(bg);
 +        }
 +        else {
++            bg.invisible = false;
 +            var visualBorderColor = thisNode.getVisual('borderColor', true);
 +            var emphasisBorderColor = itemStyleEmphasisModel.get('borderColor');
 +            var normalStyle = getItemStyleNormal(itemStyleNormalModel);
 +            normalStyle.fill = visualBorderColor;
 +            var emphasisStyle = getItemStyleEmphasis(itemStyleEmphasisModel);
 +            emphasisStyle.fill = emphasisBorderColor;
 +
 +            if (useUpperLabel) {
 +                var upperLabelWidth = thisWidth - 2 * borderWidth;
 +
 +                prepareText(
 +                    normalStyle, emphasisStyle, visualBorderColor, upperLabelWidth, upperHeight,
 +                    {x: borderWidth, y: 0, width: upperLabelWidth, height: upperHeight}
 +                );
 +            }
 +            // For old bg.
 +            else {
 +                normalStyle.text = emphasisStyle.text = null;
 +            }
 +
 +            bg.setStyle(normalStyle);
 +            graphic.setElementHoverStyle(bg, emphasisStyle);
 +        }
 +
 +        group.add(bg);
 +    }
 +
 +    function renderContent(group: graphic.Group, content: graphic.Rect) {
 +        let ecData = graphic.getECData(content);
 +        // For tooltip.
 +        ecData.dataIndex = thisNode.dataIndex;
 +        ecData.seriesIndex = seriesModel.seriesIndex;
 +
 +        var contentWidth = Math.max(thisWidth - 2 * borderWidth, 0);
 +        var contentHeight = Math.max(thisHeight - 2 * borderWidth, 0);
 +
 +        content.culling = true;
 +        content.setShape({
 +            x: borderWidth,
 +            y: borderWidth,
 +            width: contentWidth,
 +            height: contentHeight
 +        });
 +
 +        if (thisInvisible) {
 +            // If invisible, do not set visual, otherwise the element will
 +            // change immediately before animation. We think it is OK to
 +            // remain its origin color when moving out of the view window.
 +            processInvisible(content);
 +        }
 +        else {
++            content.invisible = false;
 +            var visualColor = thisNode.getVisual('color', true);
 +            var normalStyle = getItemStyleNormal(itemStyleNormalModel);
 +            normalStyle.fill = visualColor;
 +            var emphasisStyle = getItemStyleEmphasis(itemStyleEmphasisModel);
 +
 +            prepareText(normalStyle, emphasisStyle, visualColor, contentWidth, contentHeight);
 +
 +            content.setStyle(normalStyle);
 +            graphic.setElementHoverStyle(content, emphasisStyle);
 +        }
 +
 +        group.add(content);
 +    }
 +
 +    function processInvisible(element: graphic.Rect) {
 +        // Delay invisible setting utill animation finished,
 +        // avoid element vanish suddenly before animation.
 +        !element.invisible && willInvisibleEls.push(element);
 +    }
 +
 +    function prepareText(
 +        normalStyle: StyleProps,
 +        emphasisStyle: StyleProps,
 +        visualColor: ColorString,
 +        width: number,
 +        height: number,
 +        upperLabelRect?: RectLike
 +    ) {
 +        var text = retrieve(
 +            seriesModel.getFormattedLabel(
 +                thisNode.dataIndex, 'normal', null, null, upperLabelRect ? 'upperLabel' : 'label'
 +            ),
 +            nodeModel.get('name')
 +        );
 +        if (!upperLabelRect && thisLayout.isLeafRoot) {
 +            var iconChar = seriesModel.get('drillDownIcon', true);
 +            text = iconChar ? iconChar + ' ' + text : text;
 +        }
 +
 +        var normalLabelModel = nodeModel.getModel(
 +            upperLabelRect ? PATH_UPPERLABEL_NORMAL : PATH_LABEL_NOAMAL
 +        );
 +        var emphasisLabelModel = nodeModel.getModel(
 +            upperLabelRect ? PATH_UPPERLABEL_EMPHASIS : PATH_LABEL_EMPHASIS
 +        );
 +
 +        var isShow = normalLabelModel.getShallow('show');
 +
 +        graphic.setLabelStyle(
 +            normalStyle, emphasisStyle, normalLabelModel, emphasisLabelModel,
 +            {
 +                defaultText: isShow ? text : null,
 +                autoColor: visualColor,
 +                isRectText: true
 +            }
 +        );
 +
 +        upperLabelRect && (normalStyle.textRect = clone(upperLabelRect));
 +
 +        normalStyle.truncate = (isShow && normalLabelModel.get('ellipsis'))
 +            ? {
 +                outerWidth: width,
 +                outerHeight: height,
 +                minChar: 2
 +            }
 +            : null;
 +    }
 +
 +    function giveGraphic<T extends graphic.Group | graphic.Rect>(
 +        storageName: keyof RenderElementStorage,
 +        Ctor: {new(): T},
 +        depth?: number,
 +        z?: number
 +    ): T {
 +        var element = oldRawIndex != null && oldStorage[storageName][oldRawIndex];
 +        var lasts = lastsForAnimation[storageName];
 +
 +        if (element) {
 +            // Remove from oldStorage
 +            oldStorage[storageName][oldRawIndex] = null;
 +            prepareAnimationWhenHasOld(lasts, element);
 +        }
 +        // If invisible and no old element, do not create new element (for optimizing).
 +        else if (!thisInvisible) {
 +            element = new Ctor();
 +            if (element instanceof Displayable) {
 +                element.z = calculateZ(depth, z);
 +            }
 +            prepareAnimationWhenNoOld(lasts, element);
 +        }
 +
 +        // Set to thisStorage
 +        return (thisStorage[storageName][thisRawIndex] = element) as T;
 +    }
 +
 +    function prepareAnimationWhenHasOld(lasts: LastCfg[], element: graphic.Group | graphic.Rect) {
 +        var lastCfg = lasts[thisRawIndex] = {} as LastCfg;
 +        if (element instanceof Group) {
 +            lastCfg.oldPos = element.position.slice();
 +        }
 +        else {
 +            lastCfg.oldShape = extend({}, element.shape);
 +        }
 +    }
 +
 +    // If a element is new, we need to find the animation start point carefully,
 +    // otherwise it will looks strange when 'zoomToNode'.
 +    function prepareAnimationWhenNoOld(lasts: LastCfg[], element: graphic.Group | graphic.Rect) {
 +        var lastCfg = lasts[thisRawIndex] = {} as LastCfg;
 +        var parentNode = thisNode.parentNode;
 +        var isGroup = element instanceof graphic.Group;
 +
 +        if (parentNode && (!reRoot || reRoot.direction === 'drillDown')) {
 +            var parentOldX = 0;
 +            var parentOldY = 0;
 +
 +            // New nodes appear from right-bottom corner in 'zoomToNode' animation.
 +            // For convenience, get old bounding rect from background.
 +            var parentOldBg = lastsForAnimation.background[parentNode.getRawIndex()];
 +            if (!reRoot && parentOldBg && parentOldBg.oldShape) {
 +                parentOldX = parentOldBg.oldShape.width;
 +                parentOldY = parentOldBg.oldShape.height;
 +            }
 +
 +            // When no parent old shape found, its parent is new too,
 +            // so we can just use {x:0, y:0}.
 +            if (isGroup) {
 +                lastCfg.oldPos = [0, parentOldY];
 +            }
 +            else {
 +                lastCfg.oldShape = {x: parentOldX, y: parentOldY, width: 0, height: 0};
 +            }
 +        }
 +
 +        // Fade in, user can be aware that these nodes are new.
 +        lastCfg.fadein = !isGroup;
 +    }
 +
 +}
 +
 +// We can not set all backgroud with the same z, Because the behaviour of
 +// drill down and roll up differ background creation sequence from tree
 +// hierarchy sequence, which cause that lowser background element overlap
 +// upper ones. So we calculate z based on depth.
 +// Moreover, we try to shrink down z interval to [0, 1] to avoid that
 +// treemap with large z overlaps other components.
 +function calculateZ(depth: number, zInLevel: number) {
 +    var zb = depth * Z_BASE + zInLevel;
 +    return (zb - 1) / zb;
 +}
 +
 +ChartView.registerClass(TreemapView);
diff --cc src/layout/barGrid.ts
index 691202a,0000000..a79d918
mode 100644,000000..100644
--- a/src/layout/barGrid.ts
+++ b/src/layout/barGrid.ts
@@@ -1,612 -1,0 +1,618 @@@
 +/*
 +* 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 Float32Array */
 +
 +import * as zrUtil from 'zrender/src/core/util';
 +import {parsePercent} from '../util/number';
 +import {isDimensionStacked} from '../data/helper/dataStackHelper';
 +import createRenderPlanner from '../chart/helper/createRenderPlanner';
 +import BarSeriesModel from '../chart/bar/BarSeries';
 +import Axis2D from '../coord/cartesian/Axis2D';
 +import GlobalModel from '../model/Global';
 +import type Cartesian2D from '../coord/cartesian/Cartesian2D';
 +import { StageHandler, Dictionary } from '../util/types';
 +
 +var STACK_PREFIX = '__ec_stack_';
 +var LARGE_BAR_MIN_WIDTH = 0.5;
 +
 +var LargeArr = typeof Float32Array !== 'undefined' ? Float32Array : Array;
 +
 +
 +function getSeriesStackId(seriesModel: BarSeriesModel): string {
 +    return seriesModel.get('stack') || STACK_PREFIX + seriesModel.seriesIndex;
 +}
 +
 +function getAxisKey(axis: Axis2D): string {
 +    return axis.dim + axis.index;
 +}
 +
 +interface LayoutSeriesInfo {
 +    bandWidth: number
 +    barWidth: number
 +    barMaxWidth: number
 +    barMinWidth: number
 +    barGap: number | string
 +    barCategoryGap: number | string
 +    axisKey: string
 +    stackId: string
 +}
 +
 +interface StackInfo {
 +    width: number
 +    maxWidth: number
 +    minWidth?: number
 +}
 +
 +/**
 + * {
 + *  [coordSysId]: {
 + *      [stackId]: {bandWidth, offset, width}
 + *  }
 + * }
 + */
 +type BarWidthAndOffset = Dictionary<Dictionary<{
 +    bandWidth: number
 +    offset: number
 +    offsetCenter: number
 +    width: number
 +}>>
 +
 +interface LayoutOption {
 +    axis: Axis2D
 +    count: number
 +
 +    barWidth?: number
 +    barMaxWidth?: number
 +    barMinWidth?: number
 +    barGap?: number
 +    barCategoryGap?: number
 +}
 +/**
 + * @return {Object} {width, offset, offsetCenter} If axis.type is not 'category', return undefined.
 + */
 +export function getLayoutOnAxis(opt: LayoutOption) {
 +    var params: LayoutSeriesInfo[] = [];
 +    var baseAxis = opt.axis;
 +    var axisKey = 'axis0';
 +
 +    if (baseAxis.type !== 'category') {
 +        return;
 +    }
 +    var bandWidth = baseAxis.getBandWidth();
 +
 +    for (var i = 0; i < opt.count || 0; i++) {
 +        params.push(zrUtil.defaults({
 +            bandWidth: bandWidth,
 +            axisKey: axisKey,
 +            stackId: STACK_PREFIX + i
 +        }, opt) as LayoutSeriesInfo);
 +    }
 +    var widthAndOffsets = doCalBarWidthAndOffset(params);
 +
 +    var result = [];
 +    for (var i = 0; i < opt.count; i++) {
 +        var item = widthAndOffsets[axisKey][STACK_PREFIX + i];
 +        item.offsetCenter = item.offset + item.width / 2;
 +        result.push(item);
 +    }
 +
 +    return result;
 +}
 +
 +export function prepareLayoutBarSeries(seriesType: string, ecModel: GlobalModel): BarSeriesModel[] {
 +    var seriesModels: BarSeriesModel[] = [];
 +    ecModel.eachSeriesByType(seriesType, function (seriesModel: BarSeriesModel) {
 +        // Check series coordinate, do layout for cartesian2d only
 +        if (isOnCartesian(seriesModel) && !isInLargeMode(seriesModel)) {
 +            seriesModels.push(seriesModel);
 +        }
 +    });
 +    return seriesModels;
 +}
 +
 +
 +/**
 + * Map from (baseAxis.dim + '_' + baseAxis.index) to min gap of two adjacent
 + * values.
 + * This works for time axes, value axes, and log axes.
 + * For a single time axis, return value is in the form like
 + * {'x_0': [1000000]}.
 + * The value of 1000000 is in milliseconds.
 + */
 +function getValueAxesMinGaps(barSeries: BarSeriesModel[]) {
 +    /**
 +     * Map from axis.index to values.
 +     * For a single time axis, axisValues is in the form like
 +     * {'x_0': [1495555200000, 1495641600000, 1495728000000]}.
 +     * Items in axisValues[x], e.g. 1495555200000, are time values of all
 +     * series.
 +     */
 +    var axisValues: Dictionary<number[]> = {};
 +    zrUtil.each(barSeries, function (seriesModel) {
 +        var cartesian = seriesModel.coordinateSystem as Cartesian2D;
 +        var baseAxis = cartesian.getBaseAxis();
 +        if (baseAxis.type !== 'time' && baseAxis.type !== 'value') {
 +            return;
 +        }
 +
 +        var data = seriesModel.getData();
 +        var key = baseAxis.dim + '_' + baseAxis.index;
 +        var dim = data.mapDimension(baseAxis.dim);
 +        for (var i = 0, cnt = data.count(); i < cnt; ++i) {
 +            var value = data.get(dim, i) as number;
 +            if (!axisValues[key]) {
 +                // No previous data for the axis
 +                axisValues[key] = [value];
 +            }
 +            else {
 +                // No value in previous series
 +                axisValues[key].push(value);
 +            }
 +            // Ignore duplicated time values in the same axis
 +        }
 +    });
 +
 +    var axisMinGaps: Dictionary<number> = {};
 +    for (var key in axisValues) {
 +        if (axisValues.hasOwnProperty(key)) {
 +            var valuesInAxis = axisValues[key];
 +            if (valuesInAxis) {
 +                // Sort axis values into ascending order to calculate gaps
 +                valuesInAxis.sort(function (a, b) {
 +                    return a - b;
 +                });
 +
 +                var min = null;
 +                for (var j = 1; j < valuesInAxis.length; ++j) {
 +                    var delta = valuesInAxis[j] - valuesInAxis[j - 1];
 +                    if (delta > 0) {
 +                        // Ignore 0 delta because they are of the same axis value
 +                        min = min === null ? delta : Math.min(min, delta);
 +                    }
 +                }
 +                // Set to null if only have one data
 +                axisMinGaps[key] = min;
 +            }
 +        }
 +    }
 +    return axisMinGaps;
 +}
 +
 +export function makeColumnLayout(barSeries: BarSeriesModel[]) {
 +    var axisMinGaps = getValueAxesMinGaps(barSeries);
 +
 +    var seriesInfoList: LayoutSeriesInfo[] = [];
 +    zrUtil.each(barSeries, function (seriesModel) {
 +        var cartesian = seriesModel.coordinateSystem as Cartesian2D;
 +        var baseAxis = cartesian.getBaseAxis();
 +        var axisExtent = baseAxis.getExtent();
 +
 +        var bandWidth;
 +        if (baseAxis.type === 'category') {
 +            bandWidth = baseAxis.getBandWidth();
 +        }
 +        else if (baseAxis.type === 'value' || baseAxis.type === 'time') {
 +            var key = baseAxis.dim + '_' + baseAxis.index;
 +            var minGap = axisMinGaps[key];
 +            var extentSpan = Math.abs(axisExtent[1] - axisExtent[0]);
 +            var scale = baseAxis.scale.getExtent();
 +            var scaleSpan = Math.abs(scale[1] - scale[0]);
 +            bandWidth = minGap
 +                ? extentSpan / scaleSpan * minGap
 +                : extentSpan; // When there is only one data value
 +        }
 +        else {
 +            var data = seriesModel.getData();
 +            bandWidth = Math.abs(axisExtent[1] - axisExtent[0]) / data.count();
 +        }
 +
 +        var barWidth = parsePercent(
 +            seriesModel.get('barWidth'), bandWidth
 +        );
 +        var barMaxWidth = parsePercent(
 +            seriesModel.get('barMaxWidth'), bandWidth
 +        );
 +        var barMinWidth = parsePercent(
 +            // barMinWidth by default is 1 in cartesian. Because in value axis,
 +            // the auto-calculated bar width might be less than 1.
 +            seriesModel.get('barMinWidth') || 1, bandWidth
 +        );
 +        var barGap = seriesModel.get('barGap');
 +        var barCategoryGap = seriesModel.get('barCategoryGap');
 +
 +        seriesInfoList.push({
 +            bandWidth: bandWidth,
 +            barWidth: barWidth,
 +            barMaxWidth: barMaxWidth,
 +            barMinWidth: barMinWidth,
 +            barGap: barGap,
 +            barCategoryGap: barCategoryGap,
 +            axisKey: getAxisKey(baseAxis),
 +            stackId: getSeriesStackId(seriesModel)
 +        });
 +    });
 +
 +    return doCalBarWidthAndOffset(seriesInfoList);
 +}
 +
 +function doCalBarWidthAndOffset(seriesInfoList: LayoutSeriesInfo[]) {
 +    interface ColumnOnAxisInfo {
 +        bandWidth: number
 +        remainedWidth: number
 +        autoWidthCount: number
 +        categoryGap: number | string
 +        gap: number | string
 +        stacks: Dictionary<StackInfo>
 +    }
 +
 +    // Columns info on each category axis. Key is cartesian name
 +    var columnsMap: Dictionary<ColumnOnAxisInfo> = {};
 +
 +    zrUtil.each(seriesInfoList, function (seriesInfo, idx) {
 +        var axisKey = seriesInfo.axisKey;
 +        var bandWidth = seriesInfo.bandWidth;
 +        var columnsOnAxis: ColumnOnAxisInfo = columnsMap[axisKey] || {
 +            bandWidth: bandWidth,
 +            remainedWidth: bandWidth,
 +            autoWidthCount: 0,
 +            categoryGap: '20%',
 +            gap: '30%',
 +            stacks: {}
 +        };
 +        var stacks = columnsOnAxis.stacks;
 +        columnsMap[axisKey] = columnsOnAxis;
 +
 +        var stackId = seriesInfo.stackId;
 +
 +        if (!stacks[stackId]) {
 +            columnsOnAxis.autoWidthCount++;
 +        }
 +        stacks[stackId] = stacks[stackId] || {
 +            width: 0,
 +            maxWidth: 0
 +        };
 +
 +        // Caution: In a single coordinate system, these barGrid attributes
 +        // will be shared by series. Consider that they have default values,
 +        // only the attributes set on the last series will work.
 +        // Do not change this fact unless there will be a break change.
 +
 +        var barWidth = seriesInfo.barWidth;
 +        if (barWidth && !stacks[stackId].width) {
 +            // See #6312, do not restrict width.
 +            stacks[stackId].width = barWidth;
 +            barWidth = Math.min(columnsOnAxis.remainedWidth, barWidth);
 +            columnsOnAxis.remainedWidth -= barWidth;
 +        }
 +
 +        var barMaxWidth = seriesInfo.barMaxWidth;
 +        barMaxWidth && (stacks[stackId].maxWidth = barMaxWidth);
 +        var barMinWidth = seriesInfo.barMinWidth;
 +        barMinWidth && (stacks[stackId].minWidth = barMinWidth);
 +        var barGap = seriesInfo.barGap;
 +        (barGap != null) && (columnsOnAxis.gap = barGap);
 +        var barCategoryGap = seriesInfo.barCategoryGap;
 +        (barCategoryGap != null) && (columnsOnAxis.categoryGap = barCategoryGap);
 +    });
 +
 +    var result: BarWidthAndOffset = {};
 +
 +    zrUtil.each(columnsMap, function (columnsOnAxis, coordSysName) {
 +
 +        result[coordSysName] = {};
 +
 +        var stacks = columnsOnAxis.stacks;
 +        var bandWidth = columnsOnAxis.bandWidth;
 +        var categoryGap = parsePercent(columnsOnAxis.categoryGap, bandWidth);
 +        var barGapPercent = parsePercent(columnsOnAxis.gap, 1);
 +
 +        var remainedWidth = columnsOnAxis.remainedWidth;
 +        var autoWidthCount = columnsOnAxis.autoWidthCount;
 +        var autoWidth = (remainedWidth - categoryGap)
 +            / (autoWidthCount + (autoWidthCount - 1) * barGapPercent);
 +        autoWidth = Math.max(autoWidth, 0);
 +
 +        // Find if any auto calculated bar exceeded maxBarWidth
 +        zrUtil.each(stacks, function (column) {
 +            var maxWidth = column.maxWidth;
 +            var minWidth = column.minWidth;
 +
 +            if (!column.width) {
 +                var finalWidth = autoWidth;
 +                if (maxWidth && maxWidth < finalWidth) {
 +                    finalWidth = Math.min(maxWidth, remainedWidth);
 +                }
 +                // `minWidth` has higher priority. `minWidth` decide that wheter the
 +                // bar is able to be visible. So `minWidth` should not be restricted
 +                // by `maxWidth` or `remainedWidth` (which is from `bandWidth`). In
 +                // the extreme cases for `value` axis, bars are allowed to overlap
 +                // with each other if `minWidth` specified.
 +                if (minWidth && minWidth > finalWidth) {
 +                    finalWidth = minWidth;
 +                }
 +                if (finalWidth !== autoWidth) {
 +                    column.width = finalWidth;
 +                    remainedWidth -= finalWidth + barGapPercent * finalWidth;
 +                    autoWidthCount--;
 +                }
 +            }
 +            else {
 +                // `barMinWidth/barMaxWidth` has higher priority than `barWidth`, as
 +                // CSS does. Becuase barWidth can be a percent value, where
 +                // `barMaxWidth` can be used to restrict the final width.
 +                var finalWidth = column.width;
 +                if (maxWidth) {
 +                    finalWidth = Math.min(finalWidth, maxWidth);
 +                }
 +                // `minWidth` has higher priority, as described above
 +                if (minWidth) {
 +                    finalWidth = Math.max(finalWidth, minWidth);
 +                }
 +                column.width = finalWidth;
 +                remainedWidth -= finalWidth + barGapPercent * finalWidth;
 +                autoWidthCount--;
 +            }
 +        });
 +
 +        // Recalculate width again
 +        autoWidth = (remainedWidth - categoryGap)
 +            / (autoWidthCount + (autoWidthCount - 1) * barGapPercent);
 +
 +        autoWidth = Math.max(autoWidth, 0);
 +
 +
 +        var widthSum = 0;
 +        var lastColumn: StackInfo;
 +        zrUtil.each(stacks, function (column, idx) {
 +            if (!column.width) {
 +                column.width = autoWidth;
 +            }
 +            lastColumn = column;
 +            widthSum += column.width * (1 + barGapPercent);
 +        });
 +        if (lastColumn) {
 +            widthSum -= lastColumn.width * barGapPercent;
 +        }
 +
 +        var offset = -widthSum / 2;
 +        zrUtil.each(stacks, function (column, stackId) {
 +            result[coordSysName][stackId] = result[coordSysName][stackId] || {
 +                bandWidth: bandWidth,
 +                offset: offset,
 +                width: column.width
 +            } as BarWidthAndOffset[string][string];
 +
 +            offset += column.width * (1 + barGapPercent);
 +        });
 +    });
 +
 +    return result;
 +}
 +
 +/**
 + * @param barWidthAndOffset The result of makeColumnLayout
 + * @param seriesModel If not provided, return all.
 + * @return {stackId: {offset, width}} or {offset, width} if seriesModel provided.
 + */
 +function retrieveColumnLayout(barWidthAndOffset: BarWidthAndOffset, axis: Axis2D): typeof barWidthAndOffset[string]
 +// eslint-disable-next-line max-len
 +function retrieveColumnLayout(barWidthAndOffset: BarWidthAndOffset, axis: Axis2D, seriesModel: BarSeriesModel): typeof barWidthAndOffset[string][string]
 +function retrieveColumnLayout(
 +    barWidthAndOffset: BarWidthAndOffset,
 +    axis: Axis2D,
 +    seriesModel?: BarSeriesModel
 +) {
 +    if (barWidthAndOffset && axis) {
 +        var result = barWidthAndOffset[getAxisKey(axis)];
 +        if (result != null && seriesModel != null) {
 +            return result[getSeriesStackId(seriesModel)];
 +        }
 +        return result;
 +    }
 +}
 +export {retrieveColumnLayout};
 +
 +export function layout(seriesType: string, ecModel: GlobalModel) {
 +
 +    var seriesModels = prepareLayoutBarSeries(seriesType, ecModel);
 +    var barWidthAndOffset = makeColumnLayout(seriesModels);
 +
 +    var lastStackCoords: Dictionary<{p: number, n: number}[]> = {};
 +
 +    zrUtil.each(seriesModels, function (seriesModel) {
 +
 +        var data = seriesModel.getData();
 +        var cartesian = seriesModel.coordinateSystem as Cartesian2D;
 +        var baseAxis = cartesian.getBaseAxis();
 +
 +        var stackId = getSeriesStackId(seriesModel);
 +        var columnLayoutInfo = barWidthAndOffset[getAxisKey(baseAxis)][stackId];
 +        var columnOffset = columnLayoutInfo.offset;
 +        var columnWidth = columnLayoutInfo.width;
 +        var valueAxis = cartesian.getOtherAxis(baseAxis);
 +
 +        var barMinHeight = seriesModel.get('barMinHeight') || 0;
 +
 +        lastStackCoords[stackId] = lastStackCoords[stackId] || [];
 +
 +        data.setLayout({
 +            bandWidth: columnLayoutInfo.bandWidth,
 +            offset: columnOffset,
 +            size: columnWidth
 +        });
 +
 +        var valueDim = data.mapDimension(valueAxis.dim);
 +        var baseDim = data.mapDimension(baseAxis.dim);
 +        var stacked = isDimensionStacked(data, valueDim /*, baseDim*/);
 +        var isValueAxisH = valueAxis.isHorizontal();
 +
 +        var valueAxisStart = getValueAxisStart(baseAxis, valueAxis, stacked);
 +
 +        for (var idx = 0, len = data.count(); idx < len; idx++) {
 +            var value = data.get(valueDim, idx);
 +            var baseValue = data.get(baseDim, idx) as number;
 +
 +            var sign = value >= 0 ? 'p' : 'n' as 'p' | 'n';
 +            var baseCoord = valueAxisStart;
 +
 +            // Because of the barMinHeight, we can not use the value in
 +            // stackResultDimension directly.
 +            if (stacked) {
 +                // Only ordinal axis can be stacked.
 +                if (!lastStackCoords[stackId][baseValue]) {
 +                    lastStackCoords[stackId][baseValue] = {
 +                        p: valueAxisStart, // Positive stack
 +                        n: valueAxisStart  // Negative stack
 +                    };
 +                }
 +                // Should also consider #4243
 +                baseCoord = lastStackCoords[stackId][baseValue][sign];
 +            }
 +
 +            var x;
 +            var y;
 +            var width;
 +            var height;
 +
 +            if (isValueAxisH) {
 +                var coord = cartesian.dataToPoint([value, baseValue]);
 +                x = baseCoord;
 +                y = coord[1] + columnOffset;
 +                width = coord[0] - valueAxisStart;
 +                height = columnWidth;
 +
 +                if (Math.abs(width) < barMinHeight) {
 +                    width = (width < 0 ? -1 : 1) * barMinHeight;
 +                }
-                 stacked && (lastStackCoords[stackId][baseValue][sign] += width);
++                // Ignore stack from NaN value
++                if (!isNaN(width)) {
++                    stacked && (lastStackCoords[stackId][baseValue][sign] += width);
++                }
 +            }
 +            else {
 +                var coord = cartesian.dataToPoint([baseValue, value]);
 +                x = coord[0] + columnOffset;
 +                y = baseCoord;
 +                width = columnWidth;
 +                height = coord[1] - valueAxisStart;
 +
 +                if (Math.abs(height) < barMinHeight) {
 +                    // Include zero to has a positive bar
 +                    height = (height <= 0 ? -1 : 1) * barMinHeight;
 +                }
-                 stacked && (lastStackCoords[stackId][baseValue][sign] += height);
++                // Ignore stack from NaN value
++                if (!isNaN(height)) {
++                    stacked && (lastStackCoords[stackId][baseValue][sign] += height);
++                }
 +            }
 +
 +            data.setItemLayout(idx, {
 +                x: x,
 +                y: y,
 +                width: width,
 +                height: height
 +            });
 +        }
 +
 +    });
 +}
 +
 +// TODO: Do not support stack in large mode yet.
 +export var largeLayout: StageHandler = {
 +
 +    seriesType: 'bar',
 +
 +    plan: createRenderPlanner(),
 +
 +    reset: function (seriesModel: BarSeriesModel) {
 +        if (!isOnCartesian(seriesModel) || !isInLargeMode(seriesModel)) {
 +            return;
 +        }
 +
 +        var data = seriesModel.getData();
 +        var cartesian = seriesModel.coordinateSystem as Cartesian2D;
 +        var coordLayout = cartesian.grid.getRect();
 +        var baseAxis = cartesian.getBaseAxis();
 +        var valueAxis = cartesian.getOtherAxis(baseAxis);
 +        var valueDim = data.mapDimension(valueAxis.dim);
 +        var baseDim = data.mapDimension(baseAxis.dim);
 +        var valueAxisHorizontal = valueAxis.isHorizontal();
 +        var valueDimIdx = valueAxisHorizontal ? 0 : 1;
 +
 +        var barWidth = retrieveColumnLayout(
 +            makeColumnLayout([seriesModel]), baseAxis, seriesModel
 +        ).width;
 +        if (!(barWidth > LARGE_BAR_MIN_WIDTH)) { // jshint ignore:line
 +            barWidth = LARGE_BAR_MIN_WIDTH;
 +        }
 +
 +        return {
 +            progress: function (params, data) {
 +                var count = params.count;
 +                var largePoints = new LargeArr(count * 2);
 +                var largeBackgroundPoints = new LargeArr(count * 2);
 +                var largeDataIndices = new LargeArr(count);
 +                var dataIndex;
 +                var coord: number[] = [];
 +                var valuePair = [];
 +                var pointsOffset = 0;
 +                var idxOffset = 0;
 +
 +                while ((dataIndex = params.next()) != null) {
 +                    valuePair[valueDimIdx] = data.get(valueDim, dataIndex);
 +                    valuePair[1 - valueDimIdx] = data.get(baseDim, dataIndex);
 +
 +                    coord = cartesian.dataToPoint(valuePair, null, coord);
 +                    // Data index might not be in order, depends on `progressiveChunkMode`.
 +                    largeBackgroundPoints[pointsOffset] =
 +                        valueAxisHorizontal ? coordLayout.x + coordLayout.width : coord[0];
 +                    largePoints[pointsOffset++] = coord[0];
 +                    largeBackgroundPoints[pointsOffset] =
 +                        valueAxisHorizontal ? coord[1] : coordLayout.y + coordLayout.height;
 +                    largePoints[pointsOffset++] = coord[1];
 +                    largeDataIndices[idxOffset++] = dataIndex;
 +                }
 +
 +                data.setLayout({
 +                    largePoints: largePoints,
 +                    largeDataIndices: largeDataIndices,
 +                    largeBackgroundPoints: largeBackgroundPoints,
 +                    barWidth: barWidth,
 +                    valueAxisStart: getValueAxisStart(baseAxis, valueAxis, false),
 +                    backgroundStart: valueAxisHorizontal ? coordLayout.x : coordLayout.y,
 +                    valueAxisHorizontal: valueAxisHorizontal
 +                });
 +            }
 +        };
 +    }
 +};
 +
 +function isOnCartesian(seriesModel: BarSeriesModel) {
 +    return seriesModel.coordinateSystem && seriesModel.coordinateSystem.type === 'cartesian2d';
 +}
 +
 +function isInLargeMode(seriesModel: BarSeriesModel) {
 +    return seriesModel.pipelineContext && seriesModel.pipelineContext.large;
 +}
 +
 +// See cases in `test/bar-start.html` and `#7412`, `#8747`.
 +function getValueAxisStart(baseAxis: Axis2D, valueAxis: Axis2D, stacked?: boolean) {
 +    return valueAxis.toGlobalCoord(valueAxis.dataToCoord(valueAxis.type === 'log' ? 1 : 0));
 +}


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