diff --git a/src/component/labels.ts b/src/component/labels.ts index dc3bd97e3e..95cfb58418 100644 --- a/src/component/labels.ts +++ b/src/component/labels.ts @@ -149,14 +149,12 @@ export default class Labels { } private renderLabel(cfg: LabelItem, container: IGroup) { - const { id, data, mappingData, coordinate, animate, content, offset } = cfg; + const { id, data, mappingData, coordinate, animate, content } = cfg; const shapeAppendCfg = { id, data, origin: mappingData, coordinate, - /** 把 labelItem 传递下去 */ - labelItem: cfg, }; const labelGroup = container.addGroup({ name: 'label', diff --git a/src/geometry/label/layout/distribute.ts b/src/geometry/label/layout/distribute.ts deleted file mode 100644 index e396e8db76..0000000000 --- a/src/geometry/label/layout/distribute.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { find } from '@antv/util'; -import { BBox, IGroup, IShape } from '../../../dependents'; - -/** label text和line距离 4px */ -const MARGIN = 4; - -function getEndPoint(center, angle, r) { - return { - x: center.x + r * Math.cos(angle), - y: center.y + r * Math.sin(angle), - }; -} - -function antiCollision(labelGroups: IGroup[], lineHeight, plotRange, center, isRight) { - const labels = labelGroups.map((labelShape) => labelShape.get('labelItem')); - // adjust y position of labels to avoid overlapping - let overlapping = true; - const start = plotRange.start; - const end = plotRange.end; - const startY = Math.min(start.y, end.y); - let totalHeight = Math.abs(start.y - end.y); - let i; - - let maxY = 0; - let minY = Number.MIN_VALUE; - const boxes = labels.map((label) => { - if (label.y > maxY) { - maxY = label.y; - } - if (label.y < minY) { - minY = label.y; - } - return { - size: lineHeight, - targets: [label.y - startY], - pos: null, - }; - }); - minY -= startY; - if (maxY - startY > totalHeight) { - totalHeight = maxY - startY; - } - - while (overlapping) { - /* eslint no-loop-func: 0 */ - boxes.forEach((box) => { - const target = (Math.min.apply(minY, box.targets) + Math.max.apply(minY, box.targets)) / 2; - box.pos = Math.min(Math.max(minY, target - box.size / 2), totalHeight - box.size); - // box.pos = Math.max(0, target - box.size / 2); - }); - - // detect overlapping and join boxes - overlapping = false; - i = boxes.length; - while (i--) { - if (i > 0) { - const previousBox = boxes[i - 1]; - const box = boxes[i]; - if (previousBox.pos + previousBox.size > box.pos) { - // overlapping - previousBox.size += box.size; - previousBox.targets = previousBox.targets.concat(box.targets); - - // overflow, shift up - if (previousBox.pos + previousBox.size > totalHeight) { - previousBox.pos = totalHeight - previousBox.size; - } - boxes.splice(i, 1); // removing box - overlapping = true; - } - } - } - } - - i = 0; - // step 4: normalize y and adjust x - boxes.forEach((b) => { - let posInCompositeBox = startY + lineHeight / 2; // middle of the label - b.targets.forEach(() => { - labels[i].y = b.pos + posInCompositeBox; - posInCompositeBox += lineHeight; - i++; - }); - }); - - // (x - cx)^2 + (y - cy)^2 = totalR^2 - labels.forEach((label) => { - const rPow2 = label.r * label.r; - const dyPow2 = Math.pow(Math.abs(label.y - center.y), 2); - if (rPow2 < dyPow2) { - label.x = center.x; - } else { - const dx = Math.sqrt(rPow2 - dyPow2); - if (!isRight) { - // left - label.x = center.x - dx; - } else { - // right - label.x = center.x + dx; - } - } - }); - - labels.forEach((label) => { - const labelGroup = find(labelGroups, (group) => group.get('id') === label.id); - const coordinate = labelGroup.get('coordinate'); - const coordRadius = coordinate.getRadius(); - if (labelGroup) { - const labelShape = labelGroup.getChildren()[0]; - const labelLineShape = labelGroup.getChildren()[1]; - if (labelShape) { - labelShape.attr('x', label.x); - labelShape.attr('y', label.y); - } - if (labelLineShape) { - const distance = label.offset; - const angle = label.angle; - // 贴近圆周 - const startPoint = getEndPoint(center, angle, coordRadius); - const inner = getEndPoint(center, angle, coordRadius + distance / 2); - const endPoint = { - x: label.x - Math.cos(angle) * MARGIN, - y: label.y - Math.sin(angle) * MARGIN, - }; - labelLineShape.attr('path', [`M ${startPoint.x}`, `${startPoint.y} Q${inner.x}`, `${inner.y} ${endPoint.x}`, endPoint.y].join(',')); - } - } - }); -} - -/** - * pie outer-label: distribute algorithm - */ -export function distribute(labels: IGroup[], shapes: IShape[] | IGroup[], region: BBox) { - const offset = labels[0] ? labels[0].get('labelItem').offset : 0; - const lineHeight = labels[0] ? labels[0].get('labelItem').labelHeight : 14; - const coordinate = labels[0] ? labels[0].get('coordinate') : null; - if (coordinate && offset > 0) { - // @ts-ignore - const radius = coordinate.getRadius(); - const center = coordinate.getCenter(); - const totalR = radius + offset; - const totalHeight = totalR * 2 + lineHeight * 2; - const plotRange = { - start: coordinate.start, - end: coordinate.end, - }; - - // step 1: separate labels - const halves = [ - [], // left - [], // right - ]; - labels.forEach((label) => { - if (!label) { - return; - } - if (label.getChildren()[0].attr('textAlign') === 'right') { - // left - halves[0].push(label); - } else { - // right or center will be put on the right side - halves[1].push(label); - } - }); - - halves.forEach((half, index) => { - // step 2: reduce labels - const maxLabelsCountForOneSide = Math.floor(totalHeight / lineHeight); - if (half.length > maxLabelsCountForOneSide) { - half.sort((a, b) => { - // sort by percentage DESC - return b['..percent'] - a['..percent']; - }); - half.forEach((label, idx) => { - if (idx >= maxLabelsCountForOneSide) { - label.remove(true); // 超出则不展示 - } - }); - // 同时移除 - half.splice(maxLabelsCountForOneSide, half.length - maxLabelsCountForOneSide); - } - - // step 3: distribute position (x and y) - half.sort((a, b) => { - // sort by y ASC - return a.get('labelItem').y - b.get('labelItem').y; - }); - antiCollision(half, lineHeight, plotRange, center, index); - }); - } -} diff --git a/src/geometry/label/pie.ts b/src/geometry/label/pie.ts index 31abdcd7f0..8772f18e0f 100644 --- a/src/geometry/label/pie.ts +++ b/src/geometry/label/pie.ts @@ -14,6 +14,95 @@ function getEndPoint(center, angle, r) { }; } +function antiCollision(labels, lineHeight, plotRange, center, isRight) { + // adjust y position of labels to avoid overlapping + let overlapping = true; + const start = plotRange.start; + const end = plotRange.end; + const startY = Math.min(start.y, end.y); + let totalHeight = Math.abs(start.y - end.y); + let i; + + let maxY = 0; + let minY = Number.MIN_VALUE; + const boxes = labels.map((label) => { + if (label.y > maxY) { + maxY = label.y; + } + if (label.y < minY) { + minY = label.y; + } + return { + size: lineHeight, + targets: [label.y - startY], + }; + }); + minY -= startY; + if (maxY - startY > totalHeight) { + totalHeight = maxY - startY; + } + + while (overlapping) { + /* eslint no-loop-func: 0 */ + boxes.forEach((box) => { + const target = (Math.min.apply(minY, box.targets) + Math.max.apply(minY, box.targets)) / 2; + box.pos = Math.min(Math.max(minY, target - box.size / 2), totalHeight - box.size); + // box.pos = Math.max(0, target - box.size / 2); + }); + + // detect overlapping and join boxes + overlapping = false; + i = boxes.length; + while (i--) { + if (i > 0) { + const previousBox = boxes[i - 1]; + const box = boxes[i]; + if (previousBox.pos + previousBox.size > box.pos) { + // overlapping + previousBox.size += box.size; + previousBox.targets = previousBox.targets.concat(box.targets); + + // overflow, shift up + if (previousBox.pos + previousBox.size > totalHeight) { + previousBox.pos = totalHeight - previousBox.size; + } + boxes.splice(i, 1); // removing box + overlapping = true; + } + } + } + } + + i = 0; + // step 4: normalize y and adjust x + boxes.forEach((b) => { + let posInCompositeBox = startY + lineHeight / 2; // middle of the label + b.targets.forEach(() => { + labels[i].y = b.pos + posInCompositeBox; + posInCompositeBox += lineHeight; + i++; + }); + }); + + // (x - cx)^2 + (y - cy)^2 = totalR^2 + labels.forEach((label) => { + const rPow2 = label.r * label.r; + const dyPow2 = Math.pow(Math.abs(label.y - center.y), 2); + if (rPow2 < dyPow2) { + label.x = center.x; + } else { + const dx = Math.sqrt(rPow2 - dyPow2); + if (!isRight) { + // left + label.x = center.x - dx; + } else { + // right + label.x = center.x + dx; + } + } + }); +} + /** * 饼图 label */ @@ -26,6 +115,14 @@ export default class PieLabel extends PolarLabel { return offset || 0; } + protected adjustItems(items: LabelItem[]) { + const offset = items[0] ? items[0].offset : 0; + if (offset > 0) { + items = this.distribute(items, offset); + } + return super.adjustItems(items); + } + // 连接线 protected lineToLabel(label: LabelItem) { const coordinate = this.coordinate; @@ -123,4 +220,58 @@ export default class PieLabel extends PolarLabel { r, }; } + + // distribute labels + private distribute(labels, offset) { + const coordinate = this.coordinate; + // @ts-ignore + const radius = coordinate.getRadius(); + const lineHeight = get(this.geometry.theme, ['pieLabels', 'labelHeight'], 14); + const center = coordinate.getCenter(); + const totalR = radius + offset; + const totalHeight = totalR * 2 + lineHeight * 2; + const plotRange = { + start: coordinate.start, + end: coordinate.end, + }; + + // step 1: separate labels + const halves = [ + [], // left + [], // right + ]; + labels.forEach((label) => { + if (!label) { + return; + } + if (label.textAlign === 'right') { + // left + halves[0].push(label); + } else { + // right or center will be put on the right side + halves[1].push(label); + } + }); + + halves.forEach((half, index) => { + // step 2: reduce labels + const maxLabelsCountForOneSide = totalHeight / lineHeight; + if (half.length > maxLabelsCountForOneSide) { + half.sort((a, b) => { + // sort by percentage DESC + return b['..percent'] - a['..percent']; + }); + half.splice(maxLabelsCountForOneSide, half.length - maxLabelsCountForOneSide); + } + + // step 3: distribute position (x and y) + half.sort((a, b) => { + // sort by y ASC + return a.y - b.y; + }); + antiCollision(half, lineHeight, plotRange, center, index); + }); + + return halves[0].concat(halves[1]); + } } diff --git a/src/index.ts b/src/index.ts index 766be4765c..60ea2cdff9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -76,7 +76,6 @@ registerGeometryLabel('polar', PolarLabel); // 注册 Geometry label 内置的布局函数 import { registerGeometryLabelLayout } from './core'; -import { distribute } from './geometry/label/layout/distribute'; import { limitInCanvas } from './geometry/label/layout/limit-in-canvas'; import { limitInShape } from './geometry/label/layout/limit-in-shape'; import { fixedOverlap, overlap } from './geometry/label/layout/overlap'; @@ -85,7 +84,6 @@ registerGeometryLabelLayout('overlap', overlap); registerGeometryLabelLayout('fixed-overlap', fixedOverlap); registerGeometryLabelLayout('limit-in-shape', limitInShape); registerGeometryLabelLayout('limit-in-canvas', limitInCanvas); -registerGeometryLabelLayout('distribute', distribute); // 注册需要的动画执行函数 import { fadeIn, fadeOut } from './animate/animation/fade';