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/06/03 13:07:37 UTC

[incubator-echarts] branch label-enhancement updated: feat: add labelLine for all series

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

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


The following commit(s) were added to refs/heads/label-enhancement by this push:
     new dae5a2b  feat: add labelLine for all series
dae5a2b is described below

commit dae5a2b495ce5e8642f85fa175c40e5d059a1966
Author: pissang <bm...@gmail.com>
AuthorDate: Wed Jun 3 21:06:57 2020 +0800

    feat: add labelLine for all series
---
 src/chart/funnel/FunnelSeries.ts |  10 +--
 src/chart/funnel/FunnelView.ts   |  41 +++++-----
 src/chart/pie/PieSeries.ts       |  10 +--
 src/chart/pie/PieView.ts         |  27 ++----
 src/echarts.ts                   |   5 +-
 src/label/LabelManager.ts        | 172 ++++++++++++++++++++++++---------------
 src/label/labelGuideHelper.ts    | 113 +++++++++++++++++++++----
 src/util/types.ts                |   4 +-
 src/view/Chart.ts                |   6 ++
 test/labelLine.html              | 124 ++++++++++++++++++++++++++++
 test/pie-alignTo.html            |  16 ++--
 11 files changed, 383 insertions(+), 145 deletions(-)

diff --git a/src/chart/funnel/FunnelSeries.ts b/src/chart/funnel/FunnelSeries.ts
index a54d967..3e3c98f 100644
--- a/src/chart/funnel/FunnelSeries.ts
+++ b/src/chart/funnel/FunnelSeries.ts
@@ -28,7 +28,7 @@ import {
     BoxLayoutOptionMixin,
     HorizontalAlign,
     LabelOption,
-    LabelGuideLineOption,
+    LabelLineOption,
     ItemStyleOption,
     OptionDataValueNumeric
 } from '../../util/types';
@@ -51,12 +51,12 @@ export interface FunnelDataItemOption {
         height?: number | string
     }
     label?: FunnelLabelOption
-    labelLine?: LabelGuideLineOption
+    labelLine?: LabelLineOption
 
     emphasis?: {
         itemStyle?: ItemStyleOption
         label?: FunnelLabelOption
-        labelLine?: LabelGuideLineOption
+        labelLine?: LabelLineOption
     }
 }
 
@@ -80,12 +80,12 @@ export interface FunnelSeriesOption
     funnelAlign?: HorizontalAlign
 
     label?: FunnelLabelOption
-    labelLine?: LabelGuideLineOption
+    labelLine?: LabelLineOption
     itemStyle?: ItemStyleOption
 
     emphasis?: {
         label?: FunnelLabelOption
-        labelLine?: LabelGuideLineOption
+        labelLine?: LabelLineOption
         itemStyle?: ItemStyleOption
     }
 
diff --git a/src/chart/funnel/FunnelView.ts b/src/chart/funnel/FunnelView.ts
index 9f6ab67..a93299e 100644
--- a/src/chart/funnel/FunnelView.ts
+++ b/src/chart/funnel/FunnelView.ts
@@ -25,30 +25,30 @@ import ExtensionAPI from '../../ExtensionAPI';
 import List from '../../data/List';
 import { ColorString, LabelOption } from '../../util/types';
 import Model from '../../model/Model';
+import { setLabelLineStyle } from '../../label/labelGuideHelper';
 
 const opacityAccessPath = ['itemStyle', 'opacity'] as const;
 
 /**
  * Piece of pie including Sector, Label, LabelLine
  */
