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

sushuang pushed a commit to branch graph-thumbnail-upgrade
in repository https://gitbox.apache.org/repos/asf/echarts.git

commit f7e61f20c1f91431d4ef4f70b5ecd392dc764a13
Author: 100pah <sushuang0...@gmail.com>
AuthorDate: Thu Dec 8 15:02:08 2022 +0800

    feat(thumbnail): upgrade thumbnail
    (1) Enable pan and zoom on thumbnail
    (2) Rename selectedAreaStyle to windowStyle
    (3) Clean up the code
    (4) Support border-radius and clip.
---
 src/chart/graph/GraphSeries.ts         |  34 +--
 src/chart/graph/GraphView.ts           | 161 ++++++++---
 src/chart/graph/Thumbnail.ts           | 429 ++++++++++++++--------------
 src/chart/tree/TreeView.ts             |   6 +-
 src/component/helper/RoamController.ts |  17 +-
 src/component/helper/roamHelper.ts     |  12 +-
 src/util/layout.ts                     |  37 ++-
 test/graph-thumbnail.html              | 505 +++++++--------------------------
 8 files changed, 492 insertions(+), 709 deletions(-)

diff --git a/src/chart/graph/GraphSeries.ts b/src/chart/graph/GraphSeries.ts
index 5d69a30f3..3d96ac34e 100644
--- a/src/chart/graph/GraphSeries.ts
+++ b/src/chart/graph/GraphSeries.ts
@@ -43,8 +43,7 @@ import {
     GraphEdgeItemObject,
     OptionDataValueNumeric,
     CallbackDataParams,
-    DefaultEmphasisFocus,
-    ZRColor
+    DefaultEmphasisFocus
 } from '../../util/types';
 import SeriesModel from '../../model/Series';
 import Graph from '../../data/Graph';
@@ -55,6 +54,7 @@ import { LineDataVisual } from 
'../../visual/commonVisualTypes';
 import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup';
 import { defaultSeriesFormatTooltip } from 
'../../component/tooltip/seriesFormatTooltip';
 import {initCurvenessList, createEdgeMapForCurveness} from 
'../helper/multipleGraphEdgeHelper';
+import Thumbnail, { ThumbnailOption } from './Thumbnail';
 
 
 type GraphDataValue = OptionDataValue | OptionDataValue[];
@@ -230,13 +230,7 @@ export interface GraphSeriesOption
      */
     autoCurveness?: boolean | number | number[]
 
-    thumbnail?: BoxLayoutOptionMixin & {
-        show?: boolean,
-
-        itemStyle?: ItemStyleOption
-
-        selectedAreaStyle?: ItemStyleOption
-    }
+    thumbnail?: ThumbnailOption
 }
 
 class GraphSeriesModel extends SeriesModel<GraphSeriesOption> {
@@ -519,27 +513,7 @@ class GraphSeriesModel extends 
SeriesModel<GraphSeriesOption> {
             }
         },
 
-        thumbnail: {
-            show: false,
-
-            right: 0,
-            bottom: 0,
-
-            height: '25%',
-            width: '25%',
-
-            itemStyle: {
-                color: 'white',
-                borderColor: 'black'
-            },
-
-            selectedAreaStyle: {
-                color: 'white',
-                borderColor: 'black',
-                borderWidth: 1,
-                opacity: 0.5
-            }
-        }
+        thumbnail: Thumbnail.defaultOption
     };
 }
 
diff --git a/src/chart/graph/GraphView.ts b/src/chart/graph/GraphView.ts
index 329cfe5cf..f5c4b7ba6 100644
--- a/src/chart/graph/GraphView.ts
+++ b/src/chart/graph/GraphView.ts
@@ -19,8 +19,12 @@
 
 import SymbolDraw, { ListForSymbolDraw } from '../helper/SymbolDraw';
 import LineDraw from '../helper/LineDraw';
-import RoamController, { RoamControllerHost } from 
'../../component/helper/RoamController';
-import * as roamHelper from '../../component/helper/roamHelper';
+import RoamController from '../../component/helper/RoamController';
+import {
+    updateViewOnZoom,
+    updateViewOnPan,
+    RoamControllerHost
+} from '../../component/helper/roamHelper';
 import {onIrrelevantElement} from '../../component/helper/cursorHelper';
 import * as graphic from '../../util/graphic';
 import adjustEdge from './adjustEdge';
@@ -39,6 +43,8 @@ import Thumbnail from './Thumbnail';
 
 import { simpleLayoutEdge } from './simpleLayoutHelper';
 import { circularLayout, rotateNodeLabel } from './circularLayoutHelper';
