You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by GitBox <gi...@apache.org> on 2021/08/11 18:12:42 UTC

[GitHub] [echarts] 1774150545 opened a new pull request #15510: feat(scatter): remove label overlap for scatter point #15509

1774150545 opened a new pull request #15510:
URL: https://github.com/apache/echarts/pull/15510


   <!-- Please fill in the following information to help us review your PR more efficiently. -->
   
   ## Brief Information
   
   This pull request is in the type of:
   
   - [ ] bug fixing
   - [x] new feature
   - [ ] others
   
   
   
   ### What does this PR do?
   
   <!-- USE ONCE SENTENCE TO DESCRIBE WHAT THIS PR DOES. -->
   remove label overlap for scatter point
   
   
   ### Fixed issues
   #15509
   <!--
   - #xxxx: ...
   -->
   
   
   ## Details
   
   
   ### Before: What was the problem?
   
   <!-- DESCRIBE THE BUG OR REQUIREMENT HERE. -->
   
   <!-- ADD SCREENSHOT HERE IF APPLICABLE. -->
   
   ### After: How is it fixed in this PR?
   
   <!-- THE RESULT AFTER FIXING AND A SIMPLE EXPLANATION ABOUT HOW IT IS FIXED. -->
   
   <!-- ADD SCREENSHOT HERE IF APPLICABLE. -->
   ![image](https://user-images.githubusercontent.com/42894308/129080475-870b860f-d72a-47bd-85f9-1ed99e6e3180.png)
   
   
   ## Misc
   
   <!-- ADD RELATED ISSUE ID WHEN APPLICABLE -->
   
   - [ ] The API has been changed (apache/echarts-doc#xxx).
   - [ ] This PR depends on ZRender changes (ecomfe/zrender#xxx).
   
   ### Related test cases or examples to use the new APIs
   scatter-remove-overlap.html
   radar-remove-overlap.html
   
   
   ## Others
   
   ### Merging options
   
   - [ ] Please squash the commits into a single one when merge.
   
   ### Other information
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [echarts] echarts-bot[bot] commented on pull request #15510: feat(scatter): remove label overlap for scatter point #15509

Posted by GitBox <gi...@apache.org>.
echarts-bot[bot] commented on pull request #15510:
URL: https://github.com/apache/echarts/pull/15510#issuecomment-897042909


   Thanks for your contribution!
   The community will review it ASAP. In the meanwhile, please checkout [the coding standard](https://echarts.apache.org/en/coding-standard.html) and Wiki about [How to make a pull request](https://github.com/apache/echarts/wiki/How-to-make-a-pull-request).


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [echarts] pissang commented on a change in pull request #15510: feat(scatter): remove label overlap for scatter point #15509

Posted by GitBox <gi...@apache.org>.
pissang commented on a change in pull request #15510:
URL: https://github.com/apache/echarts/pull/15510#discussion_r700696021



##########
File path: src/label/labelLayoutHelper.ts
##########
@@ -362,4 +372,318 @@ export function hideOverlap(labelList: LabelLayoutInfo[]) {
             displayedLabels.push(labelItem);
         }
     }
+}
+
+/**
+ * remove label overlaps
+ */
+export function removeOverlap(

Review comment:
       This method can be moved out to an individual file and named with a specific algorithm.

##########
File path: src/util/types.ts
##########
@@ -1198,6 +1198,11 @@ export interface LabelLayoutOption {
      * @default 'none'
      */
     hideOverlap?: boolean
+    /**
+     * If labels overlap. It will Remove overlaps;
+     * @default 'none'
+     */
+    removeOverlap?: boolean

Review comment:
       `moveOverlap` is used for different label layout algorithms. Perhaps we can add a value to it to represent this default layout algorithm.
   ```ts
   // Apply default layout algorithm. Which will be this one I think.
   moveOverlap: true
   // Or name of this specific layout algorithm.
   moverOverlap: 'xxx'
   ```




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [echarts] pissang commented on a change in pull request #15510: feat(scatter): remove label overlap for scatter point #15509

Posted by GitBox <gi...@apache.org>.
pissang commented on a change in pull request #15510:
URL: https://github.com/apache/echarts/pull/15510#discussion_r716194907



##########
File path: test/fix-15509.html
##########
@@ -0,0 +1,58 @@
+<html>

Review comment:
       Please use a more specific name for the test case. You create a single test file for this algorithm. Run
   ```ts
   npm run mktest testName
   ```

##########
File path: src/label/repelLabels.ts
##########
@@ -0,0 +1,370 @@
+/*
+* 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 ZRText from 'zrender/src/graphic/Text';
+import { BoundingRect, Point } from '../util/graphic';
+import { RectLike } from 'zrender/src/core/BoundingRect';
+
+interface RepelLabelLayoutInput {
+    label: ZRText
+    rect: BoundingRect
+}
+
+export interface RepelLabelLayoutInfo{
+    label: ZRText
+    rect: BoundingRect
+    hostRect: RectLike
+}
+
+function prepareRepelLabelLayoutInfo(input: RepelLabelLayoutInput[]): RepelLabelLayoutInfo[] {
+    const list: RepelLabelLayoutInfo[] = [];
+    for (let i = 0; i < input.length; i++) {
+        const rawItem = input[i];
+        const label = rawItem.label;
+        const rect = rawItem.rect;
+        const host = label.__hostTarget;
+        let hostRect;
+        if (host) {
+            hostRect = host.getBoundingRect().plain();
+            const transform = host.getComputedTransform();
+            BoundingRect.applyTransform(hostRect, hostRect, transform);
+        }
+        list.push({
+            label,
+            rect,
+            hostRect
+        });
+    }
+    return list;
+}
+
+/**
+ * remove label overlaps by repelling
+ */
+export function shiftLayoutByRepelling(
+    labelListInput: RepelLabelLayoutInput[],
+    leftBound: number,
+    rightBound: number,
+    lowerBound: number,
+    upperBound: number,
+    forcePush: number = 1e-6,
+    forcePull: number = 1e-4,
+    maxIter: number = 3000,
+    xBounds: Point = new Point(leftBound, rightBound),
+    yBounds: Point = new Point(lowerBound, upperBound),
+    friction: number = 0.7
+) {
+    const labelList = prepareRepelLabelLayoutInfo(labelListInput);
+    const xScale = rightBound - leftBound;
+    const yScale = upperBound - lowerBound;
+
+    //Rescale x、y to be in the range [0,1]
+    function zoomOut() {
+        for (let i = 0; i < labelList.length; i++) {
+            const rect = labelList[i].rect;
+            const host = labelList[i].hostRect;
+            const label = labelList[i].label;
+
+            rect.x = (rect.x - leftBound) / xScale;
+            rect.y = (rect.y - lowerBound) / yScale;
+            rect.width /= xScale;
+            rect.height /= yScale;
+            host.x = (host.x - leftBound) / xScale;
+            host.y = (host.y - lowerBound) / yScale;
+            host.width /= xScale;
+            host.height /= yScale;
+            label.x = (label.x - leftBound) / xScale;
+            label.y = (label.y - lowerBound) / yScale;
+        }
+    }
+
+    // zoomIn x、y
+    function zoomIn() {
+        for (let i = 0; i < labelList.length; i++) {
+            const rect = labelList[i].rect;
+            const host = labelList[i].hostRect;
+            const label = labelList[i].label;
+
+            rect.x = rect.x * xScale + leftBound;
+            rect.y = rect.y * yScale + lowerBound;
+            rect.width *= xScale;
+            rect.height *= yScale;
+            host.x = host.x * xScale + leftBound;
+            host.y = host.y * yScale + lowerBound;
+            host.width *= xScale;
+            host.height *= yScale;
+            label.x = label.x * xScale + leftBound;
+            label.y = label.y * yScale + lowerBound;
+        }
+    }
+
+    // move rect center to circle center
+    function initPosition() {
+        for (let i = 0; i < labelList.length; i++) {
+            const item = labelList[i];
+            const host = item.hostRect;
+            const hostCenter = [host.x + host.width / 2, host.y + host.height / 2];
+            const rectCenter = [item.rect.x + item.rect.width / 2, item.rect.y + item.rect.height / 2];
+            const delta_x = hostCenter[0] - rectCenter[0];
+            const delta_y = hostCenter[1] - rectCenter[1];
+            item.rect.x += delta_x;
+            item.rect.y += delta_y;
+            item.label.x += delta_x;
+            item.label.y += delta_y;
+        }
+    }
+
+    // get force
+    function repelForce(
+        p1: Point, p2: Point, force = 0.0001
+    ) {
+        const dx = Math.abs(p1.x - p2.x);
+        const dy = Math.abs(p1.y - p2.y);
+        const d2 = Math.max(dx * dx + dy * dy, 0.0004);
+        const v = new Point();
+        const f = new Point();
+        Point.sub(v, p1, p2);
+        v.scale(1 / Math.sqrt(d2));
+        Point.scale(f, v, force / d2);
+        return f;
+    }
+
+    function moveRect(rect: RectLike, label: ZRText, move: Point) {
+        rect.x += move.x;
+        rect.y += move.y;
+        label.x += move.x;
+        label.y += move.y;
+    }
+
+    function springForce(p1: Point, p2: Point, force = 0.000001) {
+        const f = new Point();
+        const v = new Point();
+        Point.sub(v, p1, p2);
+        Point.scale(f, v, force);
+        return f;
+    }
+
+    function overlapsRect2(rect: RectLike, other: RectLike) {
+        const ax1 = rect.x;
+        const ax2 = rect.x + rect.width;
+        const ay1 = rect.y;
+        const ay2 = rect.y + rect.height;
+        const bx1 = other.x;
+        const bx2 = other.x + other.width;
+        const by1 = other.y;
+        const by2 = other.y + other.height;
+
+        return bx1 <= ax2 && by1 <= ay2
+            && bx2 >= ax1 && by2 >= ay1;
+    }
+
+    function lineIntersect(p1: Point, q1: Point, p2: Point, q2: Point) {
+        // returns true if two lines intersect, else false
+        const denom = (q2.y - p2.y) * (q1.x - p1.x) - (q2.x - p2.x) * (q1.y - p1.y);
+        const numera = (q2.x - p2.x) * (p1.y - p2.y) - (q2.y - p2.y) * (p1.x - p2.x);
+        const numerb = (q1.x - p1.x) * (p1.y - p2.y) - (q1.y - p1.y) * (p1.x - p2.x);
+
+        /* Is the intersection along the the segments */
+        const mua = numera / denom;
+        const mub = numerb / denom;
+        if (!(mua < 0 || mua > 1 || mub < 0 || mub > 1)) {
+            return true;
+        }
+        return false;
+    }
+
+    function putWithinBounds(rect: RectLike, label: ZRText, xlim: Point, ylim: Point) {
+        if (rect.x < xlim.x) {
+            rect.x = xlim.x;
+            label.x = xlim.x;
+        }
+        else if (rect.x + rect.width > xlim.y) {
+            rect.x = xlim.y - rect.width;
+            label.x = xlim.y - rect.width;
+        }
+        if (rect.y < ylim.x) {
+            rect.y = ylim.x;
+            label.y = ylim.y;
+        }
+        else if (rect.y + rect.height > ylim.y) {
+            rect.y = ylim.y - rect.height;
+            label.y = ylim.y - rect.height;
+        }
+    }
+
+    function isStuck(p1: Point, host1: Point, host2: Point, out: Point) {
+        const v1 = new Point(p1.x - host1.x, p1.y - host1.y);
+        const v2 = new Point(host2.x - p1.x, host2.y - p1.y);
+        // whether label is stuck by two data points
+        if (Math.abs(v1.x * v2.y - v2.x * v1.y - v2.x * v2.y) < 1e-5) {
+            out = p1.clone();
+            return true;
+        }
+        return false;
+    }
+
+    xBounds.scale(xScale);
+    yBounds.scale(yScale);
+    const forcePointSize = 100;
+    const maxOverlaps = 10;
+    const DataPoints: Point[] = [];
+    const LabelRects: BoundingRect[] = [];
+    const Labels: ZRText[] = [];
+    const velocities: Point[] = [];
+    const f = new Point();

Review comment:
       I can't quite understand what's these variables mean

##########
File path: src/util/types.ts
##########
@@ -1193,6 +1193,8 @@ export interface LabelLayoutOption {
         | 'shiftY'
         | 'shuffleX'
         | 'shuffleY'
+        | 'repel'

Review comment:
       Is `repel` the name of the algorithm?  What does it mean?
   
   From the code I guess it's like force directed layout.

##########
File path: src/label/repelLabels.ts
##########
@@ -0,0 +1,370 @@
+/*
+* 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 ZRText from 'zrender/src/graphic/Text';
+import { BoundingRect, Point } from '../util/graphic';
+import { RectLike } from 'zrender/src/core/BoundingRect';
+
+interface RepelLabelLayoutInput {
+    label: ZRText
+    rect: BoundingRect
+}
+
+export interface RepelLabelLayoutInfo{
+    label: ZRText
+    rect: BoundingRect
+    hostRect: RectLike
+}
+
+function prepareRepelLabelLayoutInfo(input: RepelLabelLayoutInput[]): RepelLabelLayoutInfo[] {
+    const list: RepelLabelLayoutInfo[] = [];
+    for (let i = 0; i < input.length; i++) {
+        const rawItem = input[i];
+        const label = rawItem.label;
+        const rect = rawItem.rect;
+        const host = label.__hostTarget;
+        let hostRect;
+        if (host) {
+            hostRect = host.getBoundingRect().plain();
+            const transform = host.getComputedTransform();
+            BoundingRect.applyTransform(hostRect, hostRect, transform);
+        }
+        list.push({
+            label,
+            rect,
+            hostRect
+        });
+    }
+    return list;
+}
+
+/**
+ * remove label overlaps by repelling
+ */
+export function shiftLayoutByRepelling(
+    labelListInput: RepelLabelLayoutInput[],
+    leftBound: number,
+    rightBound: number,
+    lowerBound: number,
+    upperBound: number,
+    forcePush: number = 1e-6,
+    forcePull: number = 1e-4,
+    maxIter: number = 3000,
+    xBounds: Point = new Point(leftBound, rightBound),
+    yBounds: Point = new Point(lowerBound, upperBound),
+    friction: number = 0.7
+) {
+    const labelList = prepareRepelLabelLayoutInfo(labelListInput);
+    const xScale = rightBound - leftBound;
+    const yScale = upperBound - lowerBound;
+
+    //Rescale x、y to be in the range [0,1]
+    function zoomOut() {
+        for (let i = 0; i < labelList.length; i++) {
+            const rect = labelList[i].rect;
+            const host = labelList[i].hostRect;
+            const label = labelList[i].label;
+
+            rect.x = (rect.x - leftBound) / xScale;
+            rect.y = (rect.y - lowerBound) / yScale;
+            rect.width /= xScale;
+            rect.height /= yScale;
+            host.x = (host.x - leftBound) / xScale;
+            host.y = (host.y - lowerBound) / yScale;
+            host.width /= xScale;
+            host.height /= yScale;
+            label.x = (label.x - leftBound) / xScale;
+            label.y = (label.y - lowerBound) / yScale;
+        }
+    }
+
+    // zoomIn x、y
+    function zoomIn() {
+        for (let i = 0; i < labelList.length; i++) {
+            const rect = labelList[i].rect;
+            const host = labelList[i].hostRect;
+            const label = labelList[i].label;
+
+            rect.x = rect.x * xScale + leftBound;
+            rect.y = rect.y * yScale + lowerBound;
+            rect.width *= xScale;
+            rect.height *= yScale;
+            host.x = host.x * xScale + leftBound;
+            host.y = host.y * yScale + lowerBound;
+            host.width *= xScale;
+            host.height *= yScale;
+            label.x = label.x * xScale + leftBound;
+            label.y = label.y * yScale + lowerBound;
+        }
+    }
+
+    // move rect center to circle center
+    function initPosition() {
+        for (let i = 0; i < labelList.length; i++) {
+            const item = labelList[i];
+            const host = item.hostRect;
+            const hostCenter = [host.x + host.width / 2, host.y + host.height / 2];
+            const rectCenter = [item.rect.x + item.rect.width / 2, item.rect.y + item.rect.height / 2];
+            const delta_x = hostCenter[0] - rectCenter[0];
+            const delta_y = hostCenter[1] - rectCenter[1];
+            item.rect.x += delta_x;
+            item.rect.y += delta_y;
+            item.label.x += delta_x;
+            item.label.y += delta_y;
+        }
+    }
+
+    // get force
+    function repelForce(
+        p1: Point, p2: Point, force = 0.0001
+    ) {
+        const dx = Math.abs(p1.x - p2.x);
+        const dy = Math.abs(p1.y - p2.y);
+        const d2 = Math.max(dx * dx + dy * dy, 0.0004);
+        const v = new Point();

Review comment:
       Avoid creating a new class every time. It will bring pressure to the memory

##########
File path: src/label/repelLabels.ts
##########
@@ -0,0 +1,370 @@
+/*
+* 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 ZRText from 'zrender/src/graphic/Text';
+import { BoundingRect, Point } from '../util/graphic';
+import { RectLike } from 'zrender/src/core/BoundingRect';
+
+interface RepelLabelLayoutInput {
+    label: ZRText
+    rect: BoundingRect
+}
+
+export interface RepelLabelLayoutInfo{
+    label: ZRText
+    rect: BoundingRect
+    hostRect: RectLike
+}
+
+function prepareRepelLabelLayoutInfo(input: RepelLabelLayoutInput[]): RepelLabelLayoutInfo[] {
+    const list: RepelLabelLayoutInfo[] = [];
+    for (let i = 0; i < input.length; i++) {
+        const rawItem = input[i];
+        const label = rawItem.label;
+        const rect = rawItem.rect;
+        const host = label.__hostTarget;
+        let hostRect;
+        if (host) {
+            hostRect = host.getBoundingRect().plain();
+            const transform = host.getComputedTransform();
+            BoundingRect.applyTransform(hostRect, hostRect, transform);
+        }
+        list.push({
+            label,
+            rect,
+            hostRect
+        });
+    }
+    return list;
+}
+
+/**
+ * remove label overlaps by repelling
+ */
+export function shiftLayoutByRepelling(
+    labelListInput: RepelLabelLayoutInput[],
+    leftBound: number,
+    rightBound: number,
+    lowerBound: number,
+    upperBound: number,
+    forcePush: number = 1e-6,
+    forcePull: number = 1e-4,
+    maxIter: number = 3000,
+    xBounds: Point = new Point(leftBound, rightBound),
+    yBounds: Point = new Point(lowerBound, upperBound),
+    friction: number = 0.7
+) {
+    const labelList = prepareRepelLabelLayoutInfo(labelListInput);
+    const xScale = rightBound - leftBound;
+    const yScale = upperBound - lowerBound;
+
+    //Rescale x、y to be in the range [0,1]
+    function zoomOut() {
+        for (let i = 0; i < labelList.length; i++) {
+            const rect = labelList[i].rect;
+            const host = labelList[i].hostRect;
+            const label = labelList[i].label;
+
+            rect.x = (rect.x - leftBound) / xScale;
+            rect.y = (rect.y - lowerBound) / yScale;
+            rect.width /= xScale;
+            rect.height /= yScale;
+            host.x = (host.x - leftBound) / xScale;
+            host.y = (host.y - lowerBound) / yScale;
+            host.width /= xScale;
+            host.height /= yScale;
+            label.x = (label.x - leftBound) / xScale;
+            label.y = (label.y - lowerBound) / yScale;
+        }
+    }
+
+    // zoomIn x、y
+    function zoomIn() {
+        for (let i = 0; i < labelList.length; i++) {
+            const rect = labelList[i].rect;
+            const host = labelList[i].hostRect;
+            const label = labelList[i].label;
+
+            rect.x = rect.x * xScale + leftBound;
+            rect.y = rect.y * yScale + lowerBound;
+            rect.width *= xScale;
+            rect.height *= yScale;
+            host.x = host.x * xScale + leftBound;
+            host.y = host.y * yScale + lowerBound;
+            host.width *= xScale;
+            host.height *= yScale;
+            label.x = label.x * xScale + leftBound;
+            label.y = label.y * yScale + lowerBound;
+        }
+    }
+
+    // move rect center to circle center
+    function initPosition() {
+        for (let i = 0; i < labelList.length; i++) {
+            const item = labelList[i];
+            const host = item.hostRect;
+            const hostCenter = [host.x + host.width / 2, host.y + host.height / 2];
+            const rectCenter = [item.rect.x + item.rect.width / 2, item.rect.y + item.rect.height / 2];
+            const delta_x = hostCenter[0] - rectCenter[0];
+            const delta_y = hostCenter[1] - rectCenter[1];
+            item.rect.x += delta_x;
+            item.rect.y += delta_y;
+            item.label.x += delta_x;
+            item.label.y += delta_y;
+        }
+    }
+
+    // get force
+    function repelForce(
+        p1: Point, p2: Point, force = 0.0001
+    ) {
+        const dx = Math.abs(p1.x - p2.x);
+        const dy = Math.abs(p1.y - p2.y);
+        const d2 = Math.max(dx * dx + dy * dy, 0.0004);
+        const v = new Point();
+        const f = new Point();
+        Point.sub(v, p1, p2);
+        v.scale(1 / Math.sqrt(d2));
+        Point.scale(f, v, force / d2);
+        return f;
+    }
+
+    function moveRect(rect: RectLike, label: ZRText, move: Point) {
+        rect.x += move.x;
+        rect.y += move.y;
+        label.x += move.x;
+        label.y += move.y;
+    }
+
+    function springForce(p1: Point, p2: Point, force = 0.000001) {
+        const f = new Point();
+        const v = new Point();
+        Point.sub(v, p1, p2);
+        Point.scale(f, v, force);
+        return f;
+    }
+
+    function overlapsRect2(rect: RectLike, other: RectLike) {
+        const ax1 = rect.x;
+        const ax2 = rect.x + rect.width;
+        const ay1 = rect.y;
+        const ay2 = rect.y + rect.height;
+        const bx1 = other.x;
+        const bx2 = other.x + other.width;
+        const by1 = other.y;
+        const by2 = other.y + other.height;
+
+        return bx1 <= ax2 && by1 <= ay2
+            && bx2 >= ax1 && by2 >= ay1;
+    }
+
+    function lineIntersect(p1: Point, q1: Point, p2: Point, q2: Point) {
+        // returns true if two lines intersect, else false
+        const denom = (q2.y - p2.y) * (q1.x - p1.x) - (q2.x - p2.x) * (q1.y - p1.y);
+        const numera = (q2.x - p2.x) * (p1.y - p2.y) - (q2.y - p2.y) * (p1.x - p2.x);
+        const numerb = (q1.x - p1.x) * (p1.y - p2.y) - (q1.y - p1.y) * (p1.x - p2.x);
+
+        /* Is the intersection along the the segments */
+        const mua = numera / denom;
+        const mub = numerb / denom;
+        if (!(mua < 0 || mua > 1 || mub < 0 || mub > 1)) {
+            return true;
+        }
+        return false;
+    }
+
+    function putWithinBounds(rect: RectLike, label: ZRText, xlim: Point, ylim: Point) {
+        if (rect.x < xlim.x) {
+            rect.x = xlim.x;
+            label.x = xlim.x;
+        }
+        else if (rect.x + rect.width > xlim.y) {
+            rect.x = xlim.y - rect.width;
+            label.x = xlim.y - rect.width;
+        }
+        if (rect.y < ylim.x) {
+            rect.y = ylim.x;
+            label.y = ylim.y;
+        }
+        else if (rect.y + rect.height > ylim.y) {
+            rect.y = ylim.y - rect.height;
+            label.y = ylim.y - rect.height;
+        }
+    }
+
+    function isStuck(p1: Point, host1: Point, host2: Point, out: Point) {
+        const v1 = new Point(p1.x - host1.x, p1.y - host1.y);
+        const v2 = new Point(host2.x - p1.x, host2.y - p1.y);
+        // whether label is stuck by two data points
+        if (Math.abs(v1.x * v2.y - v2.x * v1.y - v2.x * v2.y) < 1e-5) {
+            out = p1.clone();
+            return true;
+        }
+        return false;
+    }
+
+    xBounds.scale(xScale);
+    yBounds.scale(yScale);
+    const forcePointSize = 100;
+    const maxOverlaps = 10;
+    const DataPoints: Point[] = [];
+    const LabelRects: BoundingRect[] = [];
+    const Labels: ZRText[] = [];
+    const velocities: Point[] = [];
+    const f = new Point();
+    const ci = new Point();
+    const cj = new Point();
+    const overlapCount = [];
+    const tooManyOverlaps = [];
+    let iter = 0;
+    let totalOverlaps = 1;
+    let iOverlaps = false;
+
+    // init
+    zoomOut();
+    initPosition();
+    for (let i = 0; i < labelList.length; i++) {
+        const item = labelList[i];
+        Labels[i] = item.label;
+        LabelRects[i] = item.rect;
+        DataPoints[i] = new Point();
+        DataPoints[i].set(item.hostRect.x + item.hostRect.width / 2, item.hostRect.y + item.hostRect.height / 2);
+        // initialize velocities to zero
+        velocities[i] = new Point();
+        const randomMove = new Point((Math.random() - 0.5) / xScale, (Math.random() - 0.5) / yScale);
+        moveRect(LabelRects[i], Labels[i], randomMove);
+    }
+
+    while (totalOverlaps && iter < maxIter) {
+        iter += 1;
+        totalOverlaps = 0;
+        // forces get weaker over time.
+        forcePush *= 0.9999;
+        forcePull *= 0.9999;
+        for (let i = 0; i < labelList.length; i++) {
+            if (iter === 2 && overlapCount[i] > maxOverlaps) {
+                tooManyOverlaps[i] = true;
+            }
+            if (tooManyOverlaps[i]) {
+                continue;
+            }
+            overlapCount[i] = 0;
+            iOverlaps = false;
+            f.set(0, 0);
+            ci.set(LabelRects[i].x + LabelRects[i].width / 2, LabelRects[i].y + LabelRects[i].height / 2);
+            for (let j = 0; j < labelList.length; j++) {
+                if (i === j) {
+                    if (overlapsRect2(labelList[i].hostRect, LabelRects[i])) {
+                        totalOverlaps += 1;
+                        iOverlaps = true;
+                        overlapCount[i] += 1;
+                        f.add(repelForce(
+                            ci, DataPoints[i],
+                            forcePush * forcePointSize * 0.003
+                        ));
+
+                    }
+                }
+                else if (tooManyOverlaps[j]) {
+                    if (overlapsRect2(labelList[j].hostRect, LabelRects[i])) {
+                        totalOverlaps += 1;
+                        iOverlaps = true;
+                        overlapCount[i] += 1;
+                        f.add(repelForce(
+                            ci, DataPoints[j],
+                            forcePush * forcePointSize * 0.003
+                        ));
+                    }
+                }
+                else {
+                    cj.set(LabelRects[j].x + LabelRects[j].width / 2, LabelRects[j].y + LabelRects[j].height / 2);
+                    // Repel the box from other data points.
+                    if (overlapsRect2(labelList[j].hostRect, LabelRects[i])) {
+                        totalOverlaps += 1;
+                        iOverlaps = true;
+                        overlapCount[i] += 1;
+                        f.add(repelForce(ci, DataPoints[j],
+                            forcePush * forcePointSize * 0.003
+                        ));
+                    }
+                    if (overlapsRect2(LabelRects[i], LabelRects[j])) {
+                        totalOverlaps += 1;
+                        iOverlaps = true;
+                        overlapCount[i] += 1;
+                        f.add(repelForce(ci, cj, forcePush));
+                    }
+                    if (iter % 100 === 0 && overlapCount[i] === 2
+                        && overlapsRect2(LabelRects[i], labelList[i].hostRect)
+                        && overlapsRect2(LabelRects[i], labelList[j].hostRect)) {
+                        const v = new Point();
+                        const stuck = isStuck(ci, DataPoints[i], DataPoints[j], v);
+                        if (stuck) {
+                            v.scale(10);
+                            if (Math.random() < 0.5) {
+                                v.set(v.x, -v.y);
+                                moveRect(LabelRects[i], Labels[i], v);
+                            }
+                            else {
+                                v.set(-v.x, v.y);
+                                moveRect(LabelRects[i], Labels[i], v);
+                            }
+                        }
+                    }
+                }
+            }
+
+            // Pull the box.
+            if (!iOverlaps) {
+                f.add(springForce(DataPoints[i], ci, forcePull * forcePointSize));
+            }
+            let friction2 = 1.0;
+            if (overlapCount[i] > 10) {
+                friction2 += 0.5;
+            }
+            else {
+                friction2 += 0.05 * overlapCount[i];
+            }
+            velocities[i].scale(friction * friction2);
+            velocities[i].add(f);
+            moveRect(labelList[i].rect, Labels[i], velocities[i]);
+
+            // Put boxes within bounds
+            putWithinBounds(LabelRects[i], Labels[i], xBounds, yBounds);
+
+            // check line intersect
+            if (totalOverlaps === 0 || iter % 10 === 0) {
+                for (let j = 0; j < labelList.length; j++) {
+                    ci.set(labelList[i].rect.x + labelList[i].rect.width / 2,

Review comment:
       It's better to assign `labelList[i].rect` to a temporal variable for smaller code size.

##########
File path: README.md
##########
@@ -1,5 +1,28 @@
 # Apache ECharts
 
+## fix-15509: moveOver label overap

Review comment:
       Please please don't commit this.

##########
File path: src/label/repelLabels.ts
##########
@@ -0,0 +1,370 @@
+/*
+* 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 ZRText from 'zrender/src/graphic/Text';
+import { BoundingRect, Point } from '../util/graphic';
+import { RectLike } from 'zrender/src/core/BoundingRect';
+
+interface RepelLabelLayoutInput {
+    label: ZRText
+    rect: BoundingRect
+}
+
+export interface RepelLabelLayoutInfo{
+    label: ZRText
+    rect: BoundingRect
+    hostRect: RectLike
+}
+
+function prepareRepelLabelLayoutInfo(input: RepelLabelLayoutInput[]): RepelLabelLayoutInfo[] {

Review comment:
       Why creating a new method for this?

##########
File path: src/label/repelLabels.ts
##########
@@ -0,0 +1,370 @@
+/*
+* 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 ZRText from 'zrender/src/graphic/Text';
+import { BoundingRect, Point } from '../util/graphic';
+import { RectLike } from 'zrender/src/core/BoundingRect';
+
+interface RepelLabelLayoutInput {
+    label: ZRText
+    rect: BoundingRect
+}
+
+export interface RepelLabelLayoutInfo{
+    label: ZRText
+    rect: BoundingRect
+    hostRect: RectLike
+}
+
+function prepareRepelLabelLayoutInfo(input: RepelLabelLayoutInput[]): RepelLabelLayoutInfo[] {
+    const list: RepelLabelLayoutInfo[] = [];
+    for (let i = 0; i < input.length; i++) {
+        const rawItem = input[i];
+        const label = rawItem.label;
+        const rect = rawItem.rect;
+        const host = label.__hostTarget;
+        let hostRect;
+        if (host) {
+            hostRect = host.getBoundingRect().plain();
+            const transform = host.getComputedTransform();
+            BoundingRect.applyTransform(hostRect, hostRect, transform);
+        }
+        list.push({
+            label,
+            rect,
+            hostRect
+        });
+    }
+    return list;
+}
+
+/**
+ * remove label overlaps by repelling
+ */
+export function shiftLayoutByRepelling(
+    labelListInput: RepelLabelLayoutInput[],
+    leftBound: number,
+    rightBound: number,
+    lowerBound: number,
+    upperBound: number,
+    forcePush: number = 1e-6,
+    forcePull: number = 1e-4,
+    maxIter: number = 3000,
+    xBounds: Point = new Point(leftBound, rightBound),
+    yBounds: Point = new Point(lowerBound, upperBound),
+    friction: number = 0.7
+) {
+    const labelList = prepareRepelLabelLayoutInfo(labelListInput);
+    const xScale = rightBound - leftBound;
+    const yScale = upperBound - lowerBound;
+
+    //Rescale x、y to be in the range [0,1]
+    function zoomOut() {
+        for (let i = 0; i < labelList.length; i++) {
+            const rect = labelList[i].rect;
+            const host = labelList[i].hostRect;
+            const label = labelList[i].label;
+
+            rect.x = (rect.x - leftBound) / xScale;
+            rect.y = (rect.y - lowerBound) / yScale;
+            rect.width /= xScale;
+            rect.height /= yScale;
+            host.x = (host.x - leftBound) / xScale;
+            host.y = (host.y - lowerBound) / yScale;
+            host.width /= xScale;
+            host.height /= yScale;
+            label.x = (label.x - leftBound) / xScale;
+            label.y = (label.y - lowerBound) / yScale;
+        }
+    }
+
+    // zoomIn x、y
+    function zoomIn() {
+        for (let i = 0; i < labelList.length; i++) {
+            const rect = labelList[i].rect;
+            const host = labelList[i].hostRect;
+            const label = labelList[i].label;
+
+            rect.x = rect.x * xScale + leftBound;
+            rect.y = rect.y * yScale + lowerBound;
+            rect.width *= xScale;
+            rect.height *= yScale;
+            host.x = host.x * xScale + leftBound;
+            host.y = host.y * yScale + lowerBound;
+            host.width *= xScale;
+            host.height *= yScale;
+            label.x = label.x * xScale + leftBound;
+            label.y = label.y * yScale + lowerBound;
+        }
+    }
+
+    // move rect center to circle center
+    function initPosition() {
+        for (let i = 0; i < labelList.length; i++) {
+            const item = labelList[i];
+            const host = item.hostRect;
+            const hostCenter = [host.x + host.width / 2, host.y + host.height / 2];
+            const rectCenter = [item.rect.x + item.rect.width / 2, item.rect.y + item.rect.height / 2];
+            const delta_x = hostCenter[0] - rectCenter[0];

Review comment:
       Use camelCase please

##########
File path: src/label/repelLabels.ts
##########
@@ -0,0 +1,370 @@
+/*
+* 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 ZRText from 'zrender/src/graphic/Text';
+import { BoundingRect, Point } from '../util/graphic';
+import { RectLike } from 'zrender/src/core/BoundingRect';
+
+interface RepelLabelLayoutInput {
+    label: ZRText
+    rect: BoundingRect
+}
+
+export interface RepelLabelLayoutInfo{
+    label: ZRText
+    rect: BoundingRect
+    hostRect: RectLike
+}
+
+function prepareRepelLabelLayoutInfo(input: RepelLabelLayoutInput[]): RepelLabelLayoutInfo[] {
+    const list: RepelLabelLayoutInfo[] = [];
+    for (let i = 0; i < input.length; i++) {
+        const rawItem = input[i];
+        const label = rawItem.label;
+        const rect = rawItem.rect;
+        const host = label.__hostTarget;
+        let hostRect;
+        if (host) {
+            hostRect = host.getBoundingRect().plain();
+            const transform = host.getComputedTransform();
+            BoundingRect.applyTransform(hostRect, hostRect, transform);
+        }
+        list.push({
+            label,
+            rect,
+            hostRect
+        });
+    }
+    return list;
+}
+
+/**
+ * remove label overlaps by repelling
+ */
+export function shiftLayoutByRepelling(
+    labelListInput: RepelLabelLayoutInput[],
+    leftBound: number,
+    rightBound: number,
+    lowerBound: number,
+    upperBound: number,
+    forcePush: number = 1e-6,
+    forcePull: number = 1e-4,
+    maxIter: number = 3000,
+    xBounds: Point = new Point(leftBound, rightBound),
+    yBounds: Point = new Point(lowerBound, upperBound),
+    friction: number = 0.7
+) {
+    const labelList = prepareRepelLabelLayoutInfo(labelListInput);
+    const xScale = rightBound - leftBound;
+    const yScale = upperBound - lowerBound;
+
+    //Rescale x、y to be in the range [0,1]
+    function zoomOut() {
+        for (let i = 0; i < labelList.length; i++) {
+            const rect = labelList[i].rect;
+            const host = labelList[i].hostRect;
+            const label = labelList[i].label;
+
+            rect.x = (rect.x - leftBound) / xScale;
+            rect.y = (rect.y - lowerBound) / yScale;
+            rect.width /= xScale;
+            rect.height /= yScale;
+            host.x = (host.x - leftBound) / xScale;
+            host.y = (host.y - lowerBound) / yScale;
+            host.width /= xScale;
+            host.height /= yScale;
+            label.x = (label.x - leftBound) / xScale;
+            label.y = (label.y - lowerBound) / yScale;
+        }
+    }
+
+    // zoomIn x、y
+    function zoomIn() {
+        for (let i = 0; i < labelList.length; i++) {
+            const rect = labelList[i].rect;
+            const host = labelList[i].hostRect;
+            const label = labelList[i].label;
+
+            rect.x = rect.x * xScale + leftBound;
+            rect.y = rect.y * yScale + lowerBound;
+            rect.width *= xScale;
+            rect.height *= yScale;
+            host.x = host.x * xScale + leftBound;
+            host.y = host.y * yScale + lowerBound;
+            host.width *= xScale;
+            host.height *= yScale;
+            label.x = label.x * xScale + leftBound;
+            label.y = label.y * yScale + lowerBound;
+        }
+    }
+
+    // move rect center to circle center
+    function initPosition() {
+        for (let i = 0; i < labelList.length; i++) {
+            const item = labelList[i];
+            const host = item.hostRect;
+            const hostCenter = [host.x + host.width / 2, host.y + host.height / 2];
+            const rectCenter = [item.rect.x + item.rect.width / 2, item.rect.y + item.rect.height / 2];
+            const delta_x = hostCenter[0] - rectCenter[0];
+            const delta_y = hostCenter[1] - rectCenter[1];
+            item.rect.x += delta_x;
+            item.rect.y += delta_y;
+            item.label.x += delta_x;
+            item.label.y += delta_y;
+        }
+    }
+
+    // get force
+    function repelForce(
+        p1: Point, p2: Point, force = 0.0001
+    ) {
+        const dx = Math.abs(p1.x - p2.x);
+        const dy = Math.abs(p1.y - p2.y);
+        const d2 = Math.max(dx * dx + dy * dy, 0.0004);
+        const v = new Point();
+        const f = new Point();
+        Point.sub(v, p1, p2);
+        v.scale(1 / Math.sqrt(d2));
+        Point.scale(f, v, force / d2);
+        return f;
+    }
+
+    function moveRect(rect: RectLike, label: ZRText, move: Point) {
+        rect.x += move.x;
+        rect.y += move.y;
+        label.x += move.x;
+        label.y += move.y;
+    }
+
+    function springForce(p1: Point, p2: Point, force = 0.000001) {
+        const f = new Point();
+        const v = new Point();
+        Point.sub(v, p1, p2);
+        Point.scale(f, v, force);
+        return f;
+    }
+
+    function overlapsRect2(rect: RectLike, other: RectLike) {

Review comment:
       There is an existing method for check if `rect` is overlapped.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [echarts] pissang commented on a change in pull request #15510: feat(scatter): remove label overlap for scatter point #15509

Posted by GitBox <gi...@apache.org>.
pissang commented on a change in pull request #15510:
URL: https://github.com/apache/echarts/pull/15510#discussion_r716195407



##########
File path: README.md
##########
@@ -1,5 +1,28 @@
 # Apache ECharts
 
+## fix-15509: moveOver label overap

Review comment:
       Please don't commit this.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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