-class FunnelPiece extends graphic.Group {
+class FunnelPiece extends graphic.Polygon {
 
     constructor(data: List, idx: number) {
         super();
 
-        const polygon = new graphic.Polygon();
+        const polygon = this;
         const labelLine = new graphic.Polyline();
         const text = new graphic.Text();
-        this.add(polygon);
-        this.add(labelLine);
         polygon.setTextContent(text);
+        this.setTextGuideLine(labelLine);
 
         this.updateData(data, idx, true);
     }
 
     updateData(data: List, idx: number, firstCreate?: boolean) {
 
-        const polygon = this.childAt(0) as graphic.Polygon;
+        const polygon = this;
 
         const seriesModel = data.hostModel;
         const itemModel = data.getItemModel<FunnelDataItemOption>(idx);
@@ -92,8 +92,8 @@ class FunnelPiece extends graphic.Group {
     }
 
     _updateLabel(data: List, idx: number) {
-        const polygon = this.childAt(0);
-        const labelLine = this.childAt(1) as graphic.Polyline;
+        const polygon = this;
+        const labelLine = this.getTextGuideLine();
         const labelText = polygon.getTextContent();
 
         const seriesModel = data.hostModel;
@@ -131,11 +131,9 @@ class FunnelPiece extends graphic.Group {
             outsideFill: visualColor
         });
 
-        graphic.updateProps(labelLine, {
-            shape: {
-                points: labelLayout.linePoints || labelLayout.linePoints
-            }
-        }, seriesModel, idx);
+        labelLine.setShape({
+            points: labelLayout.linePoints || labelLayout.linePoints
+        });
 
         // Make sure update style on labelText after setLabelStyle.
         // Because setLabelStyle will replace a new style on it.
@@ -153,18 +151,15 @@ class FunnelPiece extends graphic.Group {
             z2: 10
         });
 
-        labelLine.ignore = !labelLineModel.get('show');
-        const labelLineEmphasisState = labelLine.ensureState('emphasis');
-        labelLineEmphasisState.ignore = !labelLineHoverModel.get('show');
-
-        // Default use item visual color
-        labelLine.setStyle({
+        setLabelLineStyle(polygon, {
+            normal: labelLineModel,
+            emphasis: labelLineHoverModel
+        }, {
+            // Default use item visual color
             stroke: visualColor
+        }, {
+            autoCalculate: false
         });
-        labelLine.setStyle(labelLineModel.getModel('lineStyle').getLineStyle());
-
-        const lineEmphasisState = labelLine.ensureState('emphasis');
-        lineEmphasisState.style = labelLineHoverModel.getModel('lineStyle').getLineStyle();
     }
 }
 
@@ -174,6 +169,8 @@ class FunnelView extends ChartView {
 
     private _data: List;
 
+    ignoreLabelLineUpdate = true;
+
     render(seriesModel: FunnelSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) {
         const data = seriesModel.getData();
         const oldData = this._data;
diff --git a/src/chart/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts
index e1cfa91..4b7fcc8 100644
--- a/src/chart/pie/PieSeries.ts
+++ b/src/chart/pie/PieSeries.ts
@@ -30,7 +30,7 @@ import {
     SeriesOption,
     CallbackDataParams,
     CircleLayoutOptionMixin,
-    LabelGuideLineOption,
+    LabelLineOption,
     ItemStyleOption,
     LabelOption,
     BoxLayoutOptionMixin,
@@ -57,12 +57,12 @@ export interface PieDataItemOption extends
 
     itemStyle?: ItemStyleOption
     label?: PieLabelOption
-    labelLine?: LabelGuideLineOption
+    labelLine?: LabelLineOption
 
     emphasis?: {
         itemStyle?: ItemStyleOption
         label?: PieLabelOption
-        labelLine?: LabelGuideLineOption
+        labelLine?: LabelLineOption
     }
 }
 export interface PieSeriesOption extends
@@ -81,7 +81,7 @@ export interface PieSeriesOption extends
     // TODO: TYPE Color Callback
     itemStyle?: ItemStyleOption
     label?: PieLabelOption
-    labelLine?: LabelGuideLineOption
+    labelLine?: LabelLineOption
 
     clockwise?: boolean
     startAngle?: number
@@ -99,7 +99,7 @@ export interface PieSeriesOption extends
     emphasis?: {
         itemStyle?: ItemStyleOption
         label?: PieLabelOption
-        labelLine?: LabelGuideLineOption
+        labelLine?: LabelLineOption
     }
 
     animationType?: 'expansion' | 'scale'
diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts
index 6241d52..54b73ba 100644
--- a/src/chart/pie/PieView.ts
+++ b/src/chart/pie/PieView.ts
@@ -28,6 +28,7 @@ import { Payload, ColorString, ECElement } from '../../util/types';
 import List from '../../data/List';
 import PieSeriesModel, {PieDataItemOption} from './PieSeries';
 import labelLayout from './labelLayout';
+import { setLabelLineStyle } from '../../label/labelGuideHelper';
 
 function updateDataSelected(
     this: PiePiece,
@@ -160,13 +161,11 @@ class PiePiece extends graphic.Sector {
 
     private _updateLabel(seriesModel: PieSeriesModel, data: List, idx: number): void {
         const sector = this;
-        const labelLine = sector.getTextGuideLine();
         const labelText = sector.getTextContent();
 
         const itemModel = data.getItemModel<PieDataItemOption>(idx);
 
         const labelTextEmphasisState = labelText.ensureState('emphasis');
-        const labelLineEmphasisState = labelLine.ensureState('emphasis');
 
         const labelModel = itemModel.getModel('label');
         const labelHoverModel = itemModel.getModel(['emphasis', 'label']);
@@ -209,25 +208,15 @@ class PiePiece extends graphic.Sector {
         labelText.ignore = !labelModel.get('show');
         labelTextEmphasisState.ignore = !labelHoverModel.get('show');
 
-        labelLine.ignore = !labelLineModel.get('show');
-        labelLineEmphasisState.ignore = !labelLineHoverModel.get('show');
-
         // Default use item visual color
-        labelLine.setStyle({
+        setLabelLineStyle(this, {
+            normal: labelLineModel,
+            emphasis: labelLineHoverModel
+        }, {
             stroke: visualColor,
             opacity: style && style.opacity
-        });
-        labelLine.setStyle(labelLineModel.getModel('lineStyle').getLineStyle());
-
-        const lineEmphasisState = labelLine.ensureState('emphasis');
-        lineEmphasisState.style = labelLineHoverModel.getModel('lineStyle').getLineStyle();
-
-        let smooth = labelLineModel.get('smooth');
-        if (smooth && smooth === true) {
-            smooth = 0.3;
-        }
-        labelLine.setShape({
-            smooth: smooth as number
+        }, {
+            autoCalculate: false
         });
     }
 }
@@ -238,6 +227,8 @@ class PieView extends ChartView {
 
     static type = 'pie';
 
+    ignoreLabelLineUpdate = true;
+
     private _sectorGroup: graphic.Group;
     private _data: List;
 
diff --git a/src/echarts.ts b/src/echarts.ts
index 485e058..d4d32c8 100644
--- a/src/echarts.ts
+++ b/src/echarts.ts
@@ -1097,7 +1097,7 @@ class ECharts extends Eventful {
         const labelManager = this._labelManager;
         labelManager.updateLayoutConfig(this._api);
         labelManager.layout();
-        labelManager.animateLabels();
+        labelManager.processLabelsOverall();
     }
 
     appendData(params: {
@@ -1727,14 +1727,13 @@ class ECharts extends Eventful {
 
                 // Add labels.
                 labelManager.addLabelsOfSeries(chartView);
-
             });
 
             scheduler.unfinished = unfinished || scheduler.unfinished;
 
             labelManager.updateLayoutConfig(api);
             labelManager.layout();
-            labelManager.animateLabels();
+            labelManager.processLabelsOverall();
 
             ecModel.eachSeries(function (seriesModel) {
                 const chartView = ecIns._chartsMap[seriesModel.__viewId];
diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts
index 9c50a06..bb3246f 100644
--- a/src/label/LabelManager.ts
+++ b/src/label/LabelManager.ts
@@ -36,17 +36,19 @@ import {
     ZRTextVerticalAlign,
     LabelLayoutOption,
     LabelLayoutOptionCallback,
-    LabelLayoutOptionCallbackParams
+    LabelLayoutOptionCallbackParams,
+    LabelLineOption
 } from '../util/types';
 import { parsePercent } from '../util/number';
 import ChartView from '../view/Chart';
-import { ElementTextConfig } from 'zrender/src/Element';
+import Element, { ElementTextConfig } from 'zrender/src/Element';
 import { RectLike } from 'zrender/src/core/BoundingRect';
 import Transformable from 'zrender/src/core/Transformable';
-import { updateLabelGuideLine } from './labelGuideHelper';
+import { updateLabelLinePoints, setLabelLineStyle } from './labelGuideHelper';
 import SeriesModel from '../model/Series';
 import { makeInner } from '../util/model';
-import { retrieve2, guid, each, keys } from 'zrender/src/core/util';
+import { retrieve2, each, keys } from 'zrender/src/core/util';
+import { PathStyleProps } from 'zrender/src/graphic/Path';
 
 interface DisplayedLabelItem {
     label: ZRText
@@ -59,7 +61,7 @@ interface DisplayedLabelItem {
 
 interface LabelLayoutDesc {
     label: ZRText
-    labelGuide: Polyline
+    labelLine: Polyline
 
     seriesModel: SeriesModel
     dataIndex: number
@@ -186,7 +188,7 @@ class LabelManager {
 
         this._labelList.push({
             label,
-            labelGuide: labelGuide,
+            labelLine: labelGuide,
 
             seriesModel,
             dataIndex,
@@ -228,7 +230,6 @@ class LabelManager {
                 attachedRot: textConfig.rotation
             }
         });
-
     }
 
     addLabelsOfSeries(chartView: ChartView) {
@@ -253,6 +254,7 @@ class LabelManager {
             // Only support label being hosted on graphic elements.
             const textEl = child.getTextContent();
             const dataIndex = getECData(child).dataIndex;
+            // Can only attach the text on the element with dataIndex
             if (textEl && dataIndex != null) {
                 this._addLabel(dataIndex, seriesModel, textEl, layoutOption);
             }
@@ -382,18 +384,18 @@ class LabelManager {
                 }
             }
 
-            const labelGuide = labelItem.labelGuide;
+            const labelLine = labelItem.labelLine;
             // TODO Callback to determine if this overlap should be handled?
             if (overlapped
                 && labelItem.layoutOption
                 && (labelItem.layoutOption as LabelLayoutOption).overlap === 'hidden'
             ) {
                 label.hide();
-                labelGuide && labelGuide.hide();
+                labelLine && labelLine.hide();
             }
             else {
                 label.attr('ignore', labelItem.defaultAttr.ignore);
-                labelGuide && labelGuide.attr('ignore', labelItem.defaultAttr.labelGuideIgnore);
+                labelLine && labelLine.attr('ignore', labelItem.defaultAttr.labelGuideIgnore);
 
                 displayedLabels.push({
                     label,
@@ -405,79 +407,115 @@ class LabelManager {
                 });
             }
 
-            updateLabelGuideLine(
-                label,
-                globalRect,
-                label.__hostTarget,
-                labelItem.hostRect,
-                labelItem.seriesModel.getModel(['labelLine'])
-            );
         }
     }
 
-    animateLabels() {
-        each(this._chartViewList, function (chartView) {
+    /**
+     * Process all labels. Not only labels with layoutOption.
+     */
+    processLabelsOverall() {
+        each(this._chartViewList, (chartView) => {
             const seriesModel = chartView.__model;
-            if (!seriesModel.isAnimationEnabled()) {
-                return;
-            }
+            const animationEnabled = seriesModel.isAnimationEnabled();
+            const ignoreLabelLineUpdate = chartView.ignoreLabelLineUpdate;
 
             chartView.group.traverse((child) => {
                 if (child.ignore) {
                     return true;    // Stop traverse descendants.
                 }
 
-                // Only support label being hosted on graphic elements.
-                const textEl = child.getTextContent();
-                const guideLine = child.getTextGuideLine();
-
-                if (textEl && !textEl.ignore && !textEl.invisible) {
-                    const layoutStore = labelAnimationStore(textEl);
-                    const oldLayout = layoutStore.oldLayout;
-                    const newProps = {
-                        x: textEl.x,
-                        y: textEl.y,
-                        rotation: textEl.rotation
-                    };
-                    if (!oldLayout) {
-                        textEl.attr(newProps);
-                        const oldOpacity = retrieve2(textEl.style.opacity, 1);
-                        // Fade in animation
-                        textEl.style.opacity = 0;
-                        initProps(textEl, {
-                            style: { opacity: oldOpacity }
-                        }, seriesModel);
-                    }
-                    else {
-                        textEl.attr(oldLayout);
-                        updateProps(textEl, newProps, seriesModel);
-                    }
-                    layoutStore.oldLayout = newProps;
+                if (!ignoreLabelLineUpdate) {
+                    this._updateLabelLine(child, seriesModel);
                 }
 
-                if (guideLine && !guideLine.ignore && !guideLine.invisible) {
-                    const layoutStore = labelLineAnimationStore(guideLine);
-                    const oldLayout = layoutStore.oldLayout;
-                    const newLayout = { points: guideLine.shape.points };
-                    if (!oldLayout) {
-                        guideLine.setShape(newLayout);
-                        guideLine.style.strokePercent = 0;
-                        initProps(guideLine, {
-                            style: { strokePercent: 1 }
-                        }, seriesModel);
-                    }
-                    else {
-                        guideLine.attr({ shape: oldLayout });
-                        updateProps(guideLine, {
-                            shape: newLayout
-                        }, seriesModel);
-                    }
-
-                    layoutStore.oldLayout = newLayout;
+                if (animationEnabled) {
+                    this._animateLabels(child, seriesModel);
                 }
             });
         });
     }
+
+    private _updateLabelLine(el: Element, seriesModel: SeriesModel) {
+        // Only support label being hosted on graphic elements.
+        const textEl = el.getTextContent();
+        // Update label line style.
+        const ecData = getECData(el);
+        const dataIndex = ecData.dataIndex;
+
+        if (textEl && dataIndex != null) {
+            const data = seriesModel.getData(ecData.dataType);
+            const itemModel = data.getItemModel<{
+                labelLine: LabelLineOption,
+                emphasis: { labelLine: LabelLineOption }
+            }>(dataIndex);
+
+            const defaultStyle: PathStyleProps = {};
+            const visualStyle = data.getItemVisual(dataIndex, 'style');
+            const visualType = data.getVisual('drawType');
+            // Default to be same with main color
+            defaultStyle.stroke = visualStyle[visualType];
+
+            const labelLineModel = itemModel.getModel('labelLine');
+
+            setLabelLineStyle(el, {
+                normal: labelLineModel,
+                emphasis: itemModel.getModel(['emphasis', 'labelLine'])
+            }, defaultStyle);
+
+
+            updateLabelLinePoints(el, labelLineModel);
+        }
+    }
+
+    private _animateLabels(el: Element, seriesModel: SeriesModel) {
+        const textEl = el.getTextContent();
+        const guideLine = el.getTextGuideLine();
+        // Animate
+        if (textEl && !textEl.ignore && !textEl.invisible) {
+            const layoutStore = labelAnimationStore(textEl);
+            const oldLayout = layoutStore.oldLayout;
+            const newProps = {
+                x: textEl.x,
+                y: textEl.y,
+                rotation: textEl.rotation
+            };
+            if (!oldLayout) {
+                textEl.attr(newProps);
+                const oldOpacity = retrieve2(textEl.style.opacity, 1);
+                // Fade in animation
+                textEl.style.opacity = 0;
+                initProps(textEl, {
+                    style: { opacity: oldOpacity }
+                }, seriesModel);
+            }
+            else {
+                textEl.attr(oldLayout);
+                updateProps(textEl, newProps, seriesModel);
+            }
+            layoutStore.oldLayout = newProps;
+        }
+
+        if (guideLine && !guideLine.ignore && !guideLine.invisible) {
+            const layoutStore = labelLineAnimationStore(guideLine);
+            const oldLayout = layoutStore.oldLayout;
+            const newLayout = { points: guideLine.shape.points };
+            if (!oldLayout) {
+                guideLine.setShape(newLayout);
+                guideLine.style.strokePercent = 0;
+                initProps(guideLine, {
+                    style: { strokePercent: 1 }
+                }, seriesModel);
+            }
+            else {
+                guideLine.attr({ shape: oldLayout });
+                updateProps(guideLine, {
+                    shape: newLayout
+                }, seriesModel);
+            }
+
+            layoutStore.oldLayout = newLayout;
+        }
+    }
 }
 
 
diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts
index ddeba60..c3cbc90 100644
--- a/src/label/labelGuideHelper.ts
+++ b/src/label/labelGuideHelper.ts
@@ -20,19 +20,24 @@
 import {
     Text as ZRText,
     Point,
-    Path
+    Path,
+    Polyline
 } from '../util/graphic';
 import PathProxy from 'zrender/src/core/PathProxy';
 import { RectLike } from 'zrender/src/core/BoundingRect';
 import { normalizeRadian } from 'zrender/src/contain/util';
 import { cubicProjectPoint, quadraticProjectPoint } from 'zrender/src/core/curve';
 import Element from 'zrender/src/Element';
-import { LabelGuideLineOption } from '../util/types';
+import { extend, defaults, retrieve2 } from 'zrender/src/core/util';
+import { LabelLineOption } from '../util/types';
 import Model from '../model/Model';
+import { invert } from 'zrender/src/core/matrix';
 
 const PI2 = Math.PI * 2;
 const CMD = PathProxy.CMD;
 
+const STATES = ['normal', 'emphasis'] as const;
+
 const DEFAULT_SEARCH_SPACE = ['top', 'right', 'bottom', 'left'] as const;
 
 type CandidatePosition = typeof DEFAULT_SEARCH_SPACE[number];
@@ -331,50 +336,58 @@ const dir2 = new Point();
  * @param target
  * @param targetRect
  */
-export function updateLabelGuideLine(
-    label: ZRText,
-    labelRect: RectLike,
+export function updateLabelLinePoints(
     target: Element,
-    targetRect: RectLike,
-    labelLineModel: Model<LabelGuideLineOption>
+    labelLineModel: Model<LabelLineOption>
 ) {
     if (!target) {
         return;
     }
 
     const labelLine = target.getTextGuideLine();
+    const label = target.getTextContent();
     // Needs to create text guide in each charts.
-    if (!labelLine) {
+    if (!(label && labelLine)) {
         return;
     }
 
     const labelGuideConfig = target.textGuideLineConfig || {};
-    if (!labelGuideConfig.autoCalculate) {
-        return;
-    }
 
     const points = [[0, 0], [0, 0], [0, 0]];
 
     const searchSpace = labelGuideConfig.candidates || DEFAULT_SEARCH_SPACE;
+    const labelRect = label.getBoundingRect().clone();
+    labelRect.applyTransform(label.getComputedTransform());
 
     let minDist = Infinity;
     const anchorPoint = labelGuideConfig && labelGuideConfig.anchor;
+    const targetTransform = target.getComputedTransform();
+    const targetInversedTransform = invert([], targetTransform);
+    const len = labelLineModel.get('length2') || 0;
+
     if (anchorPoint) {
         pt2.copy(anchorPoint);
     }
     for (let i = 0; i < searchSpace.length; i++) {
         const candidate = searchSpace[i];
         getCandidateAnchor(candidate, 0, labelRect, pt0, dir);
-        Point.scaleAndAdd(pt1, pt0, dir, labelGuideConfig.len == null ? 15 : labelGuideConfig.len);
+        Point.scaleAndAdd(pt1, pt0, dir, len);
+
+        // Transform to target coord space.
+        pt1.transform(targetInversedTransform);
 
         const dist = anchorPoint ? anchorPoint.distance(pt1)
             : (target instanceof Path
                 ? nearestPointOnPath(pt1, target.path, pt2)
-                : nearestPointOnRect(pt1, targetRect, pt2));
+                : nearestPointOnRect(pt1, target.getBoundingRect(), pt2));
 
         // TODO pt2 is in the path
         if (dist < minDist) {
             minDist = dist;
+            // Transform back to global space.
+            pt1.transform(targetTransform);
+            pt2.transform(targetTransform);
+
             pt2.toArray(points[0]);
             pt1.toArray(points[1]);
             pt0.toArray(points[2]);
@@ -440,4 +453,76 @@ export function limitTurnAngle(linePoints: number[][], minTurnAngle: number) {
 
         tmpProjPoint.toArray(linePoints[1]);
     }
-}
\ No newline at end of file
+}
+
+
+type LabelLineModel = Model<LabelLineOption>;
+/**
+ * Create a label line if necessary and set it's style.
+ */
+export function setLabelLineStyle(
+    targetEl: Element,
+    statesModels: Record<typeof STATES[number], LabelLineModel>,
+    defaultStyle?: Polyline['style'],
+    defaultConfig?: Element['textGuideLineConfig']
+) {
+    let labelLine = targetEl.getTextGuideLine();
+    const label = targetEl.getTextContent();
+    if (!label) {
+        // Not show label line if there is no label.
+        if (labelLine) {
+            targetEl.removeTextGuideLine();
+        }
+        return;
+    }
+
+    const normalModel = statesModels.normal;
+    const showNormal = normalModel.get('show');
+    const labelShowNormal = label.ignore;
+
+    for (let i = 0; i < STATES.length; i++) {
+        const stateName = STATES[i];
+        const stateModel = statesModels[stateName];
+        const isNormal = stateName === 'normal';
+        if (stateModel) {
+            const stateShow = stateModel.get('show');
+            const isLabelIgnored = isNormal
+                ? labelShowNormal
+                : retrieve2(label.states && label.states[stateName].ignore, labelShowNormal);
+            if (isLabelIgnored  // Not show when label is not shown in this state.
+                || !retrieve2(stateShow, showNormal) // Use normal state by default if not set.
+            ) {
+                const stateObj = isNormal ? labelLine : (labelLine && labelLine.states.normal);
+                if (stateObj) {
+                    stateObj.ignore = true;
+                }
+                continue;
+            }
+            // Create labelLine if not exists
+            if (!labelLine) {
+                labelLine = new Polyline();
+                targetEl.setTextGuideLine(labelLine);
+            }
+
+            const stateObj = isNormal ? labelLine : labelLine.ensureState(stateName);
+            // Make sure display.
+            stateObj.ignore = false;
+            // Set smooth
+            let smooth = stateModel.get('smooth');
+            if (smooth && smooth === true) {
+                smooth = 0.4;
+            }
+            stateObj.shape = stateObj.shape || {};
+            (stateObj.shape as Polyline['shape']).smooth = smooth as number;
+
+            const styleObj = stateModel.getModel('lineStyle').getLineStyle();
+            isNormal ? labelLine.useStyle(styleObj) : stateObj.style = styleObj;
+        }
+    }
+
+    if (labelLine) {
+        defaults(labelLine.style, defaultStyle);
+        // Not fill.
+        labelLine.style.fill = null;
+    }
+}
diff --git a/src/util/types.ts b/src/util/types.ts
index 75ae6e3..994aab8 100644
--- a/src/util/types.ts
+++ b/src/util/types.ts
@@ -799,7 +799,7 @@ export interface LineLabelOption extends Omit<LabelOption, 'distance' | 'positio
     distance?: number | number[]
 }
 
-export interface LabelGuideLineOption {
+export interface LabelLineOption {
     show?: boolean
     length?: number
     length2?: number
@@ -1132,7 +1132,7 @@ export interface SeriesOption extends
      */
     seriesLayoutBy?: 'column' | 'row'
 
-    labelLine?: LabelGuideLineOption
+    labelLine?: LabelLineOption
 
     /**
      * Global label layout option in label layout stage.
diff --git a/src/view/Chart.ts b/src/view/Chart.ts
index 2b5e3d7..529ed94 100644
--- a/src/view/Chart.ts
+++ b/src/view/Chart.ts
@@ -114,6 +114,12 @@ class ChartView {
 
     readonly renderTask: SeriesTask;
 
+    /**
+     * Ignore label line update in global stage. Will handle it in chart itself.
+     * Used in pie / funnel
+     */
+    ignoreLabelLineUpdate: boolean;
+
     // ----------------------
     // Injectable properties
     // ----------------------
diff --git a/test/labelLine.html b/test/labelLine.html
new file mode 100644
index 0000000..38297a6
--- /dev/null
+++ b/test/labelLine.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+
+<html>
+    <head>
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <script src="lib/esl.js"></script>
+        <script src="lib/config.js"></script>
+        <script src="lib/jquery.min.js"></script>
+        <script src="lib/facePrint.js"></script>
+        <script src="lib/testHelper.js"></script>
+        <!-- <script src="ut/lib/canteen.js"></script> -->
+        <link rel="stylesheet" href="lib/reset.css" />
+    </head>
+    <body>
+        <style>
+        </style>
+
+
+
+        <div id="main0"></div>
+
+
+
+
+
+
+
+
+
+        <script>
+        require(['echarts'/*, 'map/js/china' */], function (echarts) {
+            var option;
+            var data = [
+                [[28604,77,17096869,'Australia',1990],[31163,77.4,27662440,'Canada',1990],[1516,68,1154605773,'China',1990],[13670,74.7,10582082,'Cuba',1990],[28599,75,4986705,'Finland',1990],[29476,77.1,56943299,'France',1990],[31476,75.4,78958237,'Germany',1990],[28666,78.1,254830,'Iceland',1990],[1777,57.7,870601776,'India',1990],[29550,79.1,122249285,'Japan',1990],[2076,67.9,20194354,'North Korea',1990],[12087,72,42972254,'South Korea',1990],[24021,75.4,3397534,'New Zealand',1990],[4 [...]
+                [[44056,81.8,23968973,'Australia',2015],[43294,81.7,35939927,'Canada',2015],[13334,76.9,1376048943,'China',2015],[21291,78.5,11389562,'Cuba',2015],[38923,80.8,5503457,'Finland',2015],[37599,81.9,64395345,'France',2015],[44053,81.1,80688545,'Germany',2015],[42182,82.8,329425,'Iceland',2015],[5903,66.8,1311050527,'India',2015],[36162,83.5,126573481,'Japan',2015],[1390,71.4,25155317,'North Korea',2015],[34644,80.7,50293439,'South Korea',2015],[34186,80.6,4528526,'New Zealand [...]
+            ];
+
+            option = {
+                xAxis: {},
+                yAxis: {},
+                series: [{
+                    name: '1990',
+                    data: data[0],
+                    type: 'scatter',
+                    symbolSize: function (data) {
+                        return Math.sqrt(data[2]) / 5e2;
+                    },
+                    labelLayout: {
+                        y: 20,
+                        align: 'center',
+                        overlap: 'hidden'
+                    },
+                    labelLine: {
+                        show: true
+                    },
+                    label: {
+                        show: true,
+                        formatter: function (param) {
+                            return param.data[3];
+                        },
+                        position: 'top'
+                    }
+                }, {
+                    name: '2015',
+                    data: data[1],
+                    type: 'scatter',
+                    symbolSize: function (data) {
+                        return Math.sqrt(data[2]) / 5e2;
+                    },
+                    labelLayout: {
+                        y: 40,
+                        align: 'center',
+                        overlap: 'hidden'
+                    },
+                    labelLine: {
+                        show: true
+                    },
+                    label: {
+                        show: true,
+                        formatter: function (param) {
+                            return param.data[3];
+                        },
+                        position: 'top'
+                    }
+                }]
+            };
+
+            var chart = testHelper.create(echarts, 'main0', {
+                title: [
+                    'Test Case Description of main0',
+                    '(Muliple lines and **emphasis** are supported in description)'
+                ],
+                option: option
+                // height: 300,
+                // buttons: [{text: 'btn-txt', onclick: function () {}}],
+                // recordCanvas: true,
+            });
+        });
+        </script>
+
+
+    </body>
+</html>
+
diff --git a/test/pie-alignTo.html b/test/pie-alignTo.html
index 969fde2..db08612 100644
--- a/test/pie-alignTo.html
+++ b/test/pie-alignTo.html
@@ -68,7 +68,7 @@ under the License.
                     type: 'pie',
                     radius: '50%',
                     data: data,
-                    animation: false,
+
                     labelLine: {
                         length2: 15
                     },
@@ -84,7 +84,7 @@ under the License.
                     type: 'pie',
                     radius: '50%',
                     data: data,
-                    animation: false,
+
                     labelLine: {
                         length2: 15
                     },
@@ -101,7 +101,7 @@ under the License.
                     type: 'pie',
                     radius: '50%',
                     data: data,
-                    animation: false,
+
                     labelLine: {
                         length2: 15
                     },
@@ -119,7 +119,7 @@ under the License.
                     radius: '25%',
                     center: ['50%', '50%'],
                     data: data,
-                    animation: false,
+
                     labelLine: {
                         length2: 15
                     },
@@ -136,7 +136,7 @@ under the License.
                     radius: '25%',
                     center: ['50%', '50%'],
                     data: data,
-                    animation: false,
+
                     labelLine: {
                         length2: 15
                     },
@@ -154,7 +154,7 @@ under the License.
                     radius: '25%',
                     center: ['50%', '50%'],
                     data: data,
-                    animation: false,
+
                     labelLine: {
                         length2: 15
                     },
@@ -171,7 +171,7 @@ under the License.
                     radius: '25%',
                     center: ['50%', '50%'],
                     data: data,
-                    animation: false,
+
                     labelLine: {
                         length2: 15
                     },
@@ -251,8 +251,6 @@ under the License.
                 });
             });
         </script>
-
-
     </body>
 </html>
 


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