+import { clone, extend } from 'zrender/src/core/util';
+import ECLinePath from '../helper/LinePath';
 
 function isViewCoordSys(coordSys: CoordinateSystem): coordSys is View {
     return coordSys.type === 'view';
@@ -63,7 +69,7 @@ class GraphView extends ChartView {
 
     private _layouting: boolean;
 
-    private _thumbnail: Thumbnail;
+    private _thumbnail: Thumbnail = new Thumbnail();
 
     private _mainGroup: graphic.Group;
 
@@ -218,9 +224,10 @@ class GraphView extends ChartView {
     dispose() {
         this._controller && this._controller.dispose();
         this._controllerHost = null;
+        this._thumbnail.dispose();
     }
 
-    _startForceLayoutIteration(
+    private _startForceLayoutIteration(
         forceLayout: GraphSeriesModel['forceLayout'],
         api: ExtensionAPI,
         layoutAnimation?: boolean
@@ -241,7 +248,7 @@ class GraphView extends ChartView {
         })();
     }
 
-    _updateController(
+    private _updateController(
         seriesModel: GraphSeriesModel,
         ecModel: GlobalModel,
         api: ExtensionAPI
@@ -250,10 +257,11 @@ class GraphView extends ChartView {
         const controllerHost = this._controllerHost;
         const group = this.group;
 
-        controller.setPointerChecker(function (e, x, y) {
+        controller.setPointerChecker((e, x, y) => {
             const rect = group.getBoundingRect();
             rect.applyTransform(group.transform);
             return rect.contain(x, y)
+                && !this._thumbnail.contain(x, y)
                 && !onIrrelevantElement(e, api, seriesModel);
         });
 
@@ -269,34 +277,53 @@ class GraphView extends ChartView {
             .off('pan')
             .off('zoom')
             .on('pan', (e) => {
-                roamHelper.updateViewOnPan(controllerHost, e.dx, e.dy);
-                api.dispatchAction({
-                    seriesId: seriesModel.id,
-                    type: 'graphRoam',
-                    dx: e.dx,
-                    dy: e.dy
-                });
-                this._thumbnail._updateSelectedRect('pan');
+                this._updateViewOnPan(seriesModel, api, e.dx, e.dy);
             })
             .on('zoom', (e) => {
-                roamHelper.updateViewOnZoom(controllerHost, e.scale, 
e.originX, e.originY);
-                api.dispatchAction({
-                    seriesId: seriesModel.id,
-                    type: 'graphRoam',
-                    zoom: e.scale,
-                    originX: e.originX,
-                    originY: e.originY
-                });
-                this._updateNodeAndLinkScale();
-                adjustEdge(seriesModel.getGraph(), 
getNodeGlobalScale(seriesModel));
-                this._lineDraw.updateLayout();
-                // Only update label layout on zoom
-                api.updateLabelLayout();
-                this._thumbnail._updateSelectedRect('zoom');
+                this._updateViewOnZoom(seriesModel, api, e.scale, e.originX, 
e.originY);
             });
     }
 
-    _updateNodeAndLinkScale() {
+    private _updateViewOnPan(
+        seriesModel: GraphSeriesModel,
+        api: ExtensionAPI,
+        dx: number,
+        dy: number
+    ): void {
+        updateViewOnPan(this._controllerHost, dx, dy);
+        api.dispatchAction({
+            seriesId: seriesModel.id,
+            type: 'graphRoam',
+            dx: dx,
+            dy: dy
+        });
+        this._thumbnail.updateWindow();
+    }
+
+    private _updateViewOnZoom(
+        seriesModel: GraphSeriesModel,
+        api: ExtensionAPI,
+        scale: number,
+        originX: number,
+        originY: number
+    ) {
+        updateViewOnZoom(this._controllerHost, scale, originX, originY);
+        api.dispatchAction({
+            seriesId: seriesModel.id,
+            type: 'graphRoam',
+            zoom: scale,
+            originX: originX,
+            originY: originY
+        });
+        this._updateNodeAndLinkScale();
+        adjustEdge(seriesModel.getGraph(), getNodeGlobalScale(seriesModel));
+        this._lineDraw.updateLayout();
+        // Only update label layout on zoom
+        api.updateLabelLayout();
+        this._thumbnail.updateWindow();
+    }
+
+    private _updateNodeAndLinkScale() {
         const seriesModel = this._model;
         const data = seriesModel.getData();
 
@@ -317,19 +344,85 @@ class GraphView extends ChartView {
     remove(ecModel: GlobalModel, api: ExtensionAPI) {
         this._symbolDraw && this._symbolDraw.remove();
         this._lineDraw && this._lineDraw.remove();
-        this._thumbnail && this.group.remove(this._thumbnail.group);
+        this._controller && this._controller.disable();
+        this._thumbnail.remove();
     }
 
+    // TODO: register thumbnail (consider code size).
     private _renderThumbnail(
         seriesModel: GraphSeriesModel,
         api: ExtensionAPI,
         symbolDraw: SymbolDraw,
         lineDraw: LineDraw
     ) {
-        if (this._thumbnail) {
-            this.group.remove(this._thumbnail.group);
-        }
-        (this._thumbnail = new Thumbnail(this.group)).render(seriesModel, api, 
symbolDraw, lineDraw, this._mainGroup);
+        const thumbnail = this._thumbnail;
+        this.group.add(thumbnail.group);
+
+        const renderThumbnailContent = (viewGroup: graphic.Group) => {
+            const symbolNodes = symbolDraw.group.children();
+            const lineNodes = lineDraw.group.children();
+
+            const lineGroup = new graphic.Group();
+            const symbolGroup = new graphic.Group();
+            viewGroup.add(symbolGroup);
+            viewGroup.add(lineGroup);
+
+            for (let i = 0; i < symbolNodes.length; i++) {
+                const node = symbolNodes[i];
+                const sub = (node as graphic.Group).children()[0];
+                const x = (node as Symbol).x;
+                const y = (node as Symbol).y;
+                const subShape = clone((sub as graphic.Path).shape);
+                const shape = extend(subShape, {
+                    width: sub.scaleX,
+                    height: sub.scaleY,
+                    x: x - sub.scaleX / 2,
+                    y: y - sub.scaleY / 2
+                });
+                const style = clone((sub as graphic.Path).style);
+                const subThumbnail = new (sub as any).constructor({
+                    shape,
+                    style,
+                    z2: 151
+                });
+                symbolGroup.add(subThumbnail);
+            }
+
+            for (let i = 0; i < lineNodes.length; i++) {
+                const node = lineNodes[i];
+                const line = (node as graphic.Group).children()[0];
+                const style = clone((line as ECLinePath).style);
+                const shape = clone((line as ECLinePath).shape);
+                const lineThumbnail = new ECLinePath({
+                    style,
+                    shape,
+                    z2: 151
+                });
+                lineGroup.add(lineThumbnail);
+            }
+        };
+
+        thumbnail.render({
+            seriesModel,
+            api,
+            roamType: seriesModel.get('roam'),
+            z2Setting: {
+                background: 150,
+                window: 160
+            },
+            seriesBoundingRect: this._mainGroup.getBoundingRect(),
+            renderThumbnailContent
+        });
+
+        thumbnail
+            .off('pan')
+            .off('zoom')
+            .on('pan', (event) => {
+                this._updateViewOnPan(seriesModel, api, event.dx, event.dy);
+            })
+            .on('zoom', (event) => {
+                this._updateViewOnZoom(seriesModel, api, event.scale, 
event.originX, event.originY);
+            });
     }
 }
 
diff --git a/src/chart/graph/Thumbnail.ts b/src/chart/graph/Thumbnail.ts
index bb2f2dab8..f47858437 100644
--- a/src/chart/graph/Thumbnail.ts
+++ b/src/chart/graph/Thumbnail.ts
@@ -1,265 +1,272 @@
 import * as graphic from '../../util/graphic';
 import ExtensionAPI from '../../core/ExtensionAPI';
 import * as layout from '../../util/layout';
-import { BoxLayoutOptionMixin } from '../../util/types';
-import SymbolClz from '../helper/Symbol';
-import ECLinePath from '../helper/LinePath';
 import GraphSeriesModel from './GraphSeries';
 import * as zrUtil from 'zrender/src/core/util';
 import View from '../../coord/View';
-import SymbolDraw from '../helper/SymbolDraw';
-import LineDraw from '../helper/LineDraw';
-
-interface LayoutParams {
-    pos: BoxLayoutOptionMixin
-    box: {
-        width: number,
-        height: number
-    }
+import BoundingRect from 'zrender/src/core/BoundingRect';
+import * as matrix from 'zrender/src/core/matrix';
+import * as vector from 'zrender/src/core/vector';
+import SeriesModel from '../../model/Series';
+import { BoxLayoutOptionMixin, ItemStyleOption } from '../../util/types';
+import RoamController, { RoamEventDefinition, RoamType } from 
'../../component/helper/RoamController';
+import Eventful from 'zrender/src/core/Eventful';
+
+
+// TODO:
+// Thumbnail should not be bound to a single series when used on
+// coordinate system like cartesian and geo/map?
+// Should we make thumbnail as a component like markers/axisPointer/brush did?
+
+
+interface BorderRadiusOption {
+    borderRadius?: number | number[]
 }
 
-function getViewRect(layoutParams: LayoutParams, wrapperShpae: {width: number, 
height: number}, aspect: number) {
-    const option = zrUtil.extend(layoutParams, {
-        aspect: aspect
-    });
-    return layout.getLayoutRect(option, {
-        width: wrapperShpae.width,
-        height: wrapperShpae.height
-    });
+// TODO: apply to other series
+export interface ThumbnailOption extends BoxLayoutOptionMixin {
+    show?: boolean,
+    itemStyle?: ItemStyleOption & BorderRadiusOption
+    windowStyle?: ItemStyleOption & BorderRadiusOption
 }
 
-class Thumbnail {
+interface WindowRect extends graphic.Rect {
+    __r?: BorderRadiusOption['borderRadius'];
+}
 
-    group = new graphic.Group();
+export interface ThumbnailZ2Setting {
+    background: number;
+    window: number;
+}
 
-    private _selectedRect: graphic.Rect;
+class Thumbnail extends Eventful<Pick<RoamEventDefinition, 'zoom' | 'pan'>> {
 
-    private _layoutParams: LayoutParams;
+    group = new graphic.Group();
 
-    private _graphModel: GraphSeriesModel;
+    private _api: ExtensionAPI;
+    private _seriesModel: GraphSeriesModel;
 
-    private _wrapper: graphic.Rect;
+    private _windowRect: WindowRect;
+    private _contentBoundingRect: BoundingRect;
+    private _thumbnailCoordSys: View;
 
-    private _coords: View;
+    private _mtSeriesToThumbnail: matrix.MatrixArray;
+    private _mtThumbnailToSerise: matrix.MatrixArray;
 
-    constructor(containerGroup: graphic.Group) {
-        containerGroup.add(this.group);
-    }
+    private _thumbnailController: RoamController;
+    private _isEnabled: boolean;
+
+    render(opt: {
+        seriesModel: GraphSeriesModel;
+        api: ExtensionAPI;
+        roamType: RoamType;
+        z2Setting: ThumbnailZ2Setting;
+        seriesBoundingRect: BoundingRect,
+        renderThumbnailContent: (viewGroup: graphic.Group) => void
+    }) {
+        const seriesModel = this._seriesModel = opt.seriesModel;
+        const api = this._api = opt.api;
 
-    render(
-        seriesModel: GraphSeriesModel,
-        api: ExtensionAPI,
-        symbolDraw: SymbolDraw,
-        lineDraw: LineDraw,
-         graph: graphic.Group
-    ) {
-        const model = seriesModel.getModel('thumbnail');
+        const thumbnailModel = seriesModel.getModel('thumbnail');
         const group = this.group;
-        group.removeAll();
-        if (!model.get('show')) {
+
+        this._isEnabled = thumbnailModel.get('show', true) && 
isSeriesSupported(seriesModel);
+        if (!this._isEnabled) {
+            this._clear();
             return;
         }
-        this._graphModel = seriesModel;
 
-        const symbolNodes = symbolDraw.group.children();
-        const lineNodes = lineDraw.group.children();
-
-        const lineGroup = new graphic.Group();
-        const symbolGroup = new graphic.Group();
-
-        const zoom = seriesModel.get('zoom', true);
+        group.removeAll();
 
-        const itemStyleModel = model.getModel('itemStyle');
+        const z2Setting = opt.z2Setting;
+        const cursor = opt.roamType ? 'pointer' : 'default';
+        const itemStyleModel = thumbnailModel.getModel('itemStyle');
         const itemStyle = itemStyleModel.getItemStyle();
-        const selectStyleModel = model.getModel('selectedAreaStyle');
-        const selectStyle = selectStyleModel.getItemStyle();
-        const thumbnailHeight = this._handleThumbnailShape(model.get('height', 
true), api, 'height');
-        const thumbnailWidth = this._handleThumbnailShape(model.get('width', 
true), api, 'width');
-
-        this._layoutParams = {
-            pos: {
-                left: model.get('left'),
-                right: model.get('right'),
-                top: model.get('top'),
-                bottom: model.get('bottom')
+        itemStyle.fill = seriesModel.ecModel.get('backgroundColor') || '#fff';
+
+        // Try to use border-box in thumbnail, see 
https://github.com/apache/echarts/issues/18022
+        const boxBorderWidth = itemStyle.lineWidth || 0;
+        const boxContainBorder = layout.getLayoutRect(
+            {
+                left: thumbnailModel.get('left', true),
+                top: thumbnailModel.get('top', true),
+                right: thumbnailModel.get('right', true),
+                bottom: thumbnailModel.get('bottom', true),
+                width: thumbnailModel.get('width', true),
+                height: thumbnailModel.get('height', true)
             },
-            box: {
+            {
                 width: api.getWidth(),
                 height: api.getHeight()
             }
-        };
-
-        const layoutParams = this._layoutParams;
-
-        const thumbnailGroup = new graphic.Group();
-
-        for (const node of symbolNodes) {
-            const sub = (node as graphic.Group).children()[0];
-            const x = (node as SymbolClz).x;
-            const y = (node as SymbolClz).y;
-            const subShape = zrUtil.clone((sub as graphic.Path).shape);
-            const shape = zrUtil.extend(subShape, {
-                width: sub.scaleX,
-                height: sub.scaleY,
-                x: x - sub.scaleX / 2,
-                y: y - sub.scaleY / 2
-            });
-            const style = zrUtil.clone((sub as graphic.Path).style);
-            const subThumbnail = new (sub as any).constructor({
-                shape,
-                style,
-                z2: 151
-            });
-            symbolGroup.add(subThumbnail);
-        }
-
-        for (const node of lineNodes) {
-            const line = (node as graphic.Group).children()[0];
-            const style = zrUtil.clone((line as ECLinePath).style);
-            const shape = zrUtil.clone((line as ECLinePath).shape);
-            const lineThumbnail = new ECLinePath({
-                style,
-                shape,
-                z2: 151
-            });
-            lineGroup.add(lineThumbnail);
-        }
-
-        thumbnailGroup.add(symbolGroup);
-        thumbnailGroup.add(lineGroup);
-
-        const thumbnailWrapper = new graphic.Rect({
+        );
+        const borderBoundingRect =
+            layout.applyPedding(boxContainBorder.clone(), boxBorderWidth / 2);
+        const contentBoundingRect = this._contentBoundingRect =
+            layout.applyPedding(boxContainBorder.clone(), boxBorderWidth);
+
+        const clipGroup = new graphic.Group();
+        group.add(clipGroup);
+        clipGroup.setClipPath(new graphic.Rect({
+            shape: contentBoundingRect.plain()
+        }));
+
+        const seriesViewGroup = new graphic.Group();
+        clipGroup.add(seriesViewGroup);
+        opt.renderThumbnailContent(seriesViewGroup);
+
+        // Draw border and background and shadow of thumbnail box.
+        group.add(new graphic.Rect({
             style: itemStyle,
-            shape: {
-                height: thumbnailHeight,
-                width: thumbnailWidth
+            shape: zrUtil.extend(borderBoundingRect.plain(), {
+                r: itemStyleModel.get('borderRadius', true)
+            }),
+            cursor,
+            z2: z2Setting.background
+        }));
+
+        const coordSys = this._thumbnailCoordSys = new View();
+        const seriesBoundingRect = opt.seriesBoundingRect;
+        coordSys.setBoundingRect(
+            seriesBoundingRect.x, seriesBoundingRect.y, 
seriesBoundingRect.width, seriesBoundingRect.height
+        );
+
+        // Find an approperiate rect in contentBoundingRect for the entire 
graph.
+        const graphViewRect = layout.getLayoutRect(
+            {
+                left: 'center',
+                top: 'center',
+                aspect: seriesBoundingRect.width / seriesBoundingRect.height
             },
-            z2: 150
+            contentBoundingRect
+        );
+        coordSys.setViewRect(graphViewRect.x, graphViewRect.y, 
graphViewRect.width, graphViewRect.height);
+        seriesViewGroup.attr(coordSys.getTransformInfo().raw);
+
+        const windowStyleModel = thumbnailModel.getModel('windowStyle');
+        const windowRect: WindowRect = this._windowRect = new graphic.Rect({
+            style: windowStyleModel.getItemStyle(),
+            cursor,
+            z2: z2Setting.window
         });
+        windowRect.__r = windowStyleModel.get('borderRadius', true);
+        clipGroup.add(windowRect);
 
-        this._wrapper = thumbnailWrapper;
-
-        group.add(thumbnailGroup);
-        group.add(thumbnailWrapper);
-
-        layout.positionElement(thumbnailWrapper, layoutParams.pos, 
layoutParams.box);
-
-        const coordSys = new View();
-        const boundingRect = graph.getBoundingRect();
-        coordSys.setBoundingRect(boundingRect.x, boundingRect.y, 
boundingRect.width, boundingRect.height);
-
-        this._coords = coordSys;
-
-        const viewRect = getViewRect(layoutParams, thumbnailWrapper.shape, 
boundingRect.width / boundingRect.height);
-
-        const scaleX = viewRect.width / boundingRect.width;
-        const scaleY = viewRect.height / boundingRect.height;
-        const offsetX = (thumbnailWidth - boundingRect.width * scaleX) / 2;
-        const offsetY = (thumbnailHeight - boundingRect.height * scaleY) / 2;
+        this._resetRoamController(opt.roamType);
 
+        this.updateWindow();
+    }
 
-        coordSys.setViewRect(
-            thumbnailWrapper.x + offsetX,
-            thumbnailWrapper.y + offsetY,
-            viewRect.width,
-            viewRect.height
-        );
+    /**
+     * Update window by series view roam status.
+     */
+    updateWindow(): void {
+        if (!this._isEnabled) {
+            return;
+        }
 
-        const groupNewProp = {
-            x: coordSys.x,
-            y: coordSys.y,
-            scaleX,
-            scaleY
-        };
-
-        thumbnailGroup.attr(groupNewProp);
-
-        this._selectedRect = new graphic.Rect({
-            style: selectStyle,
-            x: coordSys.x,
-            y: coordSys.y,
-            // ignore: true,
-            z2: 152
-        });
+        this._updateTransform();
 
-        group.add(this._selectedRect);
+        const rect = new BoundingRect(0, 0, this._api.getWidth(), 
this._api.getHeight());
+        rect.applyTransform(this._mtSeriesToThumbnail);
+        const windowRect = this._windowRect;
+        windowRect.setShape(zrUtil.defaults({r: windowRect.__r}, rect));
+    }
 
-        if (zoom > 1) {
-            this._updateSelectedRect('init');
-        }
+    /**
+     * Create transform that convert pixel vector from
+     * series coordinate system to thumbnail coordinate system.
+     *
+     * TODO: consider other type of series.
+     */
+    private _updateTransform(): void {
+        const seriesCoordSys = this._seriesModel.coordinateSystem as View;
+        this._mtSeriesToThumbnail = matrix.mul([], 
this._thumbnailCoordSys.transform, seriesCoordSys.invTransform);
+        this._mtThumbnailToSerise = matrix.invert([], 
this._mtSeriesToThumbnail);
     }
 
-    _updateSelectedRect(type: 'zoom' | 'pan' | 'init') {
-        const getNewRect = (min = false) => {
-            const {height, width} = this._layoutParams.box;
-            const origin = [0, 0];
-            const end = [width, height];
-            const originData = 
this._graphModel.coordinateSystem.pointToData(origin);
-            const endData = this._graphModel.coordinateSystem.pointToData(end);
+    private _resetRoamController(roamType: RoamType): void {
+        let thumbnailController = this._thumbnailController;
+        if (!thumbnailController) {
+            thumbnailController = this._thumbnailController = new 
RoamController(this._api.getZr());
+            thumbnailController.setPointerChecker((e, x, y) => this.contain(x, 
y));
+        }
 
-            const thumbnailMain = this._coords.dataToPoint(originData as 
number[]);
-            const thumbnailMax = this._coords.dataToPoint(endData as number[]);
+        thumbnailController.enable(roamType);
+        thumbnailController
+            .off('pan')
+            .off('zoom')
+            .on('pan', (event) => {
+                const transform = this._mtThumbnailToSerise;
+                const oldOffset = vector.applyTransform([], [event.oldX, 
event.oldY], transform);
+                // reverse old and new because we pan window rather graph in 
thumbnail.
+                const newOffset = vector.applyTransform([], [event.oldX - 
event.dx, event.oldY - event.dy], transform);
+                this.trigger('pan', {
+                    dx: newOffset[0] - oldOffset[0],
+                    dy: newOffset[1] - oldOffset[1],
+                    oldX: oldOffset[0],
+                    oldY: oldOffset[1],
+                    newX: newOffset[0],
+                    newY: newOffset[1],
+                    isAvailableBehavior: event.isAvailableBehavior
+                });
+            })
+            .on('zoom', (event) => {
+                const offset = vector.applyTransform([], [event.originX, 
event.originY], this._mtThumbnailToSerise);
+                this.trigger('zoom', {
+                    scale: 1 / event.scale,
+                    originX: offset[0],
+                    originY: offset[1],
+                    isAvailableBehavior: event.isAvailableBehavior
+                });
+            });
+    }
 
-            const newWidth = thumbnailMax[0] - thumbnailMain[0];
-            const newHeight = thumbnailMax[1] - thumbnailMain[1];
+    contain(x: number, y: number): boolean {
+        return this._contentBoundingRect && 
this._contentBoundingRect.contain(x, y);
+    }
 
-            rect.x = thumbnailMain[0];
-            rect.y = thumbnailMain[1];
+    private _clear(): void {
+        this.group.removeAll();
+        this._thumbnailController && this._thumbnailController.disable();
+    }
 
-            rect.shape.width = newWidth;
-            rect.shape.height = newHeight;
+    remove() {
+        this._clear();
+    }
 
-            if (min === false) {
-                rect.dirty();
-            }
-        };
-        const rect = this._selectedRect;
+    dispose() {
+        this._clear();
+    }
 
-        const {x: rMinX, y: rMinY, shape: {width: rWidth, height: rHeight}} = 
rect;
-        const {x: wMinX, y: wMinY, shape: {width: wWidth, height: wHeight}} = 
this._wrapper;
+    static defaultOption: ThumbnailOption = {
+        show: false,
 
-        const [rMaxX, rMaxY] = [rMinX + rWidth, rMinY + rHeight];
-        const [wMaxX, wMaxY] = [wMinX + wWidth, wMinY + wHeight];
+        right: 0,
+        bottom: 0,
 
-        if (type === 'init') {
-            rect.show();
-            getNewRect();
-            return;
-        }
-        else if (type === 'zoom' && rWidth < wWidth / 10) {
-            getNewRect(true);
-            return;
-        }
-        if (rMinX > wMinX && rMinY > wMinY && rMaxX < wMaxX && rMaxY < wMaxY) {
-            this._selectedRect.show();
-            // this._selectedRect.removeClipPath();
-        }
-        else {
-            // this._selectedRect.removeClipPath();
-            // this._selectedRect.setClipPath(this._wrapper);
-            this._selectedRect.hide();
-        }
+        height: '25%',
+        width: '25%',
 
-        getNewRect();
-    }
+        itemStyle: {
+            // Use echarts option.backgorundColor by default.
+            borderColor: '#555',
+            borderWidth: 2
+        },
 
-    _handleThumbnailShape(size: number | string, api: ExtensionAPI, type: 
'height' | 'width') {
-        if (typeof size === 'number') {
-            return size;
-        }
-        else {
-            const len = size.length;
-            if (size.includes('%') && size.indexOf('%') === len - 1) {
-                const screenSize = type === 'height' ? api.getHeight() : 
api.getWidth();
-                return +size.slice(0, len - 1) * screenSize / 100;
-            }
-            return 200;
+        windowStyle: {
+            borderWidth: 1,
+            color: 'green',
+            borderColor: '#000',
+            opacity: 0.3
         }
-    }
+    };
+}
 
-    remove() {
-        this.group.removeAll();
-    }
+// TODO: other coordinate system.
+function isSeriesSupported(seriesModel: SeriesModel): boolean {
+    const seriesCoordSys = seriesModel.coordinateSystem;
+    return seriesCoordSys && seriesCoordSys.type === 'view';
 }
 
 export default Thumbnail;
\ No newline at end of file
diff --git a/src/chart/tree/TreeView.ts b/src/chart/tree/TreeView.ts
index bd6a4ecc6..be32e5887 100644
--- a/src/chart/tree/TreeView.ts
+++ b/src/chart/tree/TreeView.ts
@@ -25,7 +25,7 @@ import {radialCoordinate} from './layoutHelper';
 import * as bbox from 'zrender/src/core/bbox';
 import View from '../../coord/View';
 import * as roamHelper from '../../component/helper/roamHelper';
-import RoamController, { RoamControllerHost } from 
'../../component/helper/RoamController';
+import RoamController from '../../component/helper/RoamController';
 import {onIrrelevantElement} from '../../component/helper/cursorHelper';
 import {parsePercent} from '../../util/number';
 import ChartView from '../../view/Chart';
@@ -132,7 +132,7 @@ class TreeView extends ChartView {
     private _mainGroup = new graphic.Group();
 
     private _controller: RoamController;
-    private _controllerHost: RoamControllerHost;
+    private _controllerHost: roamHelper.RoamControllerHost;
 
     private _data: SeriesData<TreeSeriesModel>;
 
@@ -145,7 +145,7 @@ class TreeView extends ChartView {
 
         this._controllerHost = {
             target: this.group
-        } as RoamControllerHost;
+        } as roamHelper.RoamControllerHost;
 
         this.group.add(this._mainGroup);
     }
diff --git a/src/component/helper/RoamController.ts 
b/src/component/helper/RoamController.ts
index 874865ebe..e316edb32 100644
--- a/src/component/helper/RoamController.ts
+++ b/src/component/helper/RoamController.ts
@@ -23,7 +23,6 @@ import * as interactionMutex from './interactionMutex';
 import { ZRenderType } from 'zrender/src/zrender';
 import { ZRElementEvent, RoamOptionMixin } from '../../util/types';
 import { Bind3, isString, bind, defaults, clone } from 'zrender/src/core/util';
-import Group from 'zrender/src/graphic/Group';
 
 // Can be null/undefined or true/false
 // or 'pan/move' or 'zoom'/'scale'
@@ -69,19 +68,11 @@ export interface RoamEventParams {
         isAvailableBehavior: Bind3<typeof isAvailableBehavior, null, 
RoamBehavior, ZRElementEvent>
     }
 };
-
-export interface RoamControllerHost {
-    target: Group
-    zoom: number
-    zoomLimit: {
-        min?: number
-        max?: number
-    }
-}
-
-class RoamController extends Eventful<{
+export type RoamEventDefinition = {
     [key in keyof RoamEventParams]: (params: RoamEventParams[key]) => void | 
undefined
-}> {
+};
+
+class RoamController extends Eventful<RoamEventDefinition> {
 
     pointerChecker: (e: ZRElementEvent, x: number, y: number) => boolean;
 
diff --git a/src/component/helper/roamHelper.ts 
b/src/component/helper/roamHelper.ts
index 07d583960..0ed6b819f 100644
--- a/src/component/helper/roamHelper.ts
+++ b/src/component/helper/roamHelper.ts
@@ -19,16 +19,16 @@
 
 import Element from 'zrender/src/Element';
 
-interface ControllerHost {
-    target: Element,
-    zoom?: number
-    zoomLimit?: {min?: number, max?: number}
+export interface RoamControllerHost {
+    target: Element;
+    zoom?: number;
+    zoomLimit?: {min?: number, max?: number};
 }
 
 /**
  * For geo and graph.
  */
-export function updateViewOnPan(controllerHost: ControllerHost, dx: number, 
dy: number) {
+export function updateViewOnPan(controllerHost: RoamControllerHost, dx: 
number, dy: number) {
     const target = controllerHost.target;
     target.x += dx;
     target.y += dy;
@@ -38,7 +38,7 @@ export function updateViewOnPan(controllerHost: 
ControllerHost, dx: number, dy:
 /**
  * For geo and graph.
  */
-export function updateViewOnZoom(controllerHost: ControllerHost, zoomDelta: 
number, zoomX: number, zoomY: number) {
+export function updateViewOnZoom(controllerHost: RoamControllerHost, 
zoomDelta: number, zoomX: number, zoomY: number) {
     const target = controllerHost.target;
     const zoomLimit = controllerHost.zoomLimit;
 
diff --git a/src/util/layout.ts b/src/util/layout.ts
index 1c30e7294..b9acfa051 100644
--- a/src/util/layout.ts
+++ b/src/util/layout.ts
@@ -20,7 +20,7 @@
 // Layout helpers for each component positioning
 
 import * as zrUtil from 'zrender/src/core/util';
-import BoundingRect from 'zrender/src/core/BoundingRect';
+import BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect';
 import {parsePercent} from './number';
 import * as formatUtil from './format';
 import { BoxLayoutOptionMixin, ComponentLayoutMode } from './types';
@@ -196,7 +196,22 @@ export function getLayoutRect(
     positionInfo: BoxLayoutOptionMixin & {
         aspect?: number // aspect is width / height
     },
-    containerRect: {width: number, height: number},
+    // The options in `positionInfo` is based on the container rect:
+    containerRect: {
+        x?: number; // by default 0
+        y?: number; // by default 0
+        width: number; // required
+        height: number; // required
+    },
+    // This is the margin to the canvas. If width/height is specified,
+    // `margin` does not effect width/height.
+    // If using `margin`, we should make sure:
+    // either [A]:
+    //      layout like CSS content-box, that is, user specified width/height 
means
+    //      content width/height, which do not include border-width and 
pedding.
+    // or [B]:
+    //      layout like CSS border-box, but user can not specify width/height
+    //      (like in `title`/`tootbox` component did)
     margin?: number | number[]
 ): LayoutRect {
     margin = formatUtil.normalizeCssArray(margin || 0);
@@ -289,6 +304,12 @@ export function getLayoutRect(
 
     const rect = new BoundingRect(left + margin[3], top + margin[0], width, 
height) as LayoutRect;
     rect.margin = margin;
+    if (containerRect.x) {
+        rect.x += containerRect.x;
+    }
+    if (containerRect.y) {
+        rect.y += containerRect.y;
+    }
     return rect;
 }
 
@@ -546,3 +567,15 @@ export function copyLayoutParams(target: 
BoxLayoutOptionMixin, source: BoxLayout
     });
     return target;
 }
+
+/**
+ * Apply pedding (CSS like) to a rect, and return the input rect.
+ */
+export function applyPedding<TRect extends RectLike>(rect: TRect, pedding?: 
number | number[]): TRect {
+    const peddingArr = formatUtil.normalizeCssArray(pedding || 0);
+    rect.x += peddingArr[3];
+    rect.y += peddingArr[0];
+    rect.width -= peddingArr[1] + peddingArr[3];
+    rect.height -= peddingArr[0] + peddingArr[2];
+    return rect;
+}
diff --git a/test/graph-thumbnail.html b/test/graph-thumbnail.html
index aa19a6813..86a17840b 100644
--- a/test/graph-thumbnail.html
+++ b/test/graph-thumbnail.html
@@ -13,18 +13,20 @@
     </head>
 
     <body>
+        <style>
+            .test-chart {
+                border: 10px solid #ddd;
+            }
+        </style>
         <div id="main0"></div>
-         <div id="main1"></div>
+        <div id="main1"></div>
         <div id="main2"></div>
         <div id="main3"></div>
     </body>
 
     <script>
-        var option;
-        require([
-            'echarts'/*, 'map/js/china' */
-        ], function (echarts) {
-            option = {
+        function createBaseGraphOption() {
+            return {
                 tooltip: {},
                 legend: {},
                 series: [{
@@ -34,16 +36,8 @@
                     circular: {
                         rotateLabel: true
                     },
-                      center: [700, '30%'],
-                      zoom: 8,
-                    thumbnail: {
-                        show: true,
-                        width: '25%',
-                        height: '25%',
-                        selectedAreaStyle: {
-                            color: 'blue'
-                        }
-                    },
+                    // center: [700, '30%'],
+                    // zoom: 8,
                     roam: true,
                     focusNodeAdjacency: true,
                     label: {
@@ -153,426 +147,117 @@
                     ]
                 }]
             };
+        }
+    </script>
+
+
+    <script>
+        require(['echarts'], function (echarts) {
+            var option = createBaseGraphOption();
+            option.series[0].thumbnail = {
+                show: true
+            };
+
             var chart = testHelper.create(echarts, 'main0', {
                 option: option,
-                title: 'has 0 value nodes'
+                width: 600,
+                title: [
+                    'minimun opiton (default behavior): ',
+                    '- thumbnail should be placed at right-bottom',
+                    '- thumbnail content should show the entire graph',
+                    '- widdth and height should be 1/4 of canvas',
+                    '- Should has white bg',
+                    '- drag on thumbnail box, the graph should be able to 
"pen"',
+                    '- mousewheel on thumbnail box, the graph should be able 
to "zoom"',
+                ]
             });
         });
     </script>
 
     <script>
-        var option;
-        require([
-            'echarts'/*, 'map/js/china' */
-        ], function (echarts) {
-            option = {
-                tooltip: {},
-                legend: {},
-                series: [{
-                    type: 'graph',
-                    name: 'Gene',
-                    layout: 'circular',
-                    circular: {
-                        rotateLabel: true
-                    },
-                    thumbnail: {
-                        show: true,
-                        width: 200,
-                        height: 400,
-                        selectedAreaStyle: {
-                            color: 'red'
-                        }
-                    },
-                    roam: true,
-                    focusNodeAdjacency: true,
-                    label: {
-                        show: true
-                    },
-                    lineStyle: {
-                        color: 'source',
-                        curveness: 0.3
-                    },
-                    emphasis: {
-                        label: {
-                            color: 'blue'
-                        },
-                        lineStyle: {
-                            width: 10
-                        }
-                    },
-                    data: [
-                        {
-                            itemStyle: null,
-                            name: 'DRD2',
-                            value: 40,
-                            symbolSize: 40
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'ADORA2A',
-                            value: 0,
-                            symbolSize: 20
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'ARRB2',
-                            value: 30,
-                            symbolSize: 20,
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'CALM1',
-                            value: 20,
-                            symbolSize: 40
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'CALM2',
-                            value: 0,
-                            symbolSize: 20
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'FLNA',
-                            value: 0,
-                            symbolSize: 20
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'NSF',
-                            value: 0,
-                            symbolSize: 20
-                        }
-                    ],
-                    links: [
-                        {
-                            source: 'DRD2',
-                            target: 'ADORA2A'
-                        },
-                        {
-                            source: 'DRD2',
-                            target: 'ARRB2'
-                        },
-                        {
-                            source: 'DRD2',
-                            target: 'CALM1'
-                        },
-                        {
-                            source: 'DRD2',
-                            target: 'CALM2'
-                        },
-                        {
-                            source: 'DRD2',
-                            target: 'FLNA'
-                        },
-                        {
-                            source: 'DRD2',
-                            target: 'NSF'
-                        },
-                        {
-                            source: 'CALM1',
-                            target: 'ADORA2A'
-                        },
-                        {
-                            source: 'CALM1',
-                            target: 'ARRB2'
-                        },
-                        {
-                            source: 'CALM1',
-                            target: 'CALM2'
-                        },
-                        {
-                            source: 'CALM1',
-                            target: 'FLNA'
-                        },
-                        {
-                            source: 'CALM1',
-                            target: 'NSF'
-                        },
-                    ]
-                }]
+        require(['echarts'], function (echarts) {
+            var option = createBaseGraphOption();
+            option.series[0].thumbnail = {
+                show: false
             };
+
             var chart = testHelper.create(echarts, 'main1', {
                 option: option,
-                title: 'has 0 value nodes'
+                width: 600,
+                title: [
+                    'thumbnail.show: false',
+                    '- thumbnail should not be displayed.',
+                    '- graph should be roam as normal.'
+                ]
             });
         });
     </script>
 
     <script>
-        var option;
-        require([
-            'echarts'/*, 'map/js/china' */
-        ], function (echarts) {
-            option = {
-                tooltip: {},
-                legend: {},
-                series: [{
-                    type: 'graph',
-                    name: 'Gene',
-                    layout: 'circular',
-                    circular: {
-                        rotateLabel: true
-                    },
-                    thumbnail: {
-                        show: true,
-                        width: 400,
-                        height: 200,
-                        selectedAreaStyle: {
-                            color: 'yellow'
-                        }
-                    },
-                    roam: true,
-                    focusNodeAdjacency: true,
-                    label: {
-                        show: true
-                    },
-                    lineStyle: {
-                        color: 'source',
-                        curveness: 0.3
-                    },
-                    emphasis: {
-                        label: {
-                            color: 'blue'
-                        },
-                        lineStyle: {
-                            width: 10
-                        }
-                    },
-                    data: [
-                        {
-                            itemStyle: null,
-                            name: 'DRD2',
-                            value: 40,
-                            symbolSize: 40
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'ADORA2A',
-                            value: 0,
-                            symbolSize: 20
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'ARRB2',
-                            value: 30,
-                            symbolSize: 20,
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'CALM1',
-                            value: 20,
-                            symbolSize: 40
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'CALM2',
-                            value: 0,
-                            symbolSize: 20
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'FLNA',
-                            value: 0,
-                            symbolSize: 20
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'NSF',
-                            value: 0,
-                            symbolSize: 20
-                        }
-                    ],
-                    links: [
-                        {
-                            source: 'DRD2',
-                            target: 'ADORA2A'
-                        },
-                        {
-                            source: 'DRD2',
-                            target: 'ARRB2'
-                        },
-                        {
-                            source: 'DRD2',
-                            target: 'CALM1'
-                        },
-                        {
-                            source: 'DRD2',
-                            target: 'CALM2'
-                        },
-                        {
-                            source: 'DRD2',
-                            target: 'FLNA'
-                        },
-                        {
-                            source: 'DRD2',
-                            target: 'NSF'
-                        },
-                        {
-                            source: 'CALM1',
-                            target: 'ADORA2A'
-                        },
-                        {
-                            source: 'CALM1',
-                            target: 'ARRB2'
-                        },
-                        {
-                            source: 'CALM1',
-                            target: 'CALM2'
-                        },
-                        {
-                            source: 'CALM1',
-                            target: 'FLNA'
-                        },
-                        {
-                            source: 'CALM1',
-                            target: 'NSF'
-                        },
-                    ]
-                }]
+        require(['echarts'], function (echarts) {
+            var option = createBaseGraphOption();
+            option.series[0].thumbnail = {
+                show: true,
+                width: 100,
+                height: 200,
+                right: 0,
+                top: 0,
+                itemStyle: {
+                    borderWidth: 20,
+                    borderColor: 'rgba(0,0,0,0.4)'
+                },
+                windowStyle: {
+                    color: 'yellow',
+                    borderWidth: 3,
+                    borderColor: 'blue',
+                    shadowBlur: 5
+                }
             };
+
             var chart = testHelper.create(echarts, 'main2', {
                 option: option,
-                title: 'has 0 value nodes'
+                width: 600,
+                height: 200,
+                title: [
+                    'Like CSS border-box (height should be just fit the canvas 
height)',
+                    '- borderWidth: 10, width: 100, height: 180, right: 10, 
top: 10',
+                    '- windowStyle: {color: "yellow", borderWidth: 3, 
borderColor: "blue", shadowBlur: 5}'
+                ]
             });
         });
     </script>
 
+
     <script>
-        var option;
-        require([
-            'echarts'/*, 'map/js/china' */
-        ], function (echarts) {
-            option = {
-                tooltip: {},
-                legend: {},
-                series: [{
-                    type: 'graph',
-                    name: 'Gene',
-                    layout: 'circular',
-                    circular: {
-                        rotateLabel: true
-                    },
-                    thumbnail: {
-                        show: true,
-                        width: 1000,
-                        height: 200,
-                        selectedAreaStyle: {
-                            color: 'green'
-                        }
-                    },
-                    roam: true,
-                    focusNodeAdjacency: true,
-                    label: {
-                        show: true
-                    },
-                    lineStyle: {
-                        color: 'source',
-                        curveness: 0.3
-                    },
-                    emphasis: {
-                        label: {
-                            color: 'blue'
-                        },
-                        lineStyle: {
-                            width: 10
-                        }
-                    },
-                    data: [
-                        {
-                            itemStyle: null,
-                            name: 'DRD2',
-                            value: 40,
-                            symbolSize: 40
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'ADORA2A',
-                            value: 0,
-                            symbolSize: 20
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'ARRB2',
-                            value: 30,
-                            symbolSize: 20,
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'CALM1',
-                            value: 20,
-                            symbolSize: 40
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'CALM2',
-                            value: 0,
-                            symbolSize: 20
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'FLNA',
-                            value: 0,
-                            symbolSize: 20
-                        },
-                        {
-                            itemStyle: null,
-                            name: 'NSF',
-                            value: 0,
-                            symbolSize: 20
-                        }
-                    ],
-                    links: [
-                        {
-                            source: 'DRD2',
-                            target: 'ADORA2A'
-                        },
-                        {
-                            source: 'DRD2',
-                            target: 'ARRB2'
-                        },
-                        {
-                            source: 'DRD2',
-                            target: 'CALM1'
-                        },
-                        {
-                            source: 'DRD2',
-                            target: 'CALM2'
-                        },
-                        {
-                            source: 'DRD2',
-                            target: 'FLNA'
-                        },
-                        {
-                            source: 'DRD2',
-                            target: 'NSF'
-                        },
-                        {
-                            source: 'CALM1',
-                            target: 'ADORA2A'
-                        },
-                        {
-                            source: 'CALM1',
-                            target: 'ARRB2'
-                        },
-                        {
-                            source: 'CALM1',
-                            target: 'CALM2'
-                        },
-                        {
-                            source: 'CALM1',
-                            target: 'FLNA'
-                        },
-                        {
-                            source: 'CALM1',
-                            target: 'NSF'
-                        },
-                    ]
-                }]
+        require(['echarts'], function (echarts) {
+            var option = createBaseGraphOption();
+            option.backgroundColor = 'rgba(222, 200, 100, 0.5)';
+            option.series[0].zoom = 5;
+            option.series[0].thumbnail = {
+                show: true,
+                left: 10,
+                top: 'center',
+                width: '10%',
+                height: '30%',
+                itemStyle: {
+                    borderRadius: 3,
+                },
+                windowStyle: {
+                    borderRadius: 3,
+                    // color: 'yellow',
+                }
             };
+
             var chart = testHelper.create(echarts, 'main3', {
+                title: [
+                    'Should have border-radius in both box and window rect',
+                    'Should auto use chart bg, should be opacity bg',
+                    'left: 10, top: "center", width: "10%", height: "30%"',
+                    'init zoom: 5'
+                ],
                 option: option,
-                title: 'has 0 value nodes'
+                height: 300,
+                width: 600
             });
         });
     </script>


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

Reply via email to