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 c30f137  feat: add auto calculated label guide line.
c30f137 is described below

commit c30f1370939545abea1b275e81f57d82d6fafb3d
Author: pissang <[email protected]>
AuthorDate: Thu May 7 21:58:52 2020 +0800

    feat: add auto calculated label guide line.
---
 src/chart/pie/PieView.ts            |  12 +-
 src/echarts.ts                      |   2 +-
 src/{util => label}/LabelManager.ts |  46 ++++-
 src/label/labelGuideHelper.ts       | 366 ++++++++++++++++++++++++++++++++++++
 4 files changed, 406 insertions(+), 20 deletions(-)

diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts
index 4409db1..b36be3c 100644
--- a/src/chart/pie/PieView.ts
+++ b/src/chart/pie/PieView.ts
@@ -235,7 +235,7 @@ class PiePiece extends graphic.Sector {
             outsideFill: visualColor
         });
 
-        const targetTextStyle = {
+        const targetTextPos = {
             x: labelLayout.x,
             y: labelLayout.y
         };
@@ -244,9 +244,7 @@ class PiePiece extends graphic.Sector {
                 shape: targetLineShape
             }, seriesModel, idx);
 
-            graphic.updateProps(labelText, {
-                style: targetTextStyle
-            }, seriesModel, idx);
+            graphic.updateProps(labelText, targetTextPos, seriesModel, idx);
         }
         else {
             labelLine.attr({
@@ -254,15 +252,11 @@ class PiePiece extends graphic.Sector {
             });
             // Make sure update style on labelText after setLabelStyle.
             // Because setLabelStyle will replace a new style on it.
-            labelText.attr({
-                style: targetTextStyle
-            });
+            labelText.attr(targetTextPos);
         }
 
         labelText.attr({
             rotation: labelLayout.rotation,
-            originX: labelLayout.x,
-            originY: labelLayout.y,
             z2: 10
         });
 
diff --git a/src/echarts.ts b/src/echarts.ts
index 4b29d76..b92fcd7 100644
--- a/src/echarts.ts
+++ b/src/echarts.ts
@@ -71,7 +71,7 @@ import IncrementalDisplayable from 
'zrender/src/graphic/IncrementalDisplayable';
 import 'zrender/src/canvas/canvas';
 import { seriesSymbolTask, dataSymbolTask } from './visual/symbol';
 import { getVisualFromData, getItemVisualFromData } from './visual/helper';
-import LabelManager from './util/LabelManager';
+import LabelManager from './label/LabelManager';
 
 declare let global: any;
 type ModelFinder = modelUtil.ModelFinder;
diff --git a/src/util/LabelManager.ts b/src/label/LabelManager.ts
similarity index 92%
rename from src/util/LabelManager.ts
rename to src/label/LabelManager.ts
index 48c6988..276ac1b 100644
--- a/src/util/LabelManager.ts
+++ b/src/label/LabelManager.ts
@@ -17,7 +17,16 @@
 * under the License.
 */
 
-import { OrientedBoundingRect, Text as ZRText, Point, BoundingRect, getECData 
} from './graphic';
+// TODO: move labels out of viewport.
+
+import {
+    OrientedBoundingRect,
+    Text as ZRText,
+    Point,
+    BoundingRect,
+    getECData,
+    Polyline
+} from '../util/graphic';
 import { MatrixArray } from 'zrender/src/core/matrix';
 import ExtensionAPI from '../ExtensionAPI';
 import {
@@ -26,12 +35,13 @@ import {
     LabelLayoutOption,
     LabelLayoutOptionCallback,
     LabelLayoutOptionCallbackParams
-} from './types';
-import { parsePercent } from './number';
+} from '../util/types';
+import { parsePercent } from '../util/number';
 import ChartView from '../view/Chart';
-import { ElementTextConfig } from 'zrender/src/Element';
+import { ElementTextConfig, ElementTextGuideLineConfig } from 
'zrender/src/Element';
 import { RectLike } from 'zrender/src/core/BoundingRect';
 import Transformable from 'zrender/src/core/Transformable';
+import { updateLabelGuideLine } from './labelGuideHelper';
 
 interface DisplayedLabelItem {
     label: ZRText
@@ -44,8 +54,11 @@ interface DisplayedLabelItem {
 
 interface LabelLayoutDesc {
     label: ZRText
+    labelGuide: Polyline
+
     seriesIndex: number
     dataIndex: number
+
     layoutOption: LabelLayoutOptionCallback | LabelLayoutOption
 
     overlap: LabelLayoutOption['overlap']
@@ -59,6 +72,7 @@ interface LabelLayoutDesc {
 
 interface SavedLabelAttr {
     ignore: boolean
+    labelGuideIgnore: boolean
 
     x: number
     y: number
@@ -151,10 +165,15 @@ class LabelManager {
             BoundingRect.applyTransform(hostRect, hostRect, transform);
         }
 
+        const labelGuide = hostRect && host.getTextGuideLine();
+
         this._labelList.push({
+            label,
+            labelGuide: labelGuide,
+
             seriesIndex,
             dataIndex,
-            label,
+
             layoutOption,
 
             hostRect,
@@ -170,6 +189,7 @@ class LabelManager {
             // For restore if developers want get back to default value in 
callback.
             defaultAttr: {
                 ignore: label.ignore,
+                labelGuideIgnore: labelGuide && labelGuide.ignore,
 
                 x: dummyTransformable.x,
                 y: dummyTransformable.y,
@@ -246,7 +266,7 @@ class LabelManager {
             if (layoutOption.x != null) {
                 // TODO width of chart view.
                 label.x = parsePercent(layoutOption.x, width);
-                label.setStyle('x', 0);  // Ignore movement in style.
+                label.setStyle('x', 0);  // Ignore movement in style. TODO: 
origin.
             }
             else {
                 label.x = defaultLabelAttr.x;
@@ -336,16 +356,19 @@ class LabelManager {
                 }
             }
 
+            const labelGuide = labelItem.labelGuide;
             // TODO Callback to determine if this overlap should be handled?
             if (overlapped) {
                 // label.setStyle({ opacity: 0.1 });
                 // label.z = 0;
                 label.hide();
+                labelGuide && labelGuide.hide();
             }
             else {
                 // TODO Restore z
                 // label.setStyle({ opacity: 1 });
                 label.attr('ignore', labelItem.defaultAttr.ignore);
+                labelGuide && labelGuide.attr('ignore', 
labelItem.defaultAttr.labelGuideIgnore);
 
                 displayedLabels.push({
                     label,
@@ -356,11 +379,14 @@ class LabelManager {
                     transform
                 });
             }
-        }
-    }
-
-    updateLabelGuidLine() {
 
+            updateLabelGuideLine(
+                label,
+                globalRect,
+                label.__hostTarget,
+                labelItem.hostRect
+            );
+        }
     }
 }
 
diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts
new file mode 100644
index 0000000..b20511a
--- /dev/null
+++ b/src/label/labelGuideHelper.ts
@@ -0,0 +1,366 @@
+/*
+* 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 {
+    Text as ZRText,
+    Point,
+    Path
+} 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';
+
+const PI2 = Math.PI * 2;
+const CMD = PathProxy.CMD;
+
+const DEFAULT_SEARCH_SPACE = ['top', 'right', 'bottom', 'left'] as const;
+
+type CandidatePosition = typeof DEFAULT_SEARCH_SPACE[number];
+
+function getCandidateAnchor(
+    pos: CandidatePosition,
+    distance: number,
+    rect: RectLike,
+    outPt: Point,
+    outDir: Point
+) {
+    const width = rect.width;
+    const height = rect.height;
+    switch (pos) {
+        case 'top':
+            outPt.set(
+                rect.x + width / 2,
+                rect.y - distance
+            );
+            outDir.set(0, -1);
+            break;
+        case 'bottom':
+            outPt.set(
+                rect.x + width / 2,
+                rect.y + height + distance
+            );
+            outDir.set(0, 1);
+            break;
+        case 'left':
+            outPt.set(
+                rect.x - distance,
+                rect.y + height / 2
+            );
+            outDir.set(-1, 0);
+            break;
+        case 'right':
+            outPt.set(
+                rect.x + width + distance,
+                rect.y + height / 2
+            );
+            outDir.set(1, 0);
+            break;
+    }
+}
+
+
+function projectPointToArc(
+    cx: number, cy: number, r: number, startAngle: number, endAngle: number, 
anticlockwise: boolean,
+    x: number, y: number, out: number[]
+): number {
+    x -= cx;
+    y -= cy;
+    const d = Math.sqrt(x * x + y * y);
+    x /= d;
+    y /= d;
+
+    // Intersect point.
+    const ox = x * r + cx;
+    const oy = y * r + cy;
+
+    if (Math.abs(startAngle - endAngle) % PI2 < 1e-4) {
+        // Is a circle
+        out[0] = ox;
+        out[1] = oy;
+        return d - r;
+    }
+
+    if (anticlockwise) {
+        const tmp = startAngle;
+        startAngle = normalizeRadian(endAngle);
+        endAngle = normalizeRadian(tmp);
+    }
+    else {
+        startAngle = normalizeRadian(startAngle);
+        endAngle = normalizeRadian(endAngle);
+    }
+    if (startAngle > endAngle) {
+        endAngle += PI2;
+    }
+
+    let angle = Math.atan2(y, x);
+    if (angle < 0) {
+        angle += PI2;
+    }
+    if ((angle >= startAngle && angle <= endAngle)
+        || (angle + PI2 >= startAngle && angle + PI2 <= endAngle)) {
+        // Project point is on the arc.
+        out[0] = ox;
+        out[1] = oy;
+        return d - r;
+    }
+
+    const x1 = r * Math.cos(startAngle) + cx;
+    const y1 = r * Math.sin(startAngle) + cy;
+
+    const x2 = r * Math.cos(endAngle) + cx;
+    const y2 = r * Math.sin(endAngle) + cy;
+
+    const d1 = (x1 - x) * (x1 - x) + (y1 - y) * (y1 - y);
+    const d2 = (x2 - x) * (x2 - x) + (y2 - y) * (y2 - y);
+
+    if (d1 < d2) {
+        out[0] = x1;
+        out[1] = y1;
+        return Math.sqrt(d1);
+    }
+    else {
+        out[0] = x2;
+        out[1] = y2;
+        return Math.sqrt(d2);
+    }
+}
+
+function projectPointToLine(x1: number, y1: number, x2: number, y2: number, x: 
number, y: number, out: number[]) {
+    const dx = x - x1;
+    const dy = y - y1;
+
+    let dx1 = x2 - x1;
+    let dy1 = y2 - y1;
+
+    const lineLen = Math.sqrt(dx1 * dx1 + dy1 * dy1);
+    dx1 /= lineLen;
+    dy1 /= lineLen;
+
+    // dot product
+    const projectedLen = dx * dx1 + dy * dy1;
+    const t = Math.min(Math.max(projectedLen / lineLen, 0), 1);
+    const ox = out[0] = x1 + t * dx1;
+    const oy = out[1] = y1 + t * dy1;
+
+    return Math.sqrt((ox - x) * (ox - x) + (oy - y) * (oy - y));
+}
+
+function projectPointToRect(
+    x1: number, y1: number, width: number, height: number, x: number, y: 
number, out: number[]
+): number {
+    if (width < 0) {
+        x1 = x1 + width;
+        width = -width;
+    }
+    if (height < 0) {
+        y1 = y1 + height;
+        height = -height;
+    }
+    const x2 = x1 + width;
+    const y2 = y1 + height;
+
+    const ox = out[0] = Math.min(Math.max(x, x1), x2);
+    const oy = out[1] = Math.min(Math.max(y, y1), y2);
+
+    return Math.sqrt((ox - x) * (ox - x) + (oy - y) * (oy - y));
+}
+
+const tmpPt: number[] = [];
+
+function nearestPointOnRect(pt: Point, rect: RectLike, out: Point) {
+    const dist = projectPointToRect(
+        rect.x, rect.y, rect.width, rect.height,
+        pt.x, pt.y, tmpPt
+    );
+    out.set(tmpPt[0], tmpPt[1]);
+    return dist;
+}
+/**
+ * Calculate min distance corresponding point.
+ * This method won't evaluate if point is in the path.
+ */
+function nearestPointOnPath(pt: Point, path: PathProxy, out: Point) {
+    let xi = 0;
+    let yi = 0;
+    let x0 = 0;
+    let y0 = 0;
+    let x1;
+    let y1;
+
+    let minDist = Infinity;
+
+    const data = path.data;
+    const x = pt.x;
+    const y = pt.y;
+
+    for (let i = 0; i < data.length;) {
+        const cmd = data[i++];
+
+        if (i === 1) {
+            xi = data[i];
+            yi = data[i + 1];
+            x0 = xi;
+            y0 = yi;
+        }
+
+        let d = minDist;
+
+        switch (cmd) {
+            case CMD.M:
+                // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点
+                // 在 closePath 的时候使用
+                x0 = data[i++];
+                y0 = data[i++];
+                xi = x0;
+                yi = y0;
+                break;
+            case CMD.L:
+                d = projectPointToLine(xi, yi, data[i], data[i + 1], x, y, 
tmpPt);
+                xi = data[i++];
+                yi = data[i++];
+                break;
+            case CMD.C:
+                d = cubicProjectPoint(
+                    xi, yi,
+                    data[i++], data[i++], data[i++], data[i++], data[i], 
data[i + 1],
+                    x, y, tmpPt
+                );
+
+                xi = data[i++];
+                yi = data[i++];
+                break;
+            case CMD.Q:
+                d = quadraticProjectPoint(
+                    xi, yi,
+                    data[i++], data[i++], data[i], data[i + 1],
+                    x, y, tmpPt
+                );
+                xi = data[i++];
+                yi = data[i++];
+                break;
+            case CMD.A:
+                // TODO Arc 判断的开销比较大
+                const cx = data[i++];
+                const cy = data[i++];
+                const rx = data[i++];
+                const ry = data[i++];
+                const theta = data[i++];
+                const dTheta = data[i++];
+                // TODO Arc 旋转
+                i += 1;
+                const anticlockwise = !!(1 - data[i++]);
+                x1 = Math.cos(theta) * rx + cx;
+                y1 = Math.sin(theta) * ry + cy;
+                // 不是直接使用 arc 命令
+                if (i <= 1) {
+                    // 第一个命令起点还未定义
+                    x0 = x1;
+                    y0 = y1;
+                }
+                // zr 使用scale来模拟椭圆, 这里也对x做一定的缩放
+                const _x = (x - cx) * ry / rx + cx;
+                d = projectPointToArc(
+                    cx, cy, ry, theta, theta + dTheta, anticlockwise,
+                    _x, y, tmpPt
+                );
+                xi = Math.cos(theta + dTheta) * rx + cx;
+                yi = Math.sin(theta + dTheta) * ry + cy;
+                break;
+            case CMD.R:
+                x0 = xi = data[i++];
+                y0 = yi = data[i++];
+                const width = data[i++];
+                const height = data[i++];
+                d = projectPointToRect(x0, y0, width, height, x, y, tmpPt);
+                break;
+            case CMD.Z:
+                d = projectPointToLine(xi, yi, x0, y0, x, y, tmpPt);
+
+                xi = x0;
+                yi = y0;
+                break;
+        }
+
+        if (d < minDist) {
+            minDist = d;
+            out.set(tmpPt[0], tmpPt[1]);
+        }
+    }
+
+    return minDist;
+}
+
+const pt0 = new Point();
+const pt1 = new Point();
+const pt2 = new Point();
+const dir = new Point();
+export function updateLabelGuideLine(
+    label: ZRText,
+    labelRect: RectLike,
+    target: Element,
+    targetRect: RectLike
+) {
+    if (!target) {
+        return;
+    }
+
+    const labelLine = target.getTextGuideLine();
+    // Needs to create text guide in each charts.
+    if (!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;
+
+    let minDist = Infinity;
+    const anchorPoint = labelGuideConfig && labelGuideConfig.anchor;
+    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);
+
+        const dist = anchorPoint ? anchorPoint.distance(pt1)
+            : (target instanceof Path
+                ? nearestPointOnPath(pt1, target.path, pt2)
+                : nearestPointOnRect(pt1, targetRect, pt2));
+
+        // TODO pt2 is in the path
+        if (dist < minDist) {
+            minDist = dist;
+            pt0.toArray(points[0]);
+            pt1.toArray(points[1]);
+            pt2.toArray(points[2]);
+        }
+    }
+
+    labelLine.setShape({ points });
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to