From ad861db533706b6a1b34625a2346d8a7ea97a02c Mon Sep 17 00:00:00 2001 From: quao Date: Wed, 31 Dec 2025 18:50:58 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat=20=E8=87=AA=E5=8A=A8=E4=B8=8A=E4=B8=8B?= =?UTF-8?q?=E5=81=8F=E7=A7=BB=E6=97=B6=EF=BC=8C=E4=B8=8D=E5=BA=94=E8=B6=85?= =?UTF-8?q?=E5=87=BA=E5=BD=93=E5=89=8Dgrid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/label/LabelManager.ts | 88 ++++++++++++++++++++++++++++++++-- src/label/labelLayoutHelper.ts | 5 +- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index b60a9ac2b2..01a4c17a95 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -444,9 +444,11 @@ class LabelManager { const height = api.getHeight(); const labelList: LabelLayoutWithGeometry[] = []; + each(this._labelList, inputItem => { if (!inputItem.defaultAttr.ignore) { - labelList.push(newLabelLayoutWithGeometry({}, inputItem)); + const labelLayout = newLabelLayoutWithGeometry({}, inputItem); + labelList.push(labelLayout); } }); @@ -457,8 +459,88 @@ class LabelManager { return item.layoutOption.moveOverlap === 'shiftY'; }); - shiftLayoutOnXY(labelsNeedsAdjustOnX, 0, 0, width); - shiftLayoutOnXY(labelsNeedsAdjustOnY, 1, 0, height); + // Group labels by grid (not by series) so that labels of multiple series + // in the same grid avoid overlapping each other. + const labelsByGridX: Dictionary = {}; + const labelsByGridY: Dictionary = {}; + + function getGridKey(inputItem: LabelLayoutWithGeometry): string { + const seriesModel = inputItem.seriesModel; + const gridId = (seriesModel as any).get('gridId'); + const gridIndex = (seriesModel as any).get('gridIndex'); + const master = seriesModel.coordinateSystem && (seriesModel.coordinateSystem as any).master; + // Prefer explicit gridId, then gridIndex, then master.uid as fallback + return gridId != null + ? `gid:${gridId}` + : gridIndex != null + ? `gidx:${gridIndex}` + : master && master.uid + ? `gmaster:${master.uid}` + : 'grid:default'; + } + + each(labelsNeedsAdjustOnX, function (item) { + const seriesModel = item.seriesModel; + if (seriesModel) { + const key = getGridKey(item); + if (!labelsByGridX[key]) { + labelsByGridX[key] = []; + } + labelsByGridX[key].push(item); + } + }); + + each(labelsNeedsAdjustOnY, function (item) { + const seriesModel = item.seriesModel; + if (seriesModel) { + const key = getGridKey(item); + if (!labelsByGridY[key]) { + labelsByGridY[key] = []; + } + labelsByGridY[key].push(item); + } + }); + + // Adjust labels for each grid with its bounds + each(labelsByGridX, function (gridLabels) { + const seriesModel = gridLabels[0].seriesModel; + if (!seriesModel) { + return; + } + const coordSys = seriesModel.coordinateSystem; + let minBound = 0; + let maxBound = width; + + if (coordSys && coordSys.master && (coordSys.master as any).getRect) { + const gridRect = (coordSys.master as any).getRect(); + if (gridRect) { + minBound = gridRect.x; + maxBound = gridRect.x + gridRect.width; + } + } + + shiftLayoutOnXY(gridLabels, 0, minBound, maxBound); + }); + + each(labelsByGridY, function (gridLabels) { + const seriesModel = gridLabels[0].seriesModel; + if (!seriesModel) { + return; + } + const coordSys = seriesModel.coordinateSystem; + let minBound = 0; + let maxBound = height; + + if (coordSys && coordSys.master && (coordSys.master as any).getRect) { + const gridRect = (coordSys.master as any).getRect(); + if (gridRect) { + minBound = gridRect.y; + maxBound = gridRect.y + gridRect.height; + } + } + + shiftLayoutOnXY(gridLabels, 1, minBound, maxBound); + }); const labelsNeedsHideOverlap = filter(labelList, function (item) { return item.layoutOption.hideOverlap; diff --git a/src/label/labelLayoutHelper.ts b/src/label/labelLayoutHelper.ts index 1136bb63d8..1d461433e3 100644 --- a/src/label/labelLayoutHelper.ts +++ b/src/label/labelLayoutHelper.ts @@ -19,6 +19,7 @@ import ZRText from 'zrender/src/graphic/Text'; import { LabelLayoutOption, NullUndefined } from '../util/types'; +import SeriesModel from '../model/Series'; import { BoundingRect, OrientedBoundingRect, Polyline, WH, XY, ensureCopyRect, ensureCopyTransform, expandOrShrinkRect, isBoundingRectAxisAligned @@ -37,6 +38,8 @@ interface LabelLayoutBase { labelLine?: Polyline | NullUndefined layoutOption?: LabelLayoutOption | NullUndefined priority: number + // Keep series model reference for later grouping without extra maps. + seriesModel?: SeriesModel // @see `SavedLabelAttr` in `LabelManager.ts` defaultAttr: { ignore?: boolean @@ -65,7 +68,7 @@ interface LabelLayoutBase { suggestIgnore?: boolean; } const LABEL_LAYOUT_BASE_PROPS = [ - 'label', 'labelLine', 'layoutOption', 'priority', 'defaultAttr', + 'label', 'labelLine', 'layoutOption', 'priority', 'seriesModel', 'defaultAttr', 'marginForce', 'minMarginForce', 'marginDefault', 'suggestIgnore' ] as const; From 813307615b676a8ee9c89be8a136ce58ca11bb5f Mon Sep 17 00:00:00 2001 From: quao Date: Tue, 6 Jan 2026 17:09:37 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat=20=E6=B7=BB=E5=8A=A0=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/label-overlap-grid.html | 241 +++++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 test/label-overlap-grid.html diff --git a/test/label-overlap-grid.html b/test/label-overlap-grid.html new file mode 100644 index 0000000000..2ba0764ea5 --- /dev/null +++ b/test/label-overlap-grid.html @@ -0,0 +1,241 @@ + + + + + + + + + + + + +
多 grid 折线图 label overlap
+
+
单 grid 折线图 label overlap
+
+ + + From 83039f89dec12984e87fd3e09155968db4bde4cb Mon Sep 17 00:00:00 2001 From: quao Date: Fri, 9 Jan 2026 18:07:54 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat=20=E4=BB=85=E4=BD=86=E7=BB=B4=E5=BA=A6?= =?UTF-8?q?=E9=87=8D=E5=8F=A0=E6=97=B6=E4=B8=8D=E5=81=8F=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/label/LabelManager.ts | 20 +++------ src/label/labelLayoutHelper.ts | 78 +++++++++++++++++++++++++++------- 2 files changed, 68 insertions(+), 30 deletions(-) diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index 01a4c17a95..e772328971 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -464,25 +464,15 @@ class LabelManager { const labelsByGridX: Dictionary = {}; const labelsByGridY: Dictionary = {}; - function getGridKey(inputItem: LabelLayoutWithGeometry): string { - const seriesModel = inputItem.seriesModel; - const gridId = (seriesModel as any).get('gridId'); - const gridIndex = (seriesModel as any).get('gridIndex'); - const master = seriesModel.coordinateSystem && (seriesModel.coordinateSystem as any).master; - // Prefer explicit gridId, then gridIndex, then master.uid as fallback - return gridId != null - ? `gid:${gridId}` - : gridIndex != null - ? `gidx:${gridIndex}` - : master && master.uid - ? `gmaster:${master.uid}` - : 'grid:default'; + function getGridName(inputItem: LabelLayoutWithGeometry): string { + const seriesMaster = inputItem?.seriesModel?.coordinateSystem?.master as any; + return seriesMaster?.name || 'grid_default'; } each(labelsNeedsAdjustOnX, function (item) { const seriesModel = item.seriesModel; if (seriesModel) { - const key = getGridKey(item); + const key = getGridName(item); if (!labelsByGridX[key]) { labelsByGridX[key] = []; } @@ -493,7 +483,7 @@ class LabelManager { each(labelsNeedsAdjustOnY, function (item) { const seriesModel = item.seriesModel; if (seriesModel) { - const key = getGridKey(item); + const key = getGridName(item); if (!labelsByGridY[key]) { labelsByGridY[key] = []; } diff --git a/src/label/labelLayoutHelper.ts b/src/label/labelLayoutHelper.ts index 1d461433e3..dbcc1d9be3 100644 --- a/src/label/labelLayoutHelper.ts +++ b/src/label/labelLayoutHelper.ts @@ -338,6 +338,8 @@ export function shiftLayoutOnXY( const len = list.length; const xyDim = XY[xyDimIdx]; const sizeDim = WH[xyDimIdx]; + const otherDim = XY[xyDimIdx ? 0 : 1]; + const otherSizeDim = WH[xyDimIdx ? 0 : 1]; if (len < 2) { return false; @@ -347,27 +349,58 @@ export function shiftLayoutOnXY( return a.rect[xyDim] - b.rect[xyDim]; }); - let lastPos = 0; - let delta; let adjusted = false; - // const shifts = []; + // 仅在跨两个维度都重叠时才认为需要沿当前维度分散。 + // 使用扫线维护同一维度重叠的活动集合,避免宽度(或高度)无交集时误偏移。 + const active: { + end: number + otherStart: number + otherEnd: number + }[] = []; let totalShifts = 0; for (let i = 0; i < len; i++) { const item = list[i]; const rect = item.rect; - delta = rect[xyDim] - lastPos; + const start = rect[xyDim]; + const otherStart = rect[otherDim]; + const otherEnd = otherStart + rect[otherSizeDim]; + + // 移除在当前维度上已无交集的活动项 + for (let j = active.length - 1; j >= 0; j--) { + if (active[j].end <= start) { + active.splice(j, 1); + } + } + + // 找到与当前矩形在另一维度也有交集的最大尾部位置 + let maxOverlapEnd = start; + for (let j = 0; j < active.length; j++) { + const act = active[j]; + const overlapOther = otherStart < act.otherEnd && otherEnd > act.otherStart; + if (overlapOther) { + if (act.end > maxOverlapEnd) { + maxOverlapEnd = act.end; + } + } + } + + const delta = start - maxOverlapEnd; if (delta < 0) { - // shiftForward(i, len, -delta); - rect[xyDim] -= delta; - item.label[xyDim] -= delta; + const shift = -delta; + rect[xyDim] += shift; + item.label[xyDim] += shift; + totalShifts += shift; adjusted = true; } - const shift = Math.max(-delta, 0); - // shifts.push(shift); - totalShifts += shift; - lastPos = rect[xyDim] + rect[sizeDim]; + // 将(可能已移动的)当前矩形加入活动集 + const newStart = rect[xyDim]; + active.push({ + end: newStart + rect[sizeDim], + otherStart, + otherEnd + }); } if (totalShifts > 0 && balanceShift) { // Shift back to make the distribution more equally. @@ -437,10 +470,25 @@ export function shiftLayoutOnXY( const gaps: number[] = []; let totalGaps = 0; for (let i = 1; i < len; i++) { - const prevItemRect = list[i - 1].rect; - const gap = Math.max(list[i].rect[xyDim] - prevItemRect[xyDim] - prevItemRect[sizeDim], 0); - gaps.push(gap); - totalGaps += gap; + const currRect = list[i].rect; + let minGap = Infinity; + for (let j = 0; j < i; j++) { + const prevRect = list[j].rect; + const overlapOther = currRect[otherDim] < prevRect[otherDim] + prevRect[otherSizeDim] + && currRect[otherDim] + currRect[otherSizeDim] > prevRect[otherDim]; + if (overlapOther) { + const gap = currRect[xyDim] - (prevRect[xyDim] + prevRect[sizeDim]); + if (gap < minGap) { + minGap = gap; + } + } + } + if (minGap === Infinity) { + minGap = 0; + } + minGap = Math.max(minGap, 0); + gaps.push(minGap); + totalGaps += minGap; } if (!totalGaps) { return;