From fb6db343e03bb1a8caad12b5e41ab5bf05ad8d84 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Tue, 25 Jul 2023 20:15:16 +0800 Subject: [PATCH 01/29] feat: inside & outside pie chart label --- .../__tests__/browser/examples/label-arc.ts | 385 ++++++++ .../__tests__/browser/main.ts | 4 + packages/vrender-components/src/label/arc.ts | 855 ++++++++++++++++++ packages/vrender-components/src/label/base.ts | 93 +- .../vrender-components/src/label/constant.ts | 19 + .../vrender-components/src/label/index.ts | 1 + packages/vrender-components/src/label/type.ts | 44 + packages/vrender-components/src/label/util.ts | 134 +++ 8 files changed, 1523 insertions(+), 12 deletions(-) create mode 100644 packages/vrender-components/__tests__/browser/examples/label-arc.ts create mode 100644 packages/vrender-components/src/label/arc.ts create mode 100644 packages/vrender-components/src/label/constant.ts create mode 100644 packages/vrender-components/src/label/util.ts diff --git a/packages/vrender-components/__tests__/browser/examples/label-arc.ts b/packages/vrender-components/__tests__/browser/examples/label-arc.ts new file mode 100644 index 000000000..138612cac --- /dev/null +++ b/packages/vrender-components/__tests__/browser/examples/label-arc.ts @@ -0,0 +1,385 @@ +import { GUI } from 'lil-gui'; +import { createGroup, Stage, createArc } from '@visactor/vrender'; +import { createRenderer } from '../../util/render'; +import { ArcLabel } from '../../../src'; + +const pieGenerator = () => { + const spec: any = { + attribute: { + width: 800, + height: 500, + pickable: false, + zIndex: 300 + }, + _uid: 14, + type: 'group', + name: 'pie_9', + children: [ + { + attribute: { + visible: true, + lineWidth: 0, + fillOpacity: 1, + padAngle: 0, + x: 388, + y: 238, + startAngle: -1.5707963267948966, + endAngle: 1.357168026350791, + outerRadius: 190.4, + innerRadius: 0, + cornerRadius: 0, + fill: '#1664FF', + stroke: '#1664FF', + pickable: true + }, + _uid: 15, + type: 'arc', + children: [] + }, + { + attribute: { + visible: true, + lineWidth: 0, + fillOpacity: 1, + padAngle: 0, + x: 388, + y: 238, + startAngle: 1.357168026350791, + endAngle: 3.0988669935009723, + outerRadius: 190.4, + innerRadius: 0, + cornerRadius: 0, + fill: '#1AC6FF', + stroke: '#1AC6FF', + pickable: true + }, + _uid: 16, + type: 'arc', + children: [] + }, + { + attribute: { + visible: true, + lineWidth: 0, + fillOpacity: 1, + padAngle: 0, + x: 388, + y: 238, + startAngle: 3.0988669935009723, + endAngle: 3.609689958974673, + outerRadius: 190.4, + innerRadius: 0, + cornerRadius: 0, + fill: '#FF8A00', + stroke: '#FF8A00', + pickable: true + }, + _uid: 17, + type: 'arc', + children: [] + }, + { + attribute: { + visible: true, + lineWidth: 0, + fillOpacity: 1, + padAngle: 0, + x: 388, + y: 238, + startAngle: 3.609689958974673, + endAngle: 3.9238492243336522, + outerRadius: 190.4, + innerRadius: 0, + cornerRadius: 0, + fill: '#3CC780', + stroke: '#3CC780', + pickable: true + }, + _uid: 18, + type: 'arc', + children: [] + }, + { + attribute: { + visible: true, + lineWidth: 0, + fillOpacity: 1, + padAngle: 0, + x: 388, + y: 238, + startAngle: 3.9238492243336522, + endAngle: 4.151928850984271, + outerRadius: 190.4, + innerRadius: 0, + cornerRadius: 0, + fill: '#7442D4', + stroke: '#7442D4', + pickable: true + }, + _uid: 19, + type: 'arc', + children: [] + }, + { + attribute: { + visible: true, + lineWidth: 0, + fillOpacity: 1, + padAngle: 0, + x: 388, + y: 238, + startAngle: 4.151928850984271, + endAngle: 4.329742995177454, + outerRadius: 190.4, + innerRadius: 0, + cornerRadius: 0, + fill: '#FFC400', + stroke: '#FFC400', + pickable: true + }, + _uid: 20, + type: 'arc', + children: [] + }, + { + attribute: { + visible: true, + lineWidth: 0, + fillOpacity: 1, + padAngle: 0, + x: 388, + y: 238, + startAngle: 4.329742995177454, + endAngle: 4.492477494633405, + outerRadius: 190.4, + innerRadius: 0, + cornerRadius: 0, + fill: '#304D77', + stroke: '#304D77', + pickable: true + }, + _uid: 21, + type: 'arc', + children: [] + }, + { + attribute: { + visible: true, + lineWidth: 0, + fillOpacity: 1, + padAngle: 0, + x: 388, + y: 238, + startAngle: 4.492477494633405, + endAngle: 4.71238898038469, + outerRadius: 190.4, + innerRadius: 0, + cornerRadius: 0, + fill: '#B48DEB', + stroke: '#B48DEB', + pickable: true + }, + _uid: 22, + type: 'arc', + children: [] + } + ] + }; + return spec; +}; + +const latestData = [ + { + type: 'oxygen', + value: '46.60', + __VCHART_DEFAULT_DATA_INDEX: 0, + __VCHART_DEFAULT_DATA_KEY: 'oxygen_oxygen_0', + __VCHART_ARC_RATIO: 0.4660000000000001, + __VCHART_ARC_START_ANGLE: -1.5707963267948966, + __VCHART_ARC_END_ANGLE: 1.357168026350791, + __VCHART_ARC_MIDDLE_ANGLE: -0.10681415022205276, + __VCHART_ARC_RADIAN: 2.9279643531456876, + __VCHART_ARC_QUADRANT: 1, + __VCHART_ARC_K: 1, + VGRAMMAR_DATA_ID_KEY_16: 0 + }, + { + type: 'silicon', + value: '27.72', + __VCHART_DEFAULT_DATA_INDEX: 1, + __VCHART_DEFAULT_DATA_KEY: 'silicon_silicon_0', + __VCHART_ARC_RATIO: 0.2772, + __VCHART_ARC_START_ANGLE: 1.357168026350791, + __VCHART_ARC_END_ANGLE: 3.0988669935009723, + __VCHART_ARC_MIDDLE_ANGLE: 2.2280175099258814, + __VCHART_ARC_RADIAN: 1.7416989671501812, + __VCHART_ARC_QUADRANT: 3, + __VCHART_ARC_K: 0.5948497854077253, + VGRAMMAR_DATA_ID_KEY_16: 1 + }, + { + type: 'aluminum', + value: '8.13', + __VCHART_DEFAULT_DATA_INDEX: 2, + __VCHART_DEFAULT_DATA_KEY: 'aluminum_aluminum_0', + __VCHART_ARC_RATIO: 0.08130000000000003, + __VCHART_ARC_START_ANGLE: 3.0988669935009723, + __VCHART_ARC_END_ANGLE: 3.609689958974673, + __VCHART_ARC_MIDDLE_ANGLE: 3.3542784762378224, + __VCHART_ARC_RADIAN: 0.5108229654737005, + __VCHART_ARC_QUADRANT: 4, + __VCHART_ARC_K: 0.17446351931330473, + VGRAMMAR_DATA_ID_KEY_16: 2 + }, + { + type: 'iron', + value: '5', + __VCHART_DEFAULT_DATA_INDEX: 3, + __VCHART_DEFAULT_DATA_KEY: 'iron_iron_0', + __VCHART_ARC_RATIO: 0.05000000000000001, + __VCHART_ARC_START_ANGLE: 3.609689958974673, + __VCHART_ARC_END_ANGLE: 3.9238492243336522, + __VCHART_ARC_MIDDLE_ANGLE: 3.7667695916541626, + __VCHART_ARC_RADIAN: 0.31415926535897937, + __VCHART_ARC_QUADRANT: 4, + __VCHART_ARC_K: 0.1072961373390558, + VGRAMMAR_DATA_ID_KEY_16: 3 + }, + { + type: 'calcium', + value: '3.63', + __VCHART_DEFAULT_DATA_INDEX: 4, + __VCHART_DEFAULT_DATA_KEY: 'calcium_calcium_0', + __VCHART_ARC_RATIO: 0.036300000000000006, + __VCHART_ARC_START_ANGLE: 3.9238492243336522, + __VCHART_ARC_END_ANGLE: 4.151928850984271, + __VCHART_ARC_MIDDLE_ANGLE: 4.037889037658962, + __VCHART_ARC_RADIAN: 0.228079626650619, + __VCHART_ARC_QUADRANT: 4, + __VCHART_ARC_K: 0.0778969957081545, + VGRAMMAR_DATA_ID_KEY_16: 4 + }, + { + type: 'sodium', + value: '2.83', + __VCHART_DEFAULT_DATA_INDEX: 5, + __VCHART_DEFAULT_DATA_KEY: 'sodium_sodium_0', + __VCHART_ARC_RATIO: 0.028300000000000006, + __VCHART_ARC_START_ANGLE: 4.151928850984271, + __VCHART_ARC_END_ANGLE: 4.329742995177454, + __VCHART_ARC_MIDDLE_ANGLE: 4.240835923080862, + __VCHART_ARC_RADIAN: 0.17781414419318234, + __VCHART_ARC_QUADRANT: 4, + __VCHART_ARC_K: 0.06072961373390558, + VGRAMMAR_DATA_ID_KEY_16: 5 + }, + { + type: 'potassium', + value: '2.59', + __VCHART_DEFAULT_DATA_INDEX: 6, + __VCHART_DEFAULT_DATA_KEY: 'potassium_potassium_0', + __VCHART_ARC_RATIO: 0.025900000000000003, + __VCHART_ARC_START_ANGLE: 4.329742995177454, + __VCHART_ARC_END_ANGLE: 4.492477494633405, + __VCHART_ARC_MIDDLE_ANGLE: 4.411110244905429, + __VCHART_ARC_RADIAN: 0.1627344994559513, + __VCHART_ARC_QUADRANT: 4, + __VCHART_ARC_K: 0.055579399141630896, + VGRAMMAR_DATA_ID_KEY_16: 6 + }, + { + type: 'others', + value: '3.5', + __VCHART_DEFAULT_DATA_INDEX: 7, + __VCHART_DEFAULT_DATA_KEY: 'others_others_0', + __VCHART_ARC_RATIO: 0.035, + __VCHART_ARC_START_ANGLE: 4.492477494633405, + __VCHART_ARC_END_ANGLE: 4.71238898038469, + __VCHART_ARC_MIDDLE_ANGLE: 4.602433237509048, + __VCHART_ARC_RADIAN: 0.21991148575128555, + __VCHART_ARC_QUADRANT: 4, + __VCHART_ARC_K: 0.07510729613733905, + VGRAMMAR_DATA_ID_KEY_16: 7 + } +]; + +function createContent(stage: Stage) { + const pieSpec = pieGenerator(); + const pieGroup = createGroup(pieSpec.attribute); + pieGroup.name = pieSpec.name; + pieGroup.id = pieSpec._uid; + stage.defaultLayer.add(pieGroup); + pieSpec.children.forEach(c => { + pieGroup.add(createArc(c.attribute)); + }); + + const pieLabel = new ArcLabel({ + baseMarkGroupName: pieSpec.name, + data: pieSpec.children.map((c, index) => { + return { + text: latestData[index].type, + fill: c.attribute.fill, + line: { + stroke: c.attribute.stroke + }, + lineWidth: 0, + ...latestData[index] + }; + }), + type: 'arc', + width: 800, + height: 500, + center: { x: 388, y: 238 }, + position: 'outside', + // position: 'inside', + + // coverEnable: false, + // layout: { + // strategy: 'none' + // }, + + zIndex: 302 + }); + + stage.defaultLayer.add(pieLabel); + + return { pie: pieGroup, label: pieLabel }; +} + +const stage = createRenderer('main', { + width: 800, + height: 500, + viewBox: { + x1: 0, + y1: 0, + x2: 800, + y2: 500 + } +}); +const { pie, label } = createContent(stage); +stage.render(); +// gui +const gui = new GUI(); +const guiObject = { + name: 'Label', + position: 'outside', + baseMarkVisible: true, + shapeCount: 100, + overlap: true, + debug() { + label.render(); + } +}; + +gui.add(guiObject, 'name'); +gui.add(guiObject, 'position', ['outside', 'inside']); +gui.add(guiObject, 'baseMarkVisible').onChange(value => { + pie.forEachChildren(s => s.setAttribute('visible', !!value)); +}); +gui.add(guiObject, 'overlap').onChange(value => { + label.setAttribute('overlap', { + enable: value + }); +}); + +gui.add(guiObject, 'debug'); diff --git a/packages/vrender-components/__tests__/browser/main.ts b/packages/vrender-components/__tests__/browser/main.ts index 32c03bc37..b5993c883 100644 --- a/packages/vrender-components/__tests__/browser/main.ts +++ b/packages/vrender-components/__tests__/browser/main.ts @@ -42,6 +42,10 @@ const specs = [ path: 'label-line', name: 'line 数据标签' }, + { + path: 'label-arc', + name: 'arc 数据标签' + }, { path: 'label-multi-mark', name: '混合图元 数据标签' diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts new file mode 100644 index 000000000..e4d8da1b5 --- /dev/null +++ b/packages/vrender-components/src/label/arc.ts @@ -0,0 +1,855 @@ +import type { IBoundsLike } from '@visactor/vutils'; +import { merge } from '@visactor/vutils'; +import { LabelBase } from './base'; +import type { ArcLabelAttrs, IPoint, Quadrant, TextAlign } from './type'; +import { IPolarPoint } from './type'; +import type { ITextGraphicAttribute } from '@visactor/vrender'; +import { isValidNumber, isNil, isLess, isGreater, isNumberClose as isClose } from '@visactor/vutils'; +import { + circlePoint, + isQuadrantRight, + isQuadrantLeft, + lineCirclePoints, + connectLineRadian, + checkBoundsOverlap, + degrees +} from './util'; +import { ARC_K, ARC_MIDDLE_ANGLE, ARC_QUADRANT, ARC_RADIAN } from './constant'; +import type { IGraphic } from '@visactor/vrender'; + +export class ArcInfo { + key!: string; + refDatum!: any; + /** + * 绘图区圆弧中点 + */ + center!: IPoint; + /** + * label起始区圆弧中点 + */ + outerCenter!: IPoint; + labelSize!: { width: number; height: number }; + labelPosition!: IPoint; + labelLimit: number; + labelVisible: boolean; + lastLabelY!: number; + labelYRange!: [number, number]; + labelText!: string | string[]; + pointA!: IPoint; + pointB!: IPoint; + pointC!: IPoint; + /** + * 象限 + */ + quadrant: Quadrant; + radian: number; + middleAngle: number; + k: number; + textAlign: string; + textBaseline: string; + angle: number; + + constructor( + refDatum: any, + center: IPoint, + outerCenter: IPoint, + quadrant: Quadrant, + radian: number, + middleAngle: number, + k: number + ) { + this.refDatum = refDatum; + this.center = center; + this.outerCenter = outerCenter; + this.quadrant = quadrant; + this.radian = radian; + this.middleAngle = middleAngle; + this.k = k; + this.labelVisible = true; + this.labelLimit = 0; + } + + getLabelBounds(): IBoundsLike { + if (!this.labelPosition || !this.labelSize) { + return { x1: 0, x2: 0, y1: 0, y2: 0 }; + } + return { + x1: this.labelPosition.x - this.labelSize.width / 2, + y1: this.labelPosition.y - this.labelSize.height / 2, + x2: this.labelPosition.x + this.labelSize.width / 2, + y2: this.labelPosition.y + this.labelSize.height / 2 + }; + } +} + +type PriorityArc = { + arc: ArcInfo; + /** + * 在初始 arc 数组中的索引 + */ + originIndex: number; + priorityIndex: number; +}; + +export class ArcLabel extends LabelBase { + name = 'arc-label'; + + static defaultAttributes: Partial = { + // visible: true, + // showRule: 'all', + // rotate: true, + coverEnable: false, + spaceWidth: 5, + layoutArcGap: 6, + textStyle: { + visible: true, + fontSize: 14, + fontWeight: 'normal', + fillOpacity: 1 + }, + position: 'outside', + offset: 0, + line: { + visible: true, + line1MinLength: 20, + line2MinLength: 10 + }, + layout: { + align: 'arc', + strategy: 'priority', + tangentConstraint: true + } + }; + + private _ellipsisWidth: number = 0; + + private _arcLeft: Map = new Map(); + private _arcRight: Map = new Map(); + + constructor(attributes: ArcLabelAttrs) { + super(merge({}, ArcLabel.defaultAttributes, attributes)); + } + + protected labeling( + textBounds: IBoundsLike, + graphicBounds: IBoundsLike, + position = 'outside', + offset = 0, + graphicAttributes: any, + textData: any, + width: number, + height: number, + attribute: any + ): Partial | undefined { + if (!textBounds || !graphicBounds) { + return; + } + + // setArcs : 根据 arc 设置 datum 中对应的标签数据 + const radiusRatio = this.computeLayoutOuterRadius(graphicAttributes.outerRadius, width, height); + const radius = this.computeRadius(radiusRatio, width, height); + const center = attribute.center ?? { x: 0, y: 0 }; + + const item = textData; + + const arcMiddle = circlePoint(center.x, center.y, radius * item[ARC_K], item[ARC_MIDDLE_ANGLE]); + + const outerArcMiddle = circlePoint( + center.x, + center.y, + radius + attribute.line.line1MinLength, + item[ARC_MIDDLE_ANGLE] + ); + + const arc = new ArcInfo( + item, + arcMiddle, + outerArcMiddle, + item[ARC_QUADRANT], + item[ARC_RADIAN], + item[ARC_MIDDLE_ANGLE], + item[ARC_K] + ); + + // refDatum: any, + // center: IPoint, + // outerCenter: IPoint, + // quadrant: Quadrant, + // radian: number, + // middleAngle: number, + // k: number + + arc.pointA = circlePoint( + (center as IPoint).x, + (center as IPoint).y, + this.computeDatumRadius(center.x * 2, center.y * 2, graphicAttributes.outerRadius), + arc.middleAngle + ); + + arc.labelSize = { + width: textBounds.x2 - textBounds.x1, + height: textBounds.y2 - textBounds.y1 + }; + if (isQuadrantRight(arc.quadrant)) { + arc.textAlign = 'left'; + arc.textBaseline = 'bottom'; + this._arcRight.set(arc.refDatum, arc); + } else if (isQuadrantLeft(arc.quadrant)) { + arc.textAlign = 'right'; + arc.textBaseline = 'bottom'; + this._arcLeft.set(arc.refDatum, arc); + } + + return { arcRight: this._arcRight, arcLeft: this._arcLeft }; + } + + // layoutLabels : 执行内部/外部标签的布局计算 + protected layoutArcLabels(position = 'outside', attribute: any, currentMarks?: IGraphic[]) { + const leftArcs = Array.from(this._arcLeft.values()); + const rightArcs = Array.from(this._arcRight.values()); + const arcs: ArcInfo[] = []; + if (position === 'inside') { + arcs.push(...this._layoutInsideLabels(rightArcs, attribute, currentMarks)); + arcs.push(...this._layoutInsideLabels(leftArcs, attribute, currentMarks)); + } else { + arcs.push(...this._layoutOutsideLabels(rightArcs, attribute, currentMarks)); + arcs.push(...this._layoutOutsideLabels(leftArcs, attribute, currentMarks)); + } + return arcs; + } + + /** + * 布局内部标签 + */ + private _layoutInsideLabels(arcs: ArcInfo[], attribute: any, currentMarks: IGraphic[]) { + const center = attribute.center ?? { x: 0, y: 0 }; + const innerRadiusRatio = this.computeLayoutOuterRadius( + currentMarks[0].attribute.innerRadius, + attribute.width, + attribute.height + ); + const outerRadiusRatio = this.computeLayoutOuterRadius( + currentMarks[0].attribute.outerRadius, + attribute.width, + attribute.height + ); + const labelConfig = attribute; + const spaceWidth = labelConfig.spaceWidth as number; + + arcs.forEach((arc: ArcInfo) => { + const { labelSize, radian } = arc; + const innerRadius = this.computeRadius(innerRadiusRatio, attribute.width, attribute.height, 1); + const outerRadius = this.computeRadius(outerRadiusRatio, attribute.width, attribute.height, 1); + const minRadian = connectLineRadian(outerRadius, labelSize.height); + let limit; + if (radian < minRadian) { + limit = 0; + } else { + let minRadius; + if (radian >= Math.PI) { + minRadius = innerRadius; + } else { + minRadius = Math.max(innerRadius, labelSize.height / 2 / Math.tan(radian / 2)); + } + limit = outerRadius - minRadius - spaceWidth; + } + // TODO: 对于不旋转的内部标签设置 limit 为 outerRadius + if (labelConfig?.rotate !== true) { + limit = outerRadius - spaceWidth; + } + const text = this._getFormatLabelText(arc.refDatum, limit); + arc.labelText = text; + const labelWidth = Math.min(limit, arc.labelSize.width); + const align = this._computeAlign(arc, attribute); + const alignOffset = align === 'left' ? labelWidth : align === 'right' ? 0 : labelWidth / 2; + const labelRadius = outerRadius - spaceWidth - alignOffset; + arc.labelPosition = circlePoint((center as IPoint).x, (center as IPoint).y, labelRadius, arc.middleAngle); + arc.labelLimit = labelWidth; + if (!isGreater(labelWidth, 0)) { + arc.labelVisible = false; + } + (arc.textAlign = 'center'), (arc.textBaseline = 'middle'); + + // arc.angle = degrees(arc.middleAngle); + arc.angle = arc.middleAngle; + }); + return arcs; + } + + /** + * 布局外部标签 + */ + private _layoutOutsideLabels(arcs: ArcInfo[], attribute: any, currentMarks: IGraphic[]) { + // const height = Math.min(attribute.center.x, attribute.center.y) * 2; + const height = attribute.center.y * 2; + const line2MinLength = attribute.line.line2MinLength as number; + const labelLayout = attribute.layout; + const spaceWidth = attribute.spaceWidth as number; + + arcs.forEach(arc => { + const direction = isQuadrantLeft(arc.quadrant) ? -1 : 1; + arc.labelPosition = { + x: arc.outerCenter.x + direction * (arc.labelSize.width / 2 + line2MinLength + spaceWidth), + y: arc.outerCenter.y + }; + }); + arcs.sort((a, b) => { + return a.labelPosition.y - b.labelPosition.y; + }); + + if (attribute.coverEnable !== false || labelLayout.strategy === 'none') { + for (const arc of arcs) { + const { labelPosition, labelSize } = arc; + arc.labelLimit = labelSize.width; + arc.pointB = isQuadrantLeft(arc.quadrant) + ? { + x: labelPosition.x + labelSize.width / 2 + line2MinLength + spaceWidth, + y: labelPosition.y + } + : { + x: labelPosition.x - labelSize.width / 2 - line2MinLength - spaceWidth, + y: labelPosition.y + }; + this._computeX(arc, attribute, currentMarks); + } + if (attribute.coverEnable === false && labelLayout.strategy === 'none') { + this._coverLabels(arcs); + } + } else { + // 由于可能存在多行标签,这里仅仅估计一个最大标签数量用于避免冗余计算 + const maxLabels = height / ((attribute.textStyle?.fontSize as number) || 16); + // 布局圆弧半径 + this._adjustY(arcs, maxLabels, attribute, currentMarks); + + const { minY, maxY } = arcs.reduce( + (yInfo, arc) => { + const { y1, y2 } = arc.getLabelBounds(); + yInfo.minY = Math.max(0, Math.min(y1, yInfo.minY)); + yInfo.maxY = Math.min(height, Math.max(y2, yInfo.maxY)); + return yInfo; + }, + { minY: Infinity, maxY: -Infinity } + ); + const halfY = Math.max(Math.abs(height / 2 - minY), Math.abs(maxY - height / 2)); + // pointB 与 label 的 y 值相同,但是 label 的 x 值依赖于 pointB 的 x 值 + const r = this._computeLayoutRadius(halfY, attribute, currentMarks); + for (const arc of arcs) { + this._computePointB(arc, r, attribute, currentMarks); + this._computeX(arc, attribute, currentMarks); + } + } + const width = attribute.center.x * 2; + arcs.forEach(arc => { + if ( + arc.labelVisible && + (isLess(arc.pointB.x, line2MinLength + spaceWidth) || + isGreater(arc.pointB.x, width - line2MinLength - spaceWidth)) + ) { + arc.labelVisible = false; + } + arc.angle = 0; + }); + return arcs; + } + + /** + * 计算 pointC 以及 label limit 与 position + */ + private _computeX(arc: ArcInfo, attribute: any, currentMarks: IGraphic[]) { + const center = attribute.center ?? { x: 0, y: 0 }; + const plotLayout = { width: attribute.center.x * 2, height: attribute.center.y * 2 }; + const radiusRatio = this.computeLayoutOuterRadius( + currentMarks[0].attribute.outerRadius, + attribute.width, + attribute.height + ); + + const line1MinLength = attribute.line.line1MinLength as number; + const line2MinLength = attribute.line.line2MinLength as number; + const labelLayoutAlign = attribute.layout?.align; + const spaceWidth = attribute.spaceWidth as number; + const align = this._computeAlign(arc, attribute) as TextAlign; + + const { labelPosition, quadrant, pointB } = arc; + if (!isValidNumber(pointB.x * pointB.y)) { + arc.pointC = { x: NaN, y: NaN }; + labelPosition.x = NaN; + arc.labelLimit = 0; + } + const radius = this.computeRadius(radiusRatio, attribute.width, attribute.height); + const flag = isQuadrantLeft(quadrant) ? -1 : 1; + let cx: number = 0; + const restWidth = flag > 0 ? plotLayout.width - pointB.x : pointB.x; + let limit = restWidth - line2MinLength - spaceWidth; + if (labelLayoutAlign === 'labelLine') { + cx = (radius + line1MinLength + line2MinLength) * flag + (center as IPoint).x; + limit = (flag > 0 ? plotLayout.width - cx : cx) - spaceWidth; + } + const text = this._getFormatLabelText(arc.refDatum, limit); + arc.labelText = text; + let labelWidth = Math.min(limit, arc.labelSize.width); + switch (labelLayoutAlign) { + case 'labelLine': + break; + case 'edge': + cx = flag > 0 ? plotLayout.width - labelWidth - spaceWidth : labelWidth + spaceWidth; + break; + case 'arc': + default: + cx = pointB.x + flag * line2MinLength; + break; + } + labelWidth = Math.max(this._ellipsisWidth, labelWidth); + arc.pointC = { x: cx, y: labelPosition.y }; + + if (labelLayoutAlign === 'edge') { + // edge 模式下的多行文本对齐方向与其他模式相反 + const alignOffset = this._computeAlignOffset(align, labelWidth, -flag); + // 贴近画布边缘的布局结果可能会由于 cx 的小数 pixel 导致被部分裁剪,因此额外做计算 + labelPosition.x = flag > 0 ? plotLayout.width + alignOffset : alignOffset; + } else { + const alignOffset = this._computeAlignOffset(align, labelWidth, flag); + labelPosition.x = cx + alignOffset + flag * spaceWidth; + } + + arc.labelLimit = labelWidth; + } + + private _computeAlignOffset(align: TextAlign, labelWidth: number, alignFlag: number): number { + switch (align) { + case 'left': + return alignFlag < 0 ? -labelWidth : 0; + case 'right': + return alignFlag < 0 ? 0 : labelWidth; + case 'center': + default: + return (labelWidth / 2) * alignFlag; + } + } + + private _computeAlign(arc: ArcInfo, attribute: any) { + const labelConfig = attribute; + // 暂时兼容两种配置方式 + const textAlign = labelConfig.textStyle?.textAlign ?? labelConfig.textStyle?.align; + const layoutAlign = labelConfig.layout?.textAlign ?? labelConfig.layout?.align; + if (labelConfig.position !== 'inside') { + if (isNil(textAlign) || textAlign === 'auto') { + // edge 模式下沿着画布对齐,与 labelLine & edge 模式相反 + if (layoutAlign === 'edge') { + return isQuadrantLeft(arc.quadrant) ? 'left' : 'right'; + } + return isQuadrantLeft(arc.quadrant) ? 'right' : 'left'; + } + return textAlign; + } + return isNil(textAlign) || textAlign === 'auto' ? 'center' : textAlign; + } + + private _getFormatLabelText(value: any, limit?: number) { + return value.text; + } + + /** + * 调整标签位置的 Y 值 + */ + private _adjustY(arcs: ArcInfo[], maxLabels: number, attribute: any, currentMarks: any) { + const plotRect = { width: attribute.center.x * 2, height: attribute.center.y * 2 }; + const labelLayout = attribute.layout; + if (labelLayout.strategy === 'vertical') { + // vertical 策略类似 echarts 方案,没有切线限制策略,没有优先级,执行整体调整没有标签数量限制 + let lastY = 0; + let delta; + const len = arcs.length; + if (len <= 0) { + return; + } + // 偏移 y 值以避免遮挡 + for (let i = 0; i < len; i++) { + const { y1 } = arcs[i].getLabelBounds(); + delta = y1 - lastY; + if (isLess(delta, 0)) { + const index = this._shiftY(arcs, i, len - 1, -delta); + this._shiftY(arcs, index, 0, delta / 2); + } + const { y2 } = arcs[i].getLabelBounds(); + lastY = y2; + } + // 将超出上界的标签下移 + const { y1: firstY1 } = arcs[0].getLabelBounds(); + delta = firstY1 - 0; + if (isLess(delta, 0)) { + this._shiftY(arcs, 0, len - 1, -delta); + } + for (let i = arcs.length - 1; i >= 0; i--) { + if (arcs[i].getLabelBounds().y2 > plotRect.height) { + arcs[i].labelVisible = false; + } else { + break; + } + } + } else if (labelLayout.strategy !== 'none') { + const priorityArcs: PriorityArc[] = arcs.map((arc, i) => { + return { + arc, + originIndex: i, + priorityIndex: 0 + }; + }); + priorityArcs.sort((a, b) => { + return b.arc.radian - a.arc.radian; + }); + priorityArcs.forEach((priorityArc, i) => { + priorityArc.priorityIndex = i; + // 首先隐藏所有标签 + priorityArc.arc.labelVisible = false; + }); + + let topLabelIndex = Infinity; + let bottomLabelIndex = -Infinity; + // 按照优先级依次布局标签 + for (let i = 0; i < maxLabels && i < arcs.length; i++) { + this._storeY(arcs); + const arc = priorityArcs[i].arc; + this._computeYRange(arc, attribute, currentMarks); + arc.labelVisible = true; + const curY = arc.labelPosition.y; + // 寻找标签在布局前垂直方向上的上下邻居,也就是饼图上的邻居关系 + const { lastIndex, nextIndex } = this._findNeighborIndex(arcs, priorityArcs[i]); + const lastArc = arcs[lastIndex]; + const nextArc = arcs[nextIndex]; + if (lastIndex === -1 && nextIndex !== -1) { + const nextY = nextArc.labelPosition.y; + if (curY > nextY) { + arc.labelPosition.y = nextY - nextArc.labelSize.height / 2 - arc.labelSize.height / 2; + } else { + this._twoWayShift(arcs, arc, nextArc, nextIndex); + } + } else if (lastIndex !== -1 && nextIndex === -1) { + const lastY = lastArc.labelPosition.y; + if (curY < lastY) { + arc.labelPosition.y = lastY + lastArc.labelSize.height / 2 + arc.labelSize.height / 2; + } else { + this._twoWayShift(arcs, lastArc, arc, priorityArcs[i].originIndex); + } + } else if (lastIndex !== -1 && nextIndex !== -1) { + const lastY = lastArc.labelPosition.y; + const nextY = nextArc.labelPosition.y; + if (curY > nextY) { + arc.labelPosition.y = nextY - nextArc.labelSize.height / 2 - arc.labelSize.height / 2; + this._twoWayShift(arcs, lastArc, arc, priorityArcs[i].originIndex); + } else if (curY < lastY) { + arc.labelPosition.y = lastY + lastArc.labelSize.height / 2 + arc.labelSize.height / 2; + this._twoWayShift(arcs, arc, nextArc, nextIndex); + } else { + this._twoWayShift(arcs, lastArc, arc, priorityArcs[i].originIndex); + this._twoWayShift(arcs, arc, nextArc, nextIndex); + } + } + + const nextTopIndex = Math.min(topLabelIndex, priorityArcs[i].originIndex); + const nextBottomIndex = Math.max(bottomLabelIndex, priorityArcs[i].originIndex); + let delta; + // 将超出下界的标签上移 + delta = arcs[nextBottomIndex].getLabelBounds().y2 - plotRect.height; + if (isGreater(delta, 0)) { + this._shiftY(arcs, nextBottomIndex, 0, -delta); + } + // 将超出上界的标签下移 + delta = arcs[nextTopIndex].getLabelBounds().y1 - 0; + if (isLess(delta, 0)) { + this._shiftY(arcs, nextTopIndex, arcs.length - 1, -delta); + } + delta = arcs[nextBottomIndex].getLabelBounds().y2 - plotRect.height; + // 当整体上下移一次之后仍然无法容纳所有标签,则当前标签应当舍去 + if (isGreater(delta, 0)) { + arc.labelVisible = false; + this._restoreY(arcs); + break; + } else if (labelLayout.tangentConstraint && !this._checkYRange(arcs)) { + // 当标签由于 Y 方向调节范围过大而舍弃时不应当终止布局过程 + arc.labelVisible = false; + this._restoreY(arcs); + } else { + topLabelIndex = nextTopIndex; + bottomLabelIndex = nextBottomIndex; + } + } + } + } + + /** + * 向某一方向调整局部标签的 Y 值 + */ + private _shiftY(arcs: ArcInfo[], start: number, end: number, delta: number) { + const direction = start < end ? 1 : -1; + let index = start; + while (index !== -1) { + arcs[index].labelPosition.y += delta; + const nextIndex = this._findNextVisibleIndex(arcs, index, end, direction); + if (nextIndex >= 0 && nextIndex < arcs.length) { + const { y1: curY1, y2: curY2 } = arcs[index].getLabelBounds(); + const { y1: nextY1, y2: nextY2 } = arcs[nextIndex].getLabelBounds(); + if ((direction > 0 && curY2 < nextY1) || (direction < 0 && curY1 > nextY2)) { + return index; + } + } + index = nextIndex; + } + return end; + } + + /** + * 寻找下一个显示标签索引 + */ + private _findNextVisibleIndex(arcs: ArcInfo[], start: number, end: number, direction: number) { + const diff = (end - start) * direction; + for (let i = 1; i <= diff; i++) { + const index = start + i * direction; + if (arcs[index].labelVisible) { + return index; + } + } + return -1; + } + + /** + * 计算 pointB,其 y 值在 adjustY 中确定,也即是 label 的 y 值 + */ + private _computePointB(arc: ArcInfo, r: number, attribute: any, currentMarks: any) { + const labelConfig = attribute; + const radiusRatio = this.computeLayoutOuterRadius( + currentMarks[0].attribute.outerRadius, + attribute.width, + attribute.height + ); + const line1MinLength = labelConfig.line.line1MinLength as number; + const labelLayout = labelConfig.layout; + + if (labelLayout.strategy === 'none') { + // 不执行躲避策略或者不显示引导线时紧挨着圆弧布局 + arc.pointB = { + x: arc.outerCenter.x, + y: arc.outerCenter.y + }; + } else { + const center = attribute.center ?? { x: 0, y: 0 }; + const radius = this.computeRadius(radiusRatio, attribute.width, attribute.height); + const { labelPosition, quadrant } = arc; + const outerR = Math.max(radius + line1MinLength, currentMarks[0].attribute.outerRadius); + const rd = r - outerR; + // x 为 pointB.x 与圆心的差值 + const x = Math.sqrt(r ** 2 - Math.abs((center as IPoint).y - labelPosition.y) ** 2) - rd; + if (isValidNumber(x)) { + arc.pointB = { + x: (center as IPoint).x + x * (isQuadrantLeft(quadrant) ? -1 : 1), + y: labelPosition.y + }; + } else { + arc.pointB = { x: NaN, y: NaN }; + } + } + } + + /** + * 存储当前所有显示标签的 Y 值 + */ + private _storeY(arcs: ArcInfo[]) { + for (const arc of arcs) { + if (arc.labelVisible) { + arc.lastLabelY = arc.labelPosition.y; + } + } + } + + /** + * 计算圆弧切线所限制的标签 Y 值范围 + */ + private _computeYRange(arc: ArcInfo, attribute: any, currentMarks: any) { + const plotRect = { width: attribute.center.x * 2, height: attribute.center.y * 2 }; + + const radiusRatio = this.computeLayoutOuterRadius( + currentMarks[0].attribute.outerRadius, + attribute.width, + attribute.height + ); + const line1MinLength = attribute.line.line1MinLength as number; + + const { width, height } = plotRect; + + const radius = this.computeRadius(radiusRatio, attribute.width, attribute.height); + // 出现 y 方向挤压过度必然是由于画布上下某一端被占满,此时半径是确定的 + const r = this._computeLayoutRadius(height / 2, attribute, currentMarks); + // 所有坐标转化到以圆心为原点的坐标系计算 + // 在饼图上左右计算对称,可以全都转化到右侧计算 + const cx = Math.abs(arc.center.x - width / 2); + const cy = arc.center.y - height / 2; + let a; + let b; + let c; + if (isClose(width / 2, cx)) { + a = 0; + b = 1; + c = -cy; + } else if (isClose(height / 2, cy)) { + a = 1; + b = 0; + c = -cx; + } else { + // 斜截式转为一般式 + const k = -1 / (cy / cx); + a = k; + b = -1; + c = cy - k * cx; + } + const points = lineCirclePoints(a, b, c, line1MinLength + radius - r, 0, r); + // 由于饼图上切点在布局圆内部,交点必然有两个 + if (points.length < 2) { + return; + } + let min; + let max; + if (points[0].x > points[1].x) { + points.reverse(); + } + if (points[0].x < 0) { + if (isClose(points[0].y, points[1].y)) { + if (Math.abs(arc.middleAngle) < Math.PI / 2) { + min = 0; + max = points[1].y + height / 2; + } else { + min = points[1].y + height / 2; + max = height; + } + } else if (points[0].y < points[1].y) { + min = 0; + max = points[1].y + height / 2; + } else { + min = points[1].y + height / 2; + max = plotRect.height; + } + } else { + min = Math.min(points[0].y, points[1].y) + height / 2; + max = Math.max(points[0].y, points[1].y) + height / 2; + } + arc.labelYRange = [min, max]; + } + + /** + * 计算标签布局圆弧半径,即 pointB 所落在的圆弧 + */ + private _computeLayoutRadius(halfYLength: number, attribute: any, currentMarks: any) { + const labelConfig = attribute; + const layoutArcGap = labelConfig.layoutArcGap as number; + const line1MinLength = labelConfig.line.line1MinLength as number; + const radiusRatio = this.computeLayoutOuterRadius( + currentMarks[0].attribute.outerRadius, + attribute.width, + attribute.height + ); + const radius = this.computeRadius(radiusRatio, attribute.width, attribute.height); + const outerR = radius + line1MinLength; + + const a = outerR - layoutArcGap; + + return Math.max((a ** 2 + halfYLength ** 2) / (2 * a), outerR); + } + + /** + * 依据初始的标签排序,寻找某一标签上下最近的显示标签索引 + */ + private _findNeighborIndex(arcs: ArcInfo[], priorityArc: PriorityArc) { + const index = priorityArc.originIndex; + let lastIndex = -1; + let nextIndex = -1; + for (let i = index - 1; i >= 0; i--) { + if (arcs[i].labelVisible) { + lastIndex = i; + break; + } + } + for (let i = index + 1; i < arcs.length; i++) { + if (arcs[i].labelVisible) { + nextIndex = i; + break; + } + } + return { + lastIndex, + nextIndex + }; + } + + /** + * 执行给定标签 Y 值的 shiftDown 以及 shiftUp + */ + private _twoWayShift(arcs: ArcInfo[], lastArc: ArcInfo, nextArc: ArcInfo, nextIndex: number) { + const delta = nextArc.getLabelBounds().y1 - lastArc.getLabelBounds().y2; + if (isLess(delta, 0)) { + const i = this._shiftY(arcs, nextIndex, arcs.length - 1, -delta); + this._shiftY(arcs, i, 0, delta / 2); + } + } + + /** + * 恢复所有显示标签在之前存储的 Y 值 + */ + private _restoreY(arcs: ArcInfo[]) { + for (const arc of arcs) { + if (arc.labelVisible) { + arc.labelPosition.y = arc.lastLabelY; + } + } + } + + /** + * 检查每个显示的标签的 Y 值是否在切线限制范围内 + */ + private _checkYRange(arcs: ArcInfo[]) { + for (const arc of arcs) { + const { labelYRange, labelPosition } = arc; + if ( + arc.labelVisible && + labelYRange && + (isLess(labelPosition.y, labelYRange[0]) || isGreater(labelPosition.y, labelYRange[1])) + ) { + return false; + } + } + return true; + } + + /** + * 自上至下计算被遮盖的标签 + */ + private _coverLabels(arcs: ArcInfo[]) { + if (arcs.length <= 1) { + return; + } + let lastBounds = arcs[0].getLabelBounds(); + for (let i = 1; i < arcs.length; i++) { + const bounds = arcs[i].getLabelBounds(); + if (!checkBoundsOverlap(lastBounds, bounds)) { + lastBounds = bounds; + } else { + arcs[i].labelVisible = false; + } + } + } + + protected computeRadius(r: number, width?: number, height?: number, k?: number): number { + return this.computeLayoutRadius(width ? width : 0, height ? height : 0) * r * (isNil(k) ? 1 : k); + } + + protected computeLayoutRadius(width: number, height: number) { + return Math.min(width / 2, height / 2); + } + + private computeLayoutOuterRadius(r: number, width: number, height: number) { + return r / (Math.min(width, height) / 2); + } + + private computeDatumRadius(width?: number, height?: number, outerRadius?: any): number { + const outerRadiusRatio = this.computeLayoutOuterRadius(outerRadius, width, height); //this.getRadius(state) + return this.computeLayoutRadius(width ? width : 0, height ? height : 0) * outerRadiusRatio; + } +} diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 6d1b8e901..c619bd952 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -1,8 +1,8 @@ /** * @description Label 基类 */ -import type { IGroup, Text, IGraphic, IText, FederatedPointerEvent, IColor } from '@visactor/vrender'; -import { createText, IncreaseCount, AttributeUpdateType } from '@visactor/vrender'; +import type { IGroup, Text, IGraphic, IText, FederatedPointerEvent, IColor, IPath, Path } from '@visactor/vrender'; +import { createText, IncreaseCount, AttributeUpdateType, createPath } from '@visactor/vrender'; import type { IBoundsLike } from '@visactor/vutils'; import { isFunction, isValidNumber, isEmpty } from '@visactor/vutils'; import { AbstractComponent } from '../core/base'; @@ -12,8 +12,9 @@ import { traverseGroup } from '../util'; import { StateValue } from '../constant'; import type { Bitmap } from './overlap'; import { bitmapTool, boundToRange, canPlace, canPlaceInside, place } from './overlap'; -import type { BaseLabelAttrs, OverlapAttrs, ILabelGraphicAttribute, ILabelAnimation } from './type'; +import type { BaseLabelAttrs, OverlapAttrs, ILabelGraphicAttribute, ILabelAnimation, ArcLabelAttrs } from './type'; import { DefaultLabelAnimation, getAnimationAttributes } from './animate/animate'; +import type { ArcInfo } from './arc'; export abstract class LabelBase extends AbstractComponent { name = 'label'; @@ -48,9 +49,20 @@ export abstract class LabelBase extends AbstractCompon textBounds: IBoundsLike, graphicBounds: IBoundsLike, position?: BaseLabelAttrs['position'], - offset?: number + offset?: number, + graphicAttributes?: any, + textData?: any, + width?: number, + height?: number, + attribute?: any ): Partial | undefined; + protected abstract layoutArcLabels( + position?: BaseLabelAttrs['position'], + attribute?: any, + currentMarks?: IGraphic[] + ): any; + protected render() { const currentBaseMarks = this._checkMarks(); const labels = this.layout(currentBaseMarks); @@ -180,6 +192,8 @@ export abstract class LabelBase extends AbstractCompon this._relationMap = new Map(); } + const { width, height } = this.attribute as ArcLabelAttrs; + // 默认根据 index 顺序排序 for (let i = 0; i < data.length; i++) { const textData = data[i]; @@ -196,28 +210,55 @@ export abstract class LabelBase extends AbstractCompon text.update(); const textBounds = this.getGraphicBounds(text); const graphicBounds = this.getGraphicBounds(baseMark, { x: textData.x as number, y: textData.y as number }); + const graphicAttributes = baseMark.attribute; const textAttributes = this.labeling( textBounds, graphicBounds, isFunction(position) ? position(textData) : position, - offset + offset, + graphicAttributes, + textData, + width, + height, + this.attribute ); if (!textAttributes) { continue; } - labelAttribute.x = textAttributes.x; - labelAttribute.y = textAttributes.y; - - labels.push(labelAttribute); + if (this.attribute.type === 'arc') { + labels.push(labelAttribute); + } else { + labelAttribute.x = textAttributes.x; + labelAttribute.y = textAttributes.y; + labels.push(labelAttribute); + } } } this._baseMarks = currentMarks as IGraphic[]; - if (this.attribute.overlap !== false) { - labels = this.overlapping(labels, this.attribute.overlap as OverlapAttrs); + if (this.attribute.type === 'arc') { + const arcs: ArcInfo[] = this.layoutArcLabels(position, this.attribute, currentMarks); + for (let i = 0; i < data.length; i++) { + const textData = data[i]; + const basedArc = arcs.find(arc => arc.labelText === textData.text); + + labels[i].x = basedArc.labelPosition.x; + labels[i].y = basedArc.labelPosition.y; + labels[i].textAlign = basedArc.textAlign; + labels[i].textBaseline = basedArc.textBaseline; + labels[i].angle = basedArc.angle; + + labels[i].pointA = basedArc.pointA; + labels[i].pointB = basedArc.pointB; + labels[i].pointC = basedArc.pointC; + } + } else { + if (this.attribute.overlap !== false) { + labels = this.overlapping(labels, this.attribute.overlap as OverlapAttrs); + } } return labels; @@ -351,18 +392,38 @@ export abstract class LabelBase extends AbstractCompon const currentTextMap = new Map(); const prevTextMap = this._textMap || new Map(); const texts = [] as IText[]; + const labelLines = [] as IPath[]; labels.forEach((label, index) => { const text = this._createLabelText(label); + let labelLine: IPath; + + if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { + labelLine = createPath({ + visible: label?.visible ?? true, + stroke: label?.line?.stroke ?? label?.fill, + lineWidth: 1, + path: + `M${Math.round(label.pointA.x)},${Math.round(label.pointA.y)}` + + ` L${Math.round(label.pointB.x)},${Math.round(label.pointB.y)}` + + ` L${Math.round(label.pointC.x)},${Math.round(label.pointC.y)}` + }) as Path; + } const relatedGraphic = this._relationMap.get(label._relatedIndex); const state = prevTextMap?.get(relatedGraphic) ? 'update' : 'enter'; if (state === 'enter') { texts.push(text); + if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { + labelLines.push(labelLine); + } currentTextMap.set(relatedGraphic, text); if (!disableAnimation && relatedGraphic) { const { from, to } = getAnimationAttributes(label, 'fadeIn'); this.add(text); + if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { + this.add(labelLine); + } relatedGraphic.onAnimateBind = () => { text.setAttributes(from); const listener = this._afterRelatedGraphicAttributeUpdate(text, texts, index, relatedGraphic, { @@ -496,10 +557,18 @@ export abstract class LabelBase extends AbstractCompon continue; } - const isInside = canPlaceInside( + let isInside = canPlaceInside( createText(label).AABBBounds, this._relationMap.get(label._relatedIndex)?.AABBBounds ); + + if (this.attribute.type === 'arc') { + if (this.attribute.position === 'inside') { + isInside = true; + } else { + isInside = false; + } + } /** * stroke 的处理逻辑 * 1. 当文本在图元内部时,有两种情况: diff --git a/packages/vrender-components/src/label/constant.ts b/packages/vrender-components/src/label/constant.ts new file mode 100644 index 000000000..c2a073ab5 --- /dev/null +++ b/packages/vrender-components/src/label/constant.ts @@ -0,0 +1,19 @@ +export const PREFIX = '__VCHART'; + +export const ARC_RATIO = `${PREFIX}_ARC_RATIO`; +export const ARC_START_ANGLE = `${PREFIX}_ARC_START_ANGLE`; +export const ARC_END_ANGLE = `${PREFIX}_ARC_END_ANGLE`; +export const ARC_K = `${PREFIX}_ARC_K`; +export const ARC_LABEL_HOVER_AX = `${PREFIX}_ARC_LABEL_HOVER_AX`; +export const ARC_LABEL_HOVER_AY = `${PREFIX}_ARC_LABEL_HOVER_AY`; +export const ARC_LABEL_POINT_AX = `${PREFIX}_ARC_LABEL_POINT_AX`; +export const ARC_LABEL_POINT_AY = `${PREFIX}_ARC_LABEL_POINT_AY`; +export const ARC_LABEL_POINT_BX = `${PREFIX}_ARC_LABEL_POINT_BX`; +export const ARC_LABEL_POINT_BY = `${PREFIX}_ARC_LABEL_POINT_BY`; +export const ARC_LABEL_POINT_CX = `${PREFIX}_ARC_LABEL_POINT_CX`; +export const ARC_LABEL_POINT_CY = `${PREFIX}_ARC_LABEL_POINT_CY`; +export const ARC_LABEL_SELECTED_AX = `${PREFIX}_ARC_LABEL_SELECTED_AX`; +export const ARC_LABEL_SELECTED_AY = `${PREFIX}_ARC_LABEL_SELECTED_AY`; +export const ARC_MIDDLE_ANGLE = `${PREFIX}_ARC_MIDDLE_ANGLE`; +export const ARC_QUADRANT = `${PREFIX}_ARC_QUADRANT`; +export const ARC_RADIAN = `${PREFIX}_ARC_RADIAN`; diff --git a/packages/vrender-components/src/label/index.ts b/packages/vrender-components/src/label/index.ts index d9fc2da16..3959d46a5 100644 --- a/packages/vrender-components/src/label/index.ts +++ b/packages/vrender-components/src/label/index.ts @@ -3,5 +3,6 @@ export * from './symbol'; export * from './rect'; export * from './line'; export * from './base'; +export * from './arc'; export * from './dataLabel'; diff --git a/packages/vrender-components/src/label/type.ts b/packages/vrender-components/src/label/type.ts index 86fdaad57..b3eda78f1 100644 --- a/packages/vrender-components/src/label/type.ts +++ b/packages/vrender-components/src/label/type.ts @@ -195,6 +195,26 @@ export interface LineLabelAttrs extends BaseLabelAttrs { position?: Functional<'start' | 'end'>; } +export interface ArcLabelAttrs extends BaseLabelAttrs { + type: 'arc'; + + /** + * 图元 group 名称 + */ + baseMarkGroupName: string; + + /** + * 标签位置 + * @default 'outside' + */ + position?: Functional<'inside' | 'outside'>; + + // 画布宽度 + width?: number; + // 画布高度 + height?: number; +} + export interface DataLabelAttrs extends IGroupGraphicAttribute { dataLabels: (RectLabelAttrs | SymbolLabelAttrs)[]; /** @@ -206,6 +226,16 @@ export interface DataLabelAttrs extends IGroupGraphicAttribute { export type Functional = T | ((data: any) => T); export interface ILabelGraphicAttribute extends ITextGraphicAttribute { + angle: any; + textBaseline: string; + fill: any; + line: any; + pointC: IPoint; + pointB: IPoint; + pointA: IPoint; + stroke: any; + visible: boolean; + textAlign: string; _relatedIndex?: number; } @@ -219,3 +249,17 @@ export interface ILabelAnimation { */ increaseEffect?: boolean; } + +export interface IPoint { + x: number; + y: number; +} + +export interface IPolarPoint { + radius: number; + angle: number; +} + +export type Quadrant = 1 | 2 | 3 | 4; + +export type TextAlign = 'left' | 'right' | 'center'; diff --git a/packages/vrender-components/src/label/util.ts b/packages/vrender-components/src/label/util.ts new file mode 100644 index 000000000..e54b46d06 --- /dev/null +++ b/packages/vrender-components/src/label/util.ts @@ -0,0 +1,134 @@ +import type { IPolarPoint, IPoint, Quadrant } from './type'; +import type { IBoundsLike } from '@visactor/vutils'; +import { radianToDegree, isValidNumber } from '@visactor/vutils'; + +/** + * 极坐标系 -> 直角坐标系 + * @param point + * @returns + */ +export function polarToCartesian(point: IPolarPoint): IPoint { + if (!point.radius) { + return { x: 0, y: 0 }; + } + return { + x: Math.cos(point.angle) * point.radius, + y: Math.sin(point.angle) * point.radius + }; +} + +/** + * 计算圆弧上的点坐标 + * @param x0 圆心 x 坐标 + * @param y0 圆心 y 坐标 + * @param radius 圆弧半径 + * @param radian 点所在弧度 + */ +export function circlePoint(x0: number, y0: number, radius: number, radian: number): IPoint { + const offset = polarToCartesian({ + radius, + angle: radian + }); + return { + x: x0 + offset.x, + y: y0 + offset.y + }; +} + +export function isQuadrantLeft(quadrant: Quadrant): boolean { + return quadrant === 3 || quadrant === 4; +} + +export function isQuadrantRight(quadrant: Quadrant): boolean { + return quadrant === 1 || quadrant === 2; +} + +/** + * 计算直线与圆交点 + * 直线方程:ax + by + c = 0 + * 圆方程:(x - x0)^2 + (y - y0)^2 = r^2 + */ +export function lineCirclePoints(a: number, b: number, c: number, x0: number, y0: number, r: number): IPoint[] { + if ((a === 0 && b === 0) || r <= 0) { + return []; + } + if (a === 0) { + const y1 = -c / b; + const fy = (y1 - y0) ** 2; + const fd = r ** 2 - fy; + if (fd < 0) { + return []; + } else if (fd === 0) { + return [{ x: x0, y: y1 }]; + } + const x1 = Math.sqrt(fd) + x0; + const x2 = -Math.sqrt(fd) + x0; + return [ + { x: x1, y: y1 }, + { x: x2, y: y1 } + ]; + } else if (b === 0) { + const x1 = -c / a; + const fx = (x1 - x0) ** 2; + const fd = r ** 2 - fx; + if (fd < 0) { + return []; + } else if (fd === 0) { + return [{ x: x1, y: y0 }]; + } + const y1 = Math.sqrt(fd) + y0; + const y2 = -Math.sqrt(fd) + y0; + return [ + { x: x1, y: y1 }, + { x: x1, y: y2 } + ]; + } + const fa = (b / a) ** 2 + 1; + const fb = 2 * ((c / a + x0) * (b / a) - y0); + const fc = (c / a + x0) ** 2 + y0 ** 2 - r ** 2; + const fd = fb ** 2 - 4 * fa * fc; + if (fd < 0) { + return []; + } + const y1 = (-fb + Math.sqrt(fd)) / (2 * fa); + const y2 = (-fb - Math.sqrt(fd)) / (2 * fa); + const x1 = -(b * y1 + c) / a; + const x2 = -(b * y2 + c) / a; + if (fd === 0) { + return [{ x: x1, y: y1 }]; + } + return [ + { x: x1, y: y1 }, + { x: x2, y: y2 } + ]; +} + +/** + * 根据圆弧两点连接线长度计算弧度 + * @param radius 圆弧半径 + * @param length 连接线长度 + */ +export function connectLineRadian(radius: number, length: number) { + if (length > radius * 2) { + return NaN; + } + return Math.asin(length / 2 / radius) * 2; +} + +export function checkBoundsOverlap(boundsA: IBoundsLike, boundsB: IBoundsLike): boolean { + const { x1: ax1, y1: ay1, x2: ax2, y2: ay2 } = boundsA; + const { x1: bx1, y1: by1, x2: bx2, y2: by2 } = boundsB; + return !( + (ax1 <= bx1 && ax2 <= bx1) || + (ax1 >= bx2 && ax2 >= bx2) || + (ay1 <= by1 && ay2 <= by1) || + (ay1 >= by2 && ay2 >= by2) + ); +} + +export const degrees = (angle?: number) => { + if (!isValidNumber(angle)) { + return null; + } + return radianToDegree(angle); +}; From ad7477f93fc543acefd41056c48fbad03421ac79 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Tue, 25 Jul 2023 21:36:46 +0800 Subject: [PATCH 02/29] feat: change layoutArcLabels of abstractMethod to nonAbstractMethod --- packages/vrender-components/src/label/arc.ts | 4 ++-- packages/vrender-components/src/label/base.ts | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts index e4d8da1b5..9e37b6a7b 100644 --- a/packages/vrender-components/src/label/arc.ts +++ b/packages/vrender-components/src/label/arc.ts @@ -2,7 +2,7 @@ import type { IBoundsLike } from '@visactor/vutils'; import { merge } from '@visactor/vutils'; import { LabelBase } from './base'; import type { ArcLabelAttrs, IPoint, Quadrant, TextAlign } from './type'; -import { IPolarPoint } from './type'; +import type { BaseLabelAttrs } from './type'; import type { ITextGraphicAttribute } from '@visactor/vrender'; import { isValidNumber, isNil, isLess, isGreater, isNumberClose as isClose } from '@visactor/vutils'; import { @@ -204,7 +204,7 @@ export class ArcLabel extends LabelBase { } // layoutLabels : 执行内部/外部标签的布局计算 - protected layoutArcLabels(position = 'outside', attribute: any, currentMarks?: IGraphic[]) { + protected layoutArcLabels(position: BaseLabelAttrs['position'], attribute: any, currentMarks?: IGraphic[]) { const leftArcs = Array.from(this._arcLeft.values()); const rightArcs = Array.from(this._arcRight.values()); const arcs: ArcInfo[] = []; diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index c619bd952..d79f9f6dd 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -57,11 +57,10 @@ export abstract class LabelBase extends AbstractCompon attribute?: any ): Partial | undefined; - protected abstract layoutArcLabels( - position?: BaseLabelAttrs['position'], - attribute?: any, - currentMarks?: IGraphic[] - ): any; + protected layoutArcLabels(position?: BaseLabelAttrs['position'], attribute?: any, currentMarks?: IGraphic[]): any { + const arcs: ArcInfo[] = []; + return arcs; + } protected render() { const currentBaseMarks = this._checkMarks(); From 8ca704c3db7d4925f2f35a3d7bf7de2c5adacb98 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Tue, 25 Jul 2023 21:45:29 +0800 Subject: [PATCH 03/29] feat: change label type as ITextGraphicAttribute to fit createText --- packages/vrender-components/src/label/base.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index d79f9f6dd..899253695 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -1,7 +1,17 @@ /** * @description Label 基类 */ -import type { IGroup, Text, IGraphic, IText, FederatedPointerEvent, IColor, IPath, Path } from '@visactor/vrender'; +import type { + IGroup, + Text, + IGraphic, + IText, + FederatedPointerEvent, + IColor, + IPath, + Path, + ITextGraphicAttribute +} from '@visactor/vrender'; import { createText, IncreaseCount, AttributeUpdateType, createPath } from '@visactor/vrender'; import type { IBoundsLike } from '@visactor/vutils'; import { isFunction, isValidNumber, isEmpty } from '@visactor/vutils'; @@ -557,7 +567,7 @@ export abstract class LabelBase extends AbstractCompon } let isInside = canPlaceInside( - createText(label).AABBBounds, + createText(label as ITextGraphicAttribute).AABBBounds, this._relationMap.get(label._relatedIndex)?.AABBBounds ); From d93de40aa7027976e80a51dc6357e9a78dc70937 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Tue, 25 Jul 2023 21:49:42 +0800 Subject: [PATCH 04/29] feat: change label type as ITextGraphicAttribute to fit getAnimationAttributes --- packages/vrender-components/src/label/base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 899253695..6bad6b968 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -428,7 +428,7 @@ export abstract class LabelBase extends AbstractCompon } currentTextMap.set(relatedGraphic, text); if (!disableAnimation && relatedGraphic) { - const { from, to } = getAnimationAttributes(label, 'fadeIn'); + const { from, to } = getAnimationAttributes(label as ITextGraphicAttribute, 'fadeIn'); this.add(text); if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { this.add(labelLine); From 802550303ddd28984e7de0a269807712f25e1da9 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Thu, 27 Jul 2023 19:42:03 +0800 Subject: [PATCH 05/29] feat: merge latest develop --- common/config/rush/pnpm-lock.yaml | 164 +++++++++--------- .../vrender-components/src/label/dataLabel.ts | 8 +- 2 files changed, 90 insertions(+), 82 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index ae4724b4d..a3901ae57 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -28,7 +28,7 @@ importers: dependencies: '@visactor/vrender': link:../../packages/vrender devDependencies: - '@antv/g': 5.18.7 + '@antv/g': 5.18.8 '@esbuild-plugins/node-globals-polyfill': 0.1.1 '@esbuild-plugins/node-modules-polyfill': 0.1.4 '@internal/eslint-config': link:../../share/eslint-config @@ -433,7 +433,7 @@ importers: '@types/merge2': 1.4.0 '@types/minimist': 1.2.2 '@types/ms': 0.7.31 - '@types/node': 20.4.4 + '@types/node': 20.4.5 '@types/semver': 7.3.12 '@types/terser': 3.12.0 '@types/through2': 2.0.38 @@ -441,7 +441,7 @@ importers: '@types/vinyl': 2.0.7 '@types/yargs-parser': 21.0.0 eslint: 8.18.0 - ts-node: 10.9.0_f7m3oso53tushofr3lnovlz6ya + ts-node: 10.9.0_gamchj74f4cosewgiaiggc3fvy typescript: 4.9.5 vitest: 0.30.1_less@4.1.3+terser@5.17.1 @@ -458,25 +458,25 @@ packages: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.18 - /@antv/g-camera-api/1.2.6: - resolution: {integrity: sha512-u2vvGba/eZKPNVs4m/9/NEVz9DCT3U3jjnzV9IPFMpB+E73Exqed1dBNGnvCFqXJmbatFU2+dB1K0FGJqDsW+A==} + /@antv/g-camera-api/1.2.7: + resolution: {integrity: sha512-gshIoy+5slnM4KAuAaVMKdyWaB6jeGDju6CdO30DGrxWsD0Cct2GcfVZt+Opx3bKYB7TJ6ws6H03yzcaWYnFTg==} dependencies: - '@antv/g-lite': 1.2.6 + '@antv/g-lite': 1.2.7 '@antv/util': 3.3.4 gl-matrix: 3.4.3 tslib: 2.6.1 dev: true - /@antv/g-dom-mutation-observer-api/1.2.6: - resolution: {integrity: sha512-IQ5z+8vCekhghlwE4sazXZO+vYhAzdO8GfdmIJJfAO2HDbuCQ3NkX+Tc0OiFkyxgO4tHunAcdtHvD8Pz8yYThQ==} + /@antv/g-dom-mutation-observer-api/1.2.7: + resolution: {integrity: sha512-lUTgROvNywkyr4tw8rLVIHfA1rCI6qm17X0sUy6JwjGKG87Yzz0VBZz5AMNv1IRaPIP7nEMtcfJ29HJAu3+X0A==} dependencies: - '@antv/g-lite': 1.2.6 + '@antv/g-lite': 1.2.7 dev: true - /@antv/g-lite/1.2.6: - resolution: {integrity: sha512-1yLjLwrC9lny7oQ2qrxQWZmLOQmQF3MzXnDHgMumNmPjRiWWGFRUyxO2Ks9V/M0o/ieyp5E4JjphHh2tnhTySg==} + /@antv/g-lite/1.2.7: + resolution: {integrity: sha512-rTToCzfZviOzJeHS0hJvRpEyWv7/6sMe7cyIXLH/BbDYpIOscx0/AV536Zd4I2dVtRiw5wpAU25FQl+Sr7hDHg==} dependencies: - '@antv/g-math': 2.0.1 + '@antv/g-math': 2.0.2 '@antv/util': 3.3.4 d3-color: 1.4.1 eventemitter3: 5.0.1 @@ -485,29 +485,29 @@ packages: tslib: 2.6.1 dev: true - /@antv/g-math/2.0.1: - resolution: {integrity: sha512-Y1DREalYzUMaglD0m6V9s9UxsseXgSmwNA3l9zDHRH1CXDLcvtmrku+DQM9oCgPtbPWV43AdyAYoNT3WLyL/WA==} + /@antv/g-math/2.0.2: + resolution: {integrity: sha512-uqGU1C+70orjeSUoIzD3TuXjL5dRQCIyjZrBrTmm0FWd6VQJMWHyG5ypuZ2lMiI5MrRajVSE1w+3J4hiNBYSJg==} dependencies: '@antv/util': 3.3.4 gl-matrix: 3.4.3 tslib: 2.6.1 dev: true - /@antv/g-web-animations-api/1.2.6: - resolution: {integrity: sha512-EeKGjUhdC41XvljfgHHhR9X6rYozGmRKVkh27VrHGONw5SIFgwBr4wJEpkYZ++Lfy8xx0dx8YlAYCcocbiQKkA==} + /@antv/g-web-animations-api/1.2.7: + resolution: {integrity: sha512-lrsZIHtcbUwkw25bV3cI8CJ8phhraewR+AI5mKqKEx+vlngd7TvfGn3aG/jQ1IA01NgkULGNQENJCySVCHtD4w==} dependencies: - '@antv/g-lite': 1.2.6 + '@antv/g-lite': 1.2.7 '@antv/util': 3.3.4 tslib: 2.6.1 dev: true - /@antv/g/5.18.7: - resolution: {integrity: sha512-t6PPs3Ow2xFGiKXGahOKmVjKsDcmhtbBMJ3CQz25DX6sRcvELR3erl5CpabFLeRue5qUFzPY5S0LwnI/Ebvvjw==} + /@antv/g/5.18.8: + resolution: {integrity: sha512-kv3AZmbOrPKncfiJ5lVaCQyMVBAxmJUppzBqU+6QplLQFDo73wBGNwHJX4LuucYO7CfcNjN94wIcT0CnX6Ls5A==} dependencies: - '@antv/g-camera-api': 1.2.6 - '@antv/g-dom-mutation-observer-api': 1.2.6 - '@antv/g-lite': 1.2.6 - '@antv/g-web-animations-api': 1.2.6 + '@antv/g-camera-api': 1.2.7 + '@antv/g-dom-mutation-observer-api': 1.2.7 + '@antv/g-lite': 1.2.7 + '@antv/g-web-animations-api': 1.2.7 dev: true /@antv/util/3.3.4: @@ -1915,7 +1915,7 @@ packages: engines: {node: '>= 10.14.2'} dependencies: '@jest/types': 26.6.2 - '@types/node': 20.4.4 + '@types/node': 20.4.5 chalk: 4.1.2 jest-message-util: 26.6.2 jest-util: 26.6.2 @@ -1930,7 +1930,7 @@ packages: '@jest/test-result': 26.6.2 '@jest/transform': 26.6.2 '@jest/types': 26.6.2 - '@types/node': 20.4.4 + '@types/node': 20.4.5 ansi-escapes: 4.3.2 chalk: 4.1.2 exit: 0.1.2 @@ -1970,7 +1970,7 @@ packages: '@jest/test-result': 26.6.2 '@jest/transform': 26.6.2 '@jest/types': 26.6.2 - '@types/node': 20.4.4 + '@types/node': 20.4.5 ansi-escapes: 4.3.2 chalk: 4.1.2 exit: 0.1.2 @@ -2018,7 +2018,7 @@ packages: dependencies: '@jest/fake-timers': 26.6.2 '@jest/types': 26.6.2 - '@types/node': 20.4.4 + '@types/node': 20.4.5 jest-mock: 26.6.2 /@jest/fake-timers/24.9.0: @@ -2036,7 +2036,7 @@ packages: dependencies: '@jest/types': 26.6.2 '@sinonjs/fake-timers': 6.0.1 - '@types/node': 20.4.4 + '@types/node': 20.4.5 jest-message-util: 26.6.2 jest-mock: 26.6.2 jest-util: 26.6.2 @@ -2065,9 +2065,9 @@ packages: graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.0 istanbul-lib-instrument: 4.0.3 - istanbul-lib-report: 3.0.0 + istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.1.5 + istanbul-reports: 3.1.6 jest-haste-map: 26.6.2 jest-resolve: 26.6.2 jest-util: 26.6.2 @@ -2223,7 +2223,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.4.4 + '@types/node': 20.4.5 '@types/yargs': 15.0.15 chalk: 4.1.2 @@ -2686,7 +2686,7 @@ packages: /@types/clean-css/4.2.6: resolution: {integrity: sha512-Ze1tf+LnGPmG6hBFMi0B4TEB0mhF7EiMM5oyjLDNPE9hxrPU0W+5+bHvO+eFPA+bt0iC1zkQMoU/iGdRVjcRbw==} dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 source-map: 0.6.1 dev: true @@ -2706,13 +2706,13 @@ packages: /@types/fs-extra/9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 dev: true /@types/glob-stream/8.0.0: resolution: {integrity: sha512-fxTWwdQmX9LWSHD7ZLlv3BHR992mKcVcDnT/2v+l/QZZo7TfDdyasqlSYVzOnMGWhRbrWeWkbj/mgezFjKynhw==} dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 '@types/picomatch': 2.3.0 '@types/streamx': 2.9.1 dev: true @@ -2720,18 +2720,18 @@ packages: /@types/glob-watcher/5.0.2: resolution: {integrity: sha512-MZeh2nIzibl/euv5UV0femkGzcKTSE4G2+zv48d6ymeitWwCx52+4X+FqzML9oH2mQnPs+N/JHp3CsBPj1x1Ug==} dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 dev: true /@types/graceful-fs/4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 /@types/gulp-if/0.0.34: resolution: {integrity: sha512-r2A04hHDC+ZWMRAm+3q6/UeC3ggvl+TZm9P1+2umnp4q9bOlBmUQnR178Io3c0DkZPQAwup8VNtOvmvaWCpP5w==} dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 '@types/vinyl': 2.0.7 dev: true @@ -2745,7 +2745,7 @@ packages: /@types/gulp-sourcemaps/0.0.35: resolution: {integrity: sha512-vUBuizwA4CAV3Mke0DJYHQxyN4YOB1aAql284qAO7Et7fe0hmnPi/R9Fhu2UhxMuSxAwFktsJUOQk5dJHOU1eA==} dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 '@types/vinyl': 2.0.7 dev: true @@ -2799,7 +2799,7 @@ packages: /@types/merge2/1.4.0: resolution: {integrity: sha512-MRHDvln2ldZELrUC8n1PGaQzZ33aNh8uDcsGehREW0zR1Fr818a4/JTZjO9eloHPPxnpUp8fz/YFTRc5CWm7Xw==} dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 dev: true /@types/minimatch/5.1.2: @@ -2817,7 +2817,7 @@ packages: /@types/node-fetch/2.6.4: resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==} dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 form-data: 3.0.1 dev: true @@ -2825,8 +2825,8 @@ packages: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} dev: true - /@types/node/20.4.4: - resolution: {integrity: sha512-CukZhumInROvLq3+b5gLev+vgpsIqC2D0deQr/yS1WnxvmYLlJXZpaQrQiseMY+6xusl79E04UjWoqyr+t1/Ew==} + /@types/node/20.4.5: + resolution: {integrity: sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==} /@types/normalize-package-data/2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} @@ -2879,7 +2879,7 @@ packages: /@types/streamx/2.9.1: resolution: {integrity: sha512-9bywzhouyedmci7WCIPFwJ8zASDnxt2gaVUy52X0p0Tt085IJSAEP0L6j4SSNeDMSLzpYu6cPz0GrJZ7kPJ6Bg==} dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 dev: true /@types/terser/3.12.0: @@ -2892,7 +2892,7 @@ packages: /@types/through2/2.0.38: resolution: {integrity: sha512-YFu+nHmjxMurkH1BSzA0Z1WrKDAY8jUKPZctNQn7mc+/KKtp2XxnclHFXxdB1m7Iqnzb5aywgP8TMK283LezGQ==} dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 dev: true /@types/undertaker-registry/1.0.1: @@ -2902,7 +2902,7 @@ packages: /@types/undertaker/1.2.8: resolution: {integrity: sha512-gW3PRqCHYpo45XFQHJBhch7L6hytPsIe0QeLujlnFsjHPnXLhJcPdN6a9368d7aIQgH2I/dUTPFBlGeSNA3qOg==} dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 '@types/undertaker-registry': 1.0.1 async-done: 1.3.2 dev: true @@ -2911,7 +2911,7 @@ packages: resolution: {integrity: sha512-ctNcmmzbMIKooXjRkyyUCOu2Z4AyqibL+RhXoF3pb7K7j+ezItnakmpm31LymkYHSIM5ey0tjIFzTvFOTSBCGw==} dependencies: '@types/glob-stream': 8.0.0 - '@types/node': 20.4.4 + '@types/node': 20.4.5 '@types/vinyl': 2.0.7 dev: true @@ -2919,7 +2919,7 @@ packages: resolution: {integrity: sha512-4UqPv+2567NhMQuMLdKAyK4yzrfCqwaTt6bLhHEs8PFcxbHILsrxaY63n4wgE/BRLDWDQeI+WcTmkXKExh9hQg==} dependencies: '@types/expect': 1.20.4 - '@types/node': 20.4.4 + '@types/node': 20.4.5 /@types/yargs-parser/21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} @@ -3826,7 +3826,7 @@ packages: hasBin: true dependencies: caniuse-lite: 1.0.30001517 - electron-to-chromium: 1.4.470 + electron-to-chromium: 1.4.473 node-releases: 2.0.13 update-browserslist-db: 1.0.11_browserslist@4.21.9 @@ -4570,8 +4570,8 @@ packages: safer-buffer: 2.1.2 dev: true - /electron-to-chromium/1.4.470: - resolution: {integrity: sha512-zZM48Lmy2FKWgqyvsX9XK+J6FfP7aCDUFLmgooLJzA7v1agCs/sxSoBpTIwDLhmbhpx9yJIxj2INig/ncjJRqg==} + /electron-to-chromium/1.4.473: + resolution: {integrity: sha512-aVfC8+440vGfl06l8HKKn8/PD5jRfSnLkTTD65EFvU46igbpQRri1gxSzW9/+TeUlwYzrXk1sw867T96zlyECA==} /electron/11.5.0: resolution: {integrity: sha512-WjNDd6lGpxyiNjE3LhnFCAk/D9GIj1rU3GSDealVShhkkkPR3Vh4q8ErXGDl1OAO/faomVa10KoFPUN/pLbNxg==} @@ -5902,7 +5902,7 @@ packages: resolution: {integrity: sha512-SVSF7ikuWKhpAW4l4wapAqPPSToJoiNKsbDoUnRrSgwZHH7lH8pbPeQj1aOVYQrbZKhfSVBxVW+Py7vtulRktw==} engines: {node: '>=10'} dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 '@types/vinyl': 2.0.7 istextorbinary: 3.3.0 replacestream: 4.0.3 @@ -6562,12 +6562,12 @@ packages: transitivePeerDependencies: - supports-color - /istanbul-lib-report/3.0.0: - resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} - engines: {node: '>=8'} + /istanbul-lib-report/3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} dependencies: istanbul-lib-coverage: 3.2.0 - make-dir: 3.1.0 + make-dir: 4.0.0 supports-color: 7.2.0 /istanbul-lib-source-maps/4.0.1: @@ -6580,12 +6580,12 @@ packages: transitivePeerDependencies: - supports-color - /istanbul-reports/3.1.5: - resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} + /istanbul-reports/3.1.6: + resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} engines: {node: '>=8'} dependencies: html-escaper: 2.0.2 - istanbul-lib-report: 3.0.0 + istanbul-lib-report: 3.0.1 /istextorbinary/3.3.0: resolution: {integrity: sha512-Tvq1W6NAcZeJ8op+Hq7tdZ434rqnMx4CCZ7H0ff83uEloDvVbqAwaMTZcafKGJT0VHkYzuXUiCY4hlXQg6WfoQ==} @@ -6851,7 +6851,7 @@ packages: '@jest/environment': 26.6.2 '@jest/fake-timers': 26.6.2 '@jest/types': 26.6.2 - '@types/node': 20.4.4 + '@types/node': 20.4.5 jest-mock: 26.6.2 jest-util: 26.6.2 jsdom: 16.7.0 @@ -6881,7 +6881,7 @@ packages: '@jest/environment': 26.6.2 '@jest/fake-timers': 26.6.2 '@jest/types': 26.6.2 - '@types/node': 20.4.4 + '@types/node': 20.4.5 jest-mock: 26.6.2 jest-util: 26.6.2 @@ -6934,7 +6934,7 @@ packages: dependencies: '@jest/types': 26.6.2 '@types/graceful-fs': 4.1.6 - '@types/node': 20.4.4 + '@types/node': 20.4.5 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -6981,7 +6981,7 @@ packages: '@jest/source-map': 26.6.2 '@jest/test-result': 26.6.2 '@jest/types': 26.6.2 - '@types/node': 20.4.4 + '@types/node': 20.4.5 chalk: 4.1.2 co: 4.6.0 expect: 26.6.2 @@ -7011,7 +7011,7 @@ packages: '@jest/source-map': 26.6.2 '@jest/test-result': 26.6.2 '@jest/types': 26.6.2 - '@types/node': 20.4.4 + '@types/node': 20.4.5 chalk: 4.1.2 co: 4.6.0 expect: 26.6.2 @@ -7115,7 +7115,7 @@ packages: engines: {node: '>= 10.14.2'} dependencies: '@jest/types': 26.6.2 - '@types/node': 20.4.4 + '@types/node': 20.4.5 /jest-pnp-resolver/1.2.3_jest-resolve@24.9.0: resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} @@ -7216,7 +7216,7 @@ packages: '@jest/environment': 26.6.2 '@jest/test-result': 26.6.2 '@jest/types': 26.6.2 - '@types/node': 20.4.4 + '@types/node': 20.4.5 chalk: 4.1.2 emittery: 0.7.2 exit: 0.1.2 @@ -7248,7 +7248,7 @@ packages: '@jest/environment': 26.6.2 '@jest/test-result': 26.6.2 '@jest/types': 26.6.2 - '@types/node': 20.4.4 + '@types/node': 20.4.5 chalk: 4.1.2 emittery: 0.7.2 exit: 0.1.2 @@ -7391,7 +7391,7 @@ packages: resolution: {integrity: sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==} engines: {node: '>= 10.14.2'} dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 graceful-fs: 4.2.11 /jest-snapshot/24.9.0: @@ -7457,7 +7457,7 @@ packages: engines: {node: '>= 10.14.2'} dependencies: '@jest/types': 26.6.2 - '@types/node': 20.4.4 + '@types/node': 20.4.5 chalk: 4.1.2 graceful-fs: 4.2.11 is-ci: 2.0.0 @@ -7492,7 +7492,7 @@ packages: dependencies: '@jest/test-result': 26.6.2 '@jest/types': 26.6.2 - '@types/node': 20.4.4 + '@types/node': 20.4.5 ansi-escapes: 4.3.2 chalk: 4.1.2 jest-util: 26.6.2 @@ -7510,7 +7510,7 @@ packages: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 merge-stream: 2.0.0 supports-color: 7.2.0 @@ -7976,6 +7976,12 @@ packages: dependencies: semver: 6.3.1 + /make-dir/4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + /make-error/1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -10225,7 +10231,7 @@ packages: typescript: 4.9.5 yargs-parser: 20.2.9 - /ts-node/10.9.0_f7m3oso53tushofr3lnovlz6ya: + /ts-node/10.9.0_gamchj74f4cosewgiaiggc3fvy: resolution: {integrity: sha512-bunW18GUyaCSYRev4DPf4SQpom3pWH29wKl0sDk5zE7ze19RImEVhCW7K4v3hHKkUyfWotU08ToE2RS+Y49aug==} hasBin: true peerDependencies: @@ -10244,7 +10250,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.4.4 + '@types/node': 20.4.5 acorn: 8.10.0 acorn-walk: 8.2.0 arg: 4.1.3 @@ -10668,7 +10674,7 @@ packages: replace-ext: 1.0.1 dev: false - /vite-node/0.30.1_xp5gls52nxtb52zcnq7yx3ryey: + /vite-node/0.30.1_c4rv5bqevikf6haxibcjp2765a: resolution: {integrity: sha512-vTikpU/J7e6LU/8iM3dzBo8ZhEiKZEKRznEMm+mJh95XhWaPrJQraT/QsT2NWmuEf+zgAoMe64PKT7hfZ1Njmg==} engines: {node: '>=v14.18.0'} hasBin: true @@ -10678,7 +10684,7 @@ packages: mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 - vite: 3.2.6_xp5gls52nxtb52zcnq7yx3ryey + vite: 3.2.6_c4rv5bqevikf6haxibcjp2765a transitivePeerDependencies: - '@types/node' - less @@ -10722,7 +10728,7 @@ packages: fsevents: 2.3.2 dev: true - /vite/3.2.6_xp5gls52nxtb52zcnq7yx3ryey: + /vite/3.2.6_c4rv5bqevikf6haxibcjp2765a: resolution: {integrity: sha512-nTXTxYVvaQNLoW5BQ8PNNQ3lPia57gzsQU/Khv+JvzKPku8kNZL6NMUR/qwXhMG6E+g1idqEPanomJ+VZgixEg==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -10747,7 +10753,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.4.4 + '@types/node': 20.4.5 esbuild: 0.15.18 less: 4.1.3 postcss: 8.4.21 @@ -10791,7 +10797,7 @@ packages: dependencies: '@types/chai': 4.3.5 '@types/chai-subset': 1.3.3 - '@types/node': 20.4.4 + '@types/node': 20.4.5 '@vitest/expect': 0.30.1 '@vitest/runner': 0.30.1 '@vitest/snapshot': 0.30.1 @@ -10812,8 +10818,8 @@ packages: strip-literal: 1.0.1 tinybench: 2.5.0 tinypool: 0.4.0 - vite: 3.2.6_xp5gls52nxtb52zcnq7yx3ryey - vite-node: 0.30.1_xp5gls52nxtb52zcnq7yx3ryey + vite: 3.2.6_c4rv5bqevikf6haxibcjp2765a + vite-node: 0.30.1_c4rv5bqevikf6haxibcjp2765a why-is-node-running: 2.2.2 transitivePeerDependencies: - less diff --git a/packages/vrender-components/src/label/dataLabel.ts b/packages/vrender-components/src/label/dataLabel.ts index 80b326d57..4a58b7719 100644 --- a/packages/vrender-components/src/label/dataLabel.ts +++ b/packages/vrender-components/src/label/dataLabel.ts @@ -1,16 +1,18 @@ import { isValidNumber, merge } from '@visactor/vutils'; -import { IGraphic, INode } from '@visactor/vrender'; +import type { IGraphic, INode } from '@visactor/vrender'; import { AbstractComponent } from '../core/base'; import type { PointLocationCfg } from '../core/type'; import { bitmapTool } from './overlap'; import { RectLabel } from './rect'; import { SymbolLabel } from './symbol'; +import { ArcLabel } from './arc'; import type { DataLabelAttrs } from './type'; -import { LabelBase } from './base'; +import type { LabelBase } from './base'; const labelComponentMap = { rect: RectLabel, - symbol: SymbolLabel + symbol: SymbolLabel, + arc: ArcLabel }; export class DataLabel extends AbstractComponent { From 7a31a8ebade14a40999165d4b488a99af784edc9 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Thu, 27 Jul 2023 21:36:03 +0800 Subject: [PATCH 06/29] feat: add interface for ArcLabelAttrs --- packages/vrender-components/src/label/type.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/vrender-components/src/label/type.ts b/packages/vrender-components/src/label/type.ts index b3eda78f1..41b5b610d 100644 --- a/packages/vrender-components/src/label/type.ts +++ b/packages/vrender-components/src/label/type.ts @@ -213,6 +213,74 @@ export interface ArcLabelAttrs extends BaseLabelAttrs { width?: number; // 画布高度 height?: number; + + /** + * 是否允许标签重叠 + * @default false + */ + coverEnable?: boolean; + /** + * 是否允许标签旋转 + * @default true + */ + rotate?: boolean; + + /** + * 文字与引导线间隔宽度 + * @default 5 + */ + spaceWidth?: number; + /** + * 扇区间标签的间隔 + * @default 6 + */ + layoutArcGap?: number; + /** 标签引导线样式 */ + line?: IArcLabelLineSpec; + /** 标签布局配置 */ + layout?: IArcLabelLayoutSpec; +} + +export interface IArcLabelLineSpec { + /** + * 是否显示引导线 + * @default true + */ + visible?: boolean; + /** + * 引导线 line1 部分最小长度 + * @default 20 + */ + line1MinLength?: number; + /** + * 引导线 line2 部分最小长度 + * @default 10 + */ + line2MinLength?: number; +} + +export type ArcLabelAlignType = 'arc' | 'labelLine' | 'edge'; + +export type ArcLabelStrategyType = 'priority' | 'vertical' | 'none'; + +export interface IArcLabelLayoutSpec { + /** + * 标签对齐方式 + * @default 'arc' + */ + textAlign?: ArcLabelAlignType; + /** @deprecate 建议统一使用textAlign,后续将废除 */ + align?: ArcLabelAlignType; + /** + * 标签布局策略 + * @default 'priority' + */ + strategy?: ArcLabelStrategyType; + /** + * 是否启用切线约束 + * @default true + */ + tangentConstraint?: boolean; } export interface DataLabelAttrs extends IGroupGraphicAttribute { From aa87f1b7f1e2c87969d9634421ff1fce807f9bc5 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Thu, 27 Jul 2023 21:50:40 +0800 Subject: [PATCH 07/29] feat: change required properties to optional in labelGraphicAttribute --- packages/vrender-components/src/label/type.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/vrender-components/src/label/type.ts b/packages/vrender-components/src/label/type.ts index 41b5b610d..8ec3b6f10 100644 --- a/packages/vrender-components/src/label/type.ts +++ b/packages/vrender-components/src/label/type.ts @@ -294,17 +294,17 @@ export interface DataLabelAttrs extends IGroupGraphicAttribute { export type Functional = T | ((data: any) => T); export interface ILabelGraphicAttribute extends ITextGraphicAttribute { - angle: any; - textBaseline: string; - fill: any; - line: any; - pointC: IPoint; - pointB: IPoint; - pointA: IPoint; - stroke: any; - visible: boolean; - textAlign: string; _relatedIndex?: number; + + angle?: any; + textBaseline?: string; + fill?: any; + line?: any; + pointC?: IPoint; + pointB?: IPoint; + pointA?: IPoint; + stroke?: any; + visible?: boolean; } export interface ILabelAnimation { From 8558613f490a2b61e1997d77a1402f92c28ca9de Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Thu, 27 Jul 2023 21:57:57 +0800 Subject: [PATCH 08/29] feat: remove duplicate properties in ILabelGraphicAttribute --- packages/vrender-components/src/label/type.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/vrender-components/src/label/type.ts b/packages/vrender-components/src/label/type.ts index 8ec3b6f10..c1a64ea58 100644 --- a/packages/vrender-components/src/label/type.ts +++ b/packages/vrender-components/src/label/type.ts @@ -296,15 +296,10 @@ export type Functional = T | ((data: any) => T); export interface ILabelGraphicAttribute extends ITextGraphicAttribute { _relatedIndex?: number; - angle?: any; - textBaseline?: string; - fill?: any; line?: any; pointC?: IPoint; pointB?: IPoint; pointA?: IPoint; - stroke?: any; - visible?: boolean; } export interface ILabelAnimation { From d6879d9b2e7dc215048ccb0bdd106b6edb1428c1 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Fri, 28 Jul 2023 14:39:46 +0800 Subject: [PATCH 09/29] feat: merge label support custom layout function #191, solve conflicts --- ...-label_custom_layout_2023-07-21-07-11.json | 10 + ...ractive-layer-create_2023-07-27-10-01.json | 10 + .../fix-area-stroke_2023-07-27-11-48.json | 10 + ...ractive-layer-create_2023-07-27-10-01.json | 10 + docs/demos/src/pages/animate.ts | 107 ++++--- docs/demos/src/pages/area.ts | 52 +-- docs/demos/src/pages/text.ts | 6 +- .../__tests__/browser/examples/label-line.ts | 4 +- packages/vrender-components/src/label/base.ts | 296 ++++++++++-------- .../src/label/overlap/place.ts | 38 ++- packages/vrender-components/src/label/rect.ts | 10 +- .../vrender-components/src/label/symbol.ts | 10 +- packages/vrender-components/src/label/type.ts | 47 +-- .../src/poptip/contribution.ts | 1 + packages/vrender/src/core/stage.ts | 14 +- .../graphic-service/graphic-service.ts | 14 + packages/vrender/src/interface/stage.ts | 1 + .../contributions/render/area-render.ts | 42 +-- 18 files changed, 418 insertions(+), 264 deletions(-) create mode 100644 common/changes/@visactor/vrender-components/feat-label_custom_layout_2023-07-21-07-11.json create mode 100644 common/changes/@visactor/vrender-components/fix-interactive-layer-create_2023-07-27-10-01.json create mode 100644 common/changes/@visactor/vrender/fix-area-stroke_2023-07-27-11-48.json create mode 100644 common/changes/@visactor/vrender/fix-interactive-layer-create_2023-07-27-10-01.json diff --git a/common/changes/@visactor/vrender-components/feat-label_custom_layout_2023-07-21-07-11.json b/common/changes/@visactor/vrender-components/feat-label_custom_layout_2023-07-21-07-11.json new file mode 100644 index 000000000..8a79f6a0a --- /dev/null +++ b/common/changes/@visactor/vrender-components/feat-label_custom_layout_2023-07-21-07-11.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-components", + "comment": "feat(component): label component support custom layout and dataFilter", + "type": "patch" + } + ], + "packageName": "@visactor/vrender-components" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-components/fix-interactive-layer-create_2023-07-27-10-01.json b/common/changes/@visactor/vrender-components/fix-interactive-layer-create_2023-07-27-10-01.json new file mode 100644 index 000000000..822bef9d0 --- /dev/null +++ b/common/changes/@visactor/vrender-components/fix-interactive-layer-create_2023-07-27-10-01.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-components", + "comment": "fix: fix the issue of using interactive layer in only on canvas env", + "type": "patch" + } + ], + "packageName": "@visactor/vrender-components" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender/fix-area-stroke_2023-07-27-11-48.json b/common/changes/@visactor/vrender/fix-area-stroke_2023-07-27-11-48.json new file mode 100644 index 000000000..3ed4c97fd --- /dev/null +++ b/common/changes/@visactor/vrender/fix-area-stroke_2023-07-27-11-48.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender", + "comment": "fix: fix the issue of area stroke with texture", + "type": "patch" + } + ], + "packageName": "@visactor/vrender" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender/fix-interactive-layer-create_2023-07-27-10-01.json b/common/changes/@visactor/vrender/fix-interactive-layer-create_2023-07-27-10-01.json new file mode 100644 index 000000000..fffa33b43 --- /dev/null +++ b/common/changes/@visactor/vrender/fix-interactive-layer-create_2023-07-27-10-01.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender", + "comment": "fix: fix the issue of using interactive layer in only on canvas env", + "type": "patch" + } + ], + "packageName": "@visactor/vrender" +} \ No newline at end of file diff --git a/docs/demos/src/pages/animate.ts b/docs/demos/src/pages/animate.ts index c459a6bd8..8dc696557 100644 --- a/docs/demos/src/pages/animate.ts +++ b/docs/demos/src/pages/animate.ts @@ -38,6 +38,7 @@ function addCase(name: string, container: HTMLElement, cb: (stage: Stage) => voi canvas: document.getElementById('main') as HTMLCanvasElement, width: 900, height: 600, + disableDirtyBounds: false, canvasControled: false, autoRender: true }); @@ -84,17 +85,17 @@ export const page = () => { stage.defaultLayer.add(g); t1.animate() - .to({dx: delta, dy: delta}, 70, 'backOut') + .to({ dx: delta, dy: delta }, 70, 'backOut') .to({ dx: -delta / 2, dy: -delta / 2 }, 100, 'backOut') .to({ dx: 0, dy: 0 }, 30, 'backOut') .wait(2000) - .loop(Infinity) + .loop(Infinity); t2.animate() - .to({dx: -delta, dy: -delta}, 70, 'backOut') + .to({ dx: -delta, dy: -delta }, 70, 'backOut') .to({ dx: delta / 2, dy: delta / 2 }, 100, 'backOut') .to({ dx: 0, dy: 0 }, 30, 'backOut') .wait(2000) - .loop(Infinity) + .loop(Infinity); const colors = ['#f00', '#0f0', '#00f', '#ff0', '#f0f', '#0ff']; const lines = new Array(16).fill(0).map(() => { @@ -107,50 +108,50 @@ export const page = () => { { x: Math.random() * 200 + 60, y: 0 } ], stroke: colors[Math.floor(Math.random() * colors.length)], - opacity: 0, + opacity: 0 }); g.add(line); return line; }); lines.forEach(line => { - line.animate() - .wait(1000) - .to({ opacity: 1 }, 1, 'linear') - .to({ x: (line.attribute?.x ?? 0) > x ? -2000 : 2000 }, 500, 'linear') - .runCb(() => { - console.log('回调') - line.setAttributes({ - x: x + (Math.random() - 0.5) * x * 2, - y: y + (Math.random() - 0.5) * y * 0.8, - lineWidth: Math.random() * 5, - points: [ - { x: 0, y: 0 }, - { x: Math.random() * 200 + 60, y: 0 } - ], - opacity: 0, - }); - }).loop(Infinity) - }) - // setInterval(function () { - // if (Math.random() > 0.2) { - // t2.set - // t2.attr('position', [w / 2 + Math.random() * 50, h / 2]); - - // setTimeout(function () { - // t2.attr('position', [w / 2, h / 2]); - - - // for (var i = 0; i < lines.length; ++i) { - // lines[i].attr('style', { - // opacity: 0 - // }); - // } - // }, 100); - // } - // }, 500); - - }) + line + .animate() + .wait(1000) + .to({ opacity: 1 }, 1, 'linear') + .to({ x: (line.attribute?.x ?? 0) > x ? -2000 : 2000 }, 500, 'linear') + .runCb(() => { + console.log('回调'); + line.setAttributes({ + x: x + (Math.random() - 0.5) * x * 2, + y: y + (Math.random() - 0.5) * y * 0.8, + lineWidth: Math.random() * 5, + points: [ + { x: 0, y: 0 }, + { x: Math.random() * 200 + 60, y: 0 } + ], + opacity: 0 + }); + }) + .loop(Infinity); + }); + // setInterval(function () { + // if (Math.random() > 0.2) { + // t2.set + // t2.attr('position', [w / 2 + Math.random() * 50, h / 2]); + + // setTimeout(function () { + // t2.attr('position', [w / 2, h / 2]); + + // for (var i = 0; i < lines.length; ++i) { + // lines[i].attr('style', { + // opacity: 0 + // }); + // } + // }, 100); + // } + // }, 500); + }); addCase('car🚗', container, stage => { const text = createText({ text: '🚗', @@ -279,7 +280,10 @@ export const page = () => { .to({ opacity: 1 }, 1, 'linear') .play(new MotionPath(null, null, 10000, 'quadInOut', { path: cp, distance: 1 })) .reversed(true); - symbol.animate().startAt(i * 600).play(new Meteor(10, 10000, 'quadIn')); + symbol + .animate() + .startAt(i * 600) + .play(new Meteor(10, 10000, 'quadIn')); stage.defaultLayer.add(symbol); } }); @@ -296,7 +300,14 @@ export const page = () => { }); text .animate() - .play(new InputText({ text: '' }, { text: 'The more beauty you see, the more insights you gain. VisActor, presenting the beauty of data.' }, 3000, 'quadIn')); + .play( + new InputText( + { text: '' }, + { text: 'The more beauty you see, the more insights you gain. VisActor, presenting the beauty of data.' }, + 3000, + 'quadIn' + ) + ); stage.defaultLayer.add(text as any); }); @@ -316,11 +327,11 @@ export const page = () => { // addCase('area', container, stage => { // const area = createArea({ // visible: true, - // + // // stroke: '#000', // lineWidth: 4, // fillOpacity: 0.3, - // + // // fill: '#6690F2', // defined: true, // points: [ @@ -387,7 +398,7 @@ export const page = () => { text: ['这是一段很长的文字这是一段很长的文字这是一段很长的文字', 'abcdefghijklmn'], fontSize: 36, x: 200, - y: 200, + y: 200 // stroke: 'red', // fill: 'red' }); @@ -594,7 +605,7 @@ export const page = () => { // shadowBlur: 10, // shadowOffsetX: 0, // shadowOffsetY: 4, - // + // // stroke: { // x0: 0, // y0: 0.5, diff --git a/docs/demos/src/pages/area.ts b/docs/demos/src/pages/area.ts index 2b7b9927c..f6a8812c4 100644 --- a/docs/demos/src/pages/area.ts +++ b/docs/demos/src/pages/area.ts @@ -32,29 +32,29 @@ const points = [ export const page = () => { const graphics: IGraphic[] = []; - ['linear', 'step', 'stepBefore', 'stepAfter', 'basis', 'monotoneX', 'monotoneY'].forEach((type, i) => { - graphics.push( - createArea({ - points, - curveType: type as any, - x: ((i * 300) % 900) + 100, - y: Math.floor((i * 300) / 900) * 200, - stroke: ['black', false], - fill: { - gradient: 'linear', - x0: 0, - y0: 0, - x1: 1, - y1: 0, - stops: [ - { offset: 0, color: 'green' }, - { offset: 0.5, color: 'orange' }, - { offset: 1, color: 'red' } - ] - } - }) - ); - }); + // ['linear', 'step', 'stepBefore', 'stepAfter', 'basis', 'monotoneX', 'monotoneY'].forEach((type, i) => { + // graphics.push( + // createArea({ + // points, + // curveType: type as any, + // x: ((i * 300) % 900) + 100, + // y: Math.floor((i * 300) / 900) * 200, + // stroke: ['black', false], + // fill: { + // gradient: 'linear', + // x0: 0, + // y0: 0, + // x1: 1, + // y1: 0, + // stops: [ + // { offset: 0, color: 'green' }, + // { offset: 0.5, color: 'orange' }, + // { offset: 1, color: 'red' } + // ] + // } + // }) + // ); + // }); ['linear', 'step', 'stepBefore', 'stepAfter', 'basis', 'monotoneX', 'monotoneY'].forEach((type, i) => { i += 7; @@ -65,15 +65,17 @@ export const page = () => { x: ((i * 300) % 900) + 100, y: Math.floor((i * 300) / 900) * 200, segments: [ - { points: subP1, fill: colorPools[3] }, + { points: subP1, fill: colorPools[3], stroke: ['red', false], lineWidth: 10 }, { points: subP2, + stroke: ['red', false], fill: colorPools[2], texture: 'bias-rl', textureColor: 'grey' } ], - fill: true + fill: true, + stroke: true }) ); }); diff --git a/docs/demos/src/pages/text.ts b/docs/demos/src/pages/text.ts index 8a01ae9a6..00f190e97 100644 --- a/docs/demos/src/pages/text.ts +++ b/docs/demos/src/pages/text.ts @@ -56,8 +56,10 @@ export const page = () => { fill: colorPools[5], // text: ['Tffg'], text: 'Tffgggaaaa', - fontSize: 15, - lineHeight: 30, + fontSize: 20, + stroke: 'green', + lineWidth: 100, + // lineHeight: 30, // lineThrough: 1, // underline: 1, textBaseline: 'alphabetic' diff --git a/packages/vrender-components/__tests__/browser/examples/label-line.ts b/packages/vrender-components/__tests__/browser/examples/label-line.ts index ea1d04135..d0bc18644 100644 --- a/packages/vrender-components/__tests__/browser/examples/label-line.ts +++ b/packages/vrender-components/__tests__/browser/examples/label-line.ts @@ -2943,8 +2943,10 @@ function createContent(stage: Stage) { animation: { // mode: 'after' }, + dataFilter: data => { + return data.filter(item => Number(item.text) > 0.59).reverse(); + }, overlap: { - enable: false, avoidBaseMark: true, size: { width: 1036.2320098876953, diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 6bad6b968..2ffc46b10 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -14,15 +14,15 @@ import type { } from '@visactor/vrender'; import { createText, IncreaseCount, AttributeUpdateType, createPath } from '@visactor/vrender'; import type { IBoundsLike } from '@visactor/vutils'; -import { isFunction, isValidNumber, isEmpty } from '@visactor/vutils'; +import { isFunction, isValidNumber, isEmpty, isValid } from '@visactor/vutils'; import { AbstractComponent } from '../core/base'; import type { PointLocationCfg } from '../core/type'; import { labelSmartInvert } from '../util/labelSmartInvert'; import { traverseGroup } from '../util'; import { StateValue } from '../constant'; import type { Bitmap } from './overlap'; -import { bitmapTool, boundToRange, canPlace, canPlaceInside, place } from './overlap'; -import type { BaseLabelAttrs, OverlapAttrs, ILabelGraphicAttribute, ILabelAnimation, ArcLabelAttrs } from './type'; +import { bitmapTool, boundToRange, canPlace, canPlaceInside, clampText, place } from './overlap'; +import type { BaseLabelAttrs, OverlapAttrs, ILabelAnimation, ArcLabelAttrs, LabelItem, SmartInvertAttrs } from './type'; import { DefaultLabelAnimation, getAnimationAttributes } from './animate/animate'; import type { ArcInfo } from './arc'; @@ -42,11 +42,9 @@ export abstract class LabelBase extends AbstractCompon this._bmpTool = bmpTool; } - protected _relationMap: Map; + protected _graphicToText: Map; - protected _prevRelationMap: Map; - - protected _textMap: Map; + protected _idToGraphic: Map; onAfterLabelOverlap?: (bitmap: Bitmap) => void; @@ -60,12 +58,13 @@ export abstract class LabelBase extends AbstractCompon graphicBounds: IBoundsLike, position?: BaseLabelAttrs['position'], offset?: number, + graphicAttributes?: any, textData?: any, width?: number, height?: number, attribute?: any - ): Partial | undefined; + ): { x: number; y: number } | undefined; protected layoutArcLabels(position?: BaseLabelAttrs['position'], attribute?: any, currentMarks?: IGraphic[]): any { const arcs: ArcInfo[] = []; @@ -73,10 +72,38 @@ export abstract class LabelBase extends AbstractCompon } protected render() { - const currentBaseMarks = this._checkMarks(); - const labels = this.layout(currentBaseMarks); + this._prepare(); + + const { overlap, smartInvert, dataFilter, customLayoutFunc, customOverlapFunc } = this.attribute; + let data = this.attribute.data; + + if (isFunction(dataFilter)) { + data = dataFilter(data); + } + + let labels: IText[]; + + if (isFunction(customLayoutFunc)) { + labels = customLayoutFunc(data, (d: LabelItem) => this._idToGraphic.get(d.id)); + } else { + // 根据关联图元和配置的position计算标签坐标 + labels = this.layout(data); + + if (this.attribute.type !== 'arc') { + if (isFunction(customOverlapFunc)) { + labels = customOverlapFunc(labels as Text[], (d: LabelItem) => this._idToGraphic.get(d.id)); + } else { + // 防重叠逻辑 + if (overlap !== false) { + labels = this._overlapping(labels); + } + } + } + } - this._smartInvert(labels); + if (smartInvert !== false) { + this._smartInvert(labels); + } this._renderLabels(labels); } @@ -161,14 +188,14 @@ export abstract class LabelBase extends AbstractCompon } }; - private _createLabelText(attributes: ILabelGraphicAttribute) { + private _createLabelText(attributes: LabelItem) { const text = createText(attributes); this._bindEvent(text); this._setStates(text); return text; } - private _checkMarks() { + private _prepare() { const baseMarks = this.getBaseMarks(); const currentBaseMarks: IGraphic[] = []; baseMarks.forEach(mark => { @@ -176,52 +203,57 @@ export abstract class LabelBase extends AbstractCompon currentBaseMarks.push(mark); } }); - this._prevRelationMap = new Map(this._relationMap); - this._relationMap?.clear(); - return currentBaseMarks; - } - protected layout(currentMarks?: IGraphic[]): ILabelGraphicAttribute[] { - const { textStyle, position, offset } = this.attribute as BaseLabelAttrs; - let { data } = this.attribute as BaseLabelAttrs; - if (isFunction(data)) { - data = data({}); - } - if (!data || data.length === 0) { - return []; + this._idToGraphic?.clear(); + this._baseMarks = currentBaseMarks; + + if (!currentBaseMarks || currentBaseMarks.length === 0) { + return; } - let labels: ILabelGraphicAttribute[] = []; + const { data } = this.attribute; - if (isFunction(this.attribute.sort) && currentMarks && currentMarks.length) { - currentMarks = currentMarks.sort(this.attribute.sort); + if (!data || data.length === 0) { + return; } - if (!this._relationMap) { - this._relationMap = new Map(); + if (!this._idToGraphic) { + this._idToGraphic = new Map(); } + // generate id mapping before data filter + for (let i = 0; i < currentBaseMarks.length; i++) { + const textData = data[i]; + const baseMark = currentBaseMarks[i] as IGraphic; + if (textData && baseMark) { + if (!isValid(textData.id)) { + textData.id = `vrender-component-${this.name}-${i}`; + } + this._idToGraphic.set(textData.id, baseMark); + } + } + } - const { width, height } = this.attribute as ArcLabelAttrs; + protected layout(data: LabelItem[] = []): IText[] { + const { textStyle = {}, position, offset } = this.attribute; + const labels = []; - // 默认根据 index 顺序排序 for (let i = 0; i < data.length; i++) { const textData = data[i]; - const baseMark = currentMarks?.[i] as IGraphic; + const baseMark = this._idToGraphic.get(textData.id); + const labelAttribute = { ...textStyle, - ...textData, - _relatedIndex: i + ...textData }; - this._relationMap.set(i, baseMark); + const text = this._createLabelText(labelAttribute); + const textBounds = this.getGraphicBounds(text); + const graphicBounds = this.getGraphicBounds(baseMark, { x: textData.x as number, y: textData.y as number }); - if (textData) { - const text = createText(labelAttribute); - text.update(); - const textBounds = this.getGraphicBounds(text); - const graphicBounds = this.getGraphicBounds(baseMark, { x: textData.x as number, y: textData.y as number }); + if (this.attribute.type === 'arc') { const graphicAttributes = baseMark.attribute; + const { width, height } = this.attribute as ArcLabelAttrs; - const textAttributes = this.labeling( + const arcRightLeft = this.labeling( textBounds, graphicBounds, isFunction(position) ? position(textData) : position, @@ -232,53 +264,60 @@ export abstract class LabelBase extends AbstractCompon height, this.attribute ); - - if (!textAttributes) { + labels.push(text); + } else { + const textLocation = this.labeling( + textBounds, + graphicBounds, + isFunction(position) ? position(textData) : position, + offset + ); + if (!textLocation) { continue; } + labelAttribute.x = textLocation.x; + labelAttribute.y = textLocation.y; - if (this.attribute.type === 'arc') { - labels.push(labelAttribute); - } else { - labelAttribute.x = textAttributes.x; - labelAttribute.y = textAttributes.y; - labels.push(labelAttribute); - } + text.setAttributes(textLocation); + labels.push(text); } } - this._baseMarks = currentMarks as IGraphic[]; if (this.attribute.type === 'arc') { - const arcs: ArcInfo[] = this.layoutArcLabels(position, this.attribute, currentMarks); + const arcs: ArcInfo[] = this.layoutArcLabels(position, this.attribute, Array.from(this._idToGraphic.values())); for (let i = 0; i < data.length; i++) { const textData = data[i]; const basedArc = arcs.find(arc => arc.labelText === textData.text); - labels[i].x = basedArc.labelPosition.x; - labels[i].y = basedArc.labelPosition.y; - labels[i].textAlign = basedArc.textAlign; - labels[i].textBaseline = basedArc.textBaseline; - labels[i].angle = basedArc.angle; + const labelAttribute = { + x: basedArc.labelPosition.x, + y: basedArc.labelPosition.y, + textAlign: basedArc.textAlign, + textBaseline: basedArc.textBaseline, + angle: basedArc.angle + }; + + labels[i].setAttributes(labelAttribute); + // 用于做labelLine labels[i].pointA = basedArc.pointA; labels[i].pointB = basedArc.pointB; labels[i].pointC = basedArc.pointC; } - } else { - if (this.attribute.overlap !== false) { - labels = this.overlapping(labels, this.attribute.overlap as OverlapAttrs); - } } return labels; } - protected overlapping(labels: ILabelGraphicAttribute[], option: OverlapAttrs = {}) { + protected _overlapping(labels: IText[]) { if (labels.length === 0) { return []; } - const result: ILabelGraphicAttribute[] = []; + const option = this.attribute.overlap as OverlapAttrs; + + const result: IText[] = []; const baseMarkGroup = this.getBaseMarkGroup(); + const size = option.size ?? { width: baseMarkGroup?.AABBBounds.width() ?? 0, height: baseMarkGroup?.AABBBounds.height() ?? 0 @@ -303,8 +342,8 @@ export abstract class LabelBase extends AbstractCompon if (labels[i].visible === false) { continue; } - const text = createText(labels[i]) as Text; - const baseMark = this._baseMarks?.[i]; + const text = labels[i] as IText; + const baseMark = this._idToGraphic.get((text.attribute as LabelItem).id); text.update(); // 默认位置可以放置 @@ -312,13 +351,31 @@ export abstract class LabelBase extends AbstractCompon // 如果配置了限制在图形内部,需要提前判断; if (!checkBounds) { bitmap.setRange(boundToRange(bmpTool, text.AABBBounds, true)); - result.push({ ...text.attribute }); + result.push(text); continue; } if (checkBounds && baseMark?.AABBBounds && canPlaceInside(text.AABBBounds, baseMark?.AABBBounds)) { bitmap.setRange(boundToRange(bmpTool, text.AABBBounds, true)); - result.push({ ...text.attribute }); + result.push(text); + continue; + } + } + + // 尝试向内挤压 + if (clampForce) { + const { dx = 0, dy = 0 } = clampText(text, bmpTool.width, bmpTool.height); + if ( + !(dx === 0 && dy === 0) && + canPlace(bmpTool, bitmap, { + x1: text.AABBBounds.x1 + dx, + x2: text.AABBBounds.x2 + dx, + y1: text.AABBBounds.y1 + dy, + y2: text.AABBBounds.y2 + dy + }) + ) { + text.setAttributes({ x: text.attribute.x + dx, y: text.attribute.y + dy }); + result.push(text); continue; } } @@ -331,21 +388,18 @@ export abstract class LabelBase extends AbstractCompon bitmap, strategy[j], this.attribute, - text, + text as Text, this.getGraphicBounds(baseMark, labels[i]), this.labeling ); if (hasPlace !== false) { - result.push({ - ...text.attribute, - x: hasPlace.x, - y: hasPlace.y - }); + text.setAttributes({ x: hasPlace.x, y: hasPlace.y }); + result.push(text); break; } } - !hasPlace && !hideOnHit && result.push({ ...text.attribute }); + !hasPlace && !hideOnHit && result.push(text); } if (isFunction(this.onAfterLabelOverlap)) { @@ -390,7 +444,7 @@ export abstract class LabelBase extends AbstractCompon ); } - protected _renderLabels(labels: ILabelGraphicAttribute[]) { + protected _renderLabels(labels: IText[]) { const animationConfig = (this.attribute.animation ?? {}) as ILabelAnimation; const disableAnimation = this._enableAnimation === false || (animationConfig as unknown as boolean) === false; const mode = animationConfig.mode ?? DefaultLabelAnimation.mode; @@ -399,28 +453,27 @@ export abstract class LabelBase extends AbstractCompon const delay = animationConfig.delay ?? 0; const currentTextMap = new Map(); - const prevTextMap = this._textMap || new Map(); + const prevTextMap = this._graphicToText || new Map(); const texts = [] as IText[]; const labelLines = [] as IPath[]; - labels.forEach((label, index) => { - const text = this._createLabelText(label); + labels.forEach((text, index) => { + // const text = this._createLabelText(label); let labelLine: IPath; if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { labelLine = createPath({ - visible: label?.visible ?? true, - stroke: label?.line?.stroke ?? label?.fill, + visible: text.attribute?.visible ?? true, + stroke: text.attribute?.line?.stroke ?? text.attribute?.fill, lineWidth: 1, path: - `M${Math.round(label.pointA.x)},${Math.round(label.pointA.y)}` + - ` L${Math.round(label.pointB.x)},${Math.round(label.pointB.y)}` + - ` L${Math.round(label.pointC.x)},${Math.round(label.pointC.y)}` + `M${Math.round(text.pointA.x)},${Math.round(text.pointA.y)}` + + ` L${Math.round(text.pointB.x)},${Math.round(text.pointB.y)}` + + ` L${Math.round(text.pointC.x)},${Math.round(text.pointC.y)}` }) as Path; } - const relatedGraphic = this._relationMap.get(label._relatedIndex); + const relatedGraphic = this._idToGraphic.get((text.attribute as LabelItem).id); const state = prevTextMap?.get(relatedGraphic) ? 'update' : 'enter'; - if (state === 'enter') { texts.push(text); if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { @@ -428,7 +481,7 @@ export abstract class LabelBase extends AbstractCompon } currentTextMap.set(relatedGraphic, text); if (!disableAnimation && relatedGraphic) { - const { from, to } = getAnimationAttributes(label as ITextGraphicAttribute, 'fadeIn'); + const { from, to } = getAnimationAttributes(text.attribute, 'fadeIn'); this.add(text); if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { this.add(labelLine); @@ -489,7 +542,7 @@ export abstract class LabelBase extends AbstractCompon } }); - this._textMap = currentTextMap; + this._graphicToText = currentTextMap; } protected _afterRelatedGraphicAttributeUpdate( @@ -556,20 +609,18 @@ export abstract class LabelBase extends AbstractCompon return listener; } - protected _smartInvert(labels: ILabelGraphicAttribute[]) { - if (this.attribute.smartInvert === false) { - return; - } + protected _smartInvert(labels: IText[]) { + const option = (this.attribute.smartInvert || {}) as SmartInvertAttrs; + const { textType, contrastRatiosThreshold, alternativeColors } = option; + for (let i = 0; i < labels.length; i++) { - const label = labels?.[i] as ILabelGraphicAttribute; + const label = labels[i]; if (!label) { continue; } - let isInside = canPlaceInside( - createText(label as ITextGraphicAttribute).AABBBounds, - this._relationMap.get(label._relatedIndex)?.AABBBounds - ); + const baseMark = this._idToGraphic.get((label.attribute as LabelItem).id); + let isInside = canPlaceInside(label.AABBBounds, baseMark?.AABBBounds); if (this.attribute.type === 'arc') { if (this.attribute.position === 'inside') { @@ -578,6 +629,7 @@ export abstract class LabelBase extends AbstractCompon isInside = false; } } + /** * stroke 的处理逻辑 * 1. 当文本在图元内部时,有两种情况: @@ -587,50 +639,42 @@ export abstract class LabelBase extends AbstractCompon * - a. 未设置stroke:此时设置strokeColor为backgroundColor。labelFill为前景色,labelStroke填充色为背景色。避免文字一半在图元内部,一半在图元外部时,在图元外部文字不可见。 * - b. 设置了stroke:保持strokeColor。labelFill为前景色,labelStroke填充色为背景色。 */ - if (label.stroke && label.lineWidth > 0) { + if (label.attribute.stroke && label.attribute.lineWidth > 0) { /** * 1-b, 2-b * 若label存在stroke,label填充色为前景色,label描边色为背景色 * WCAG 2 字母周围的文本发光/光晕可用作背景颜色 */ - label.fill = labelSmartInvert( - label.fill as IColor, - label.stroke as IColor, - this.attribute.smartInvert?.textType, - this.attribute.smartInvert?.contrastRatiosThreshold, - this.attribute.smartInvert?.alternativeColors - ); + label.setAttributes({ + fill: labelSmartInvert( + label.attribute.fill as IColor, + label.attribute.stroke as IColor, + textType, + contrastRatiosThreshold, + alternativeColors + ) + }); } else if (isInside) { /** * 1-a * label在图元内部时,label填充色为前景色,baseMark填充色为背景色 */ - const baseMark = this._relationMap.get(label._relatedIndex); const backgroundColor = baseMark.attribute.fill as IColor; - const foregroundColor = label.fill as IColor; - label.fill = labelSmartInvert( - foregroundColor, - backgroundColor, - this.attribute.smartInvert?.textType, - this.attribute.smartInvert?.contrastRatiosThreshold, - this.attribute.smartInvert?.alternativeColors - ); - } else if (label.lineWidth > 0) { + const foregroundColor = label.attribute.fill as IColor; + label.setAttributes({ + fill: labelSmartInvert(foregroundColor, backgroundColor, textType, contrastRatiosThreshold, alternativeColors) + }); + } else if (label.attribute.lineWidth > 0) { /** * 2-a * 当文本在图元外部时,设置strokeColor为backgroundColor。labelFill为前景色,labelStroke填充色为背景色。 */ - const baseMark = this._relationMap.get(label._relatedIndex); - label.stroke = baseMark.attribute.fill; - const backgroundColor = label.stroke as IColor; - const foregroundColor = label.fill as IColor; - label.fill = labelSmartInvert( - foregroundColor, - backgroundColor, - this.attribute.smartInvert?.textType, - this.attribute.smartInvert?.contrastRatiosThreshold, - this.attribute.smartInvert?.alternativeColors - ); + const backgroundColor = label.attribute.stroke as IColor; + const foregroundColor = label.attribute.fill as IColor; + label.setAttributes({ + stroke: baseMark.attribute.fill, + fill: labelSmartInvert(foregroundColor, backgroundColor, textType, contrastRatiosThreshold, alternativeColors) + }); } } } diff --git a/packages/vrender-components/src/label/overlap/place.ts b/packages/vrender-components/src/label/overlap/place.ts index a8f5a7ee0..c7327e39d 100644 --- a/packages/vrender-components/src/label/overlap/place.ts +++ b/packages/vrender-components/src/label/overlap/place.ts @@ -1,10 +1,12 @@ -import { Text } from '@visactor/vrender'; -import { IAABBBounds, IBoundsLike, isFunction } from '@visactor/vutils'; -import { PointLocationCfg } from '../../core/type'; +import type { IText, Text } from '@visactor/vrender'; +import type { IAABBBounds, IBoundsLike } from '@visactor/vutils'; +import { isFunction } from '@visactor/vutils'; +import type { PointLocationCfg } from '../../core/type'; import type { LabelBase } from '../base'; import type { BaseLabelAttrs, OverlapAttrs, Strategy } from '../type'; import type { Bitmap } from './bitmap'; -import { BitmapTool, boundToRange } from './scaler'; +import type { BitmapTool } from './scaler'; +import { boundToRange } from './scaler'; /** * 防重叠逻辑参考 https://github.com/vega/vega/ @@ -139,3 +141,31 @@ export function defaultLabelPosition(type?: string) { return DefaultPositions; } } + +export function clampText(text: IText, width: number, height: number) { + const { x1, x2, y1, y2 } = text.AABBBounds; + const minX = Math.min(x1, x2); + const maxX = Math.max(x1, x2); + + const minY = Math.min(y1, y2); + const maxY = Math.max(y1, y2); + + let dx = 0; + let dy = 0; + + // x 方向 + if (minX < 0 && maxX - minX <= width) { + dx = -minX; + } else if (maxX > width && minX - (maxX - width) >= 0) { + dx = width - maxX; + } + + // y 方向 + if (minY < 0 && maxY - minY <= height) { + dy = -minY; + } else if (maxY > height && minY - (maxY - height) >= 0) { + dy = height - maxY; + } + + return { dx, dy }; +} diff --git a/packages/vrender-components/src/label/rect.ts b/packages/vrender-components/src/label/rect.ts index c05ff43e7..4f926a592 100644 --- a/packages/vrender-components/src/label/rect.ts +++ b/packages/vrender-components/src/label/rect.ts @@ -1,4 +1,5 @@ -import { merge, IBoundsLike } from '@visactor/vutils'; +import type { IBoundsLike } from '@visactor/vutils'; +import { merge } from '@visactor/vutils'; import type { ITextGraphicAttribute } from '@visactor/vrender'; import type { RectLabelAttrs } from './type'; import { LabelBase } from './base'; @@ -23,12 +24,7 @@ export class RectLabel extends LabelBase { super(merge({}, RectLabel.defaultAttributes, attributes)); } - protected labeling( - textBounds: IBoundsLike, - graphicBounds: IBoundsLike, - position = 'top', - offset = 0 - ): Partial | undefined { + protected labeling(textBounds: IBoundsLike, graphicBounds: IBoundsLike, position = 'top', offset = 0) { if (!textBounds || !graphicBounds) { return; } diff --git a/packages/vrender-components/src/label/symbol.ts b/packages/vrender-components/src/label/symbol.ts index 2c199aaf2..c3f915f0d 100644 --- a/packages/vrender-components/src/label/symbol.ts +++ b/packages/vrender-components/src/label/symbol.ts @@ -1,4 +1,5 @@ -import { merge, IBoundsLike } from '@visactor/vutils'; +import type { IBoundsLike } from '@visactor/vutils'; +import { merge } from '@visactor/vutils'; import type { ITextGraphicAttribute } from '@visactor/vrender'; import type { SymbolLabelAttrs } from './type'; import { LabelBase } from './base'; @@ -23,12 +24,7 @@ export class SymbolLabel extends LabelBase { super(merge({}, SymbolLabel.defaultAttributes, attributes)); } - protected labeling( - textBounds: IBoundsLike, - graphicBounds: IBoundsLike, - position = 'top', - offset = 0 - ): Partial | undefined { + protected labeling(textBounds: IBoundsLike, graphicBounds: IBoundsLike, position = 'top', offset = 0) { if (!textBounds) { return; } diff --git a/packages/vrender-components/src/label/type.ts b/packages/vrender-components/src/label/type.ts index c1a64ea58..3d67cf72e 100644 --- a/packages/vrender-components/src/label/type.ts +++ b/packages/vrender-components/src/label/type.ts @@ -1,5 +1,4 @@ -import type { EasingType, IGroupGraphicAttribute, ITextGraphicAttribute } from '@visactor/vrender'; -import type { Bounds } from '@visactor/vutils'; +import type { EasingType, IGraphic, IGroupGraphicAttribute, ITextGraphicAttribute, Text } from '@visactor/vrender'; export type LabelItemStateStyle = { hover?: T; @@ -8,8 +7,20 @@ export type LabelItemStateStyle = { selected_reverse?: T; }; +export type LabelItem = { + // 用于动画 + id?: string; + // 原始数据 + data?: any; + [key: string]: any; +} & ITextGraphicAttribute; + export interface BaseLabelAttrs extends IGroupGraphicAttribute { type: string; + /** + * 图元 group 名称 + */ + baseMarkGroupName: string; /** * 是否开启选中交互 * @default false @@ -23,15 +34,7 @@ export interface BaseLabelAttrs extends IGroupGraphicAttribute { /** * 标签数据 */ - data: Functional<(ITextGraphicAttribute & Record)[]>; - - sort?: (graphicA: any, graphicB: any) => number; - - /** - * 图元 group 名称 - * 如果不配置,需要在 data 里指定标签定位 x/y - */ - baseMarkGroupName?: string; + data: LabelItem[]; /** 文本样式,优先级低于 data */ textStyle?: Partial; @@ -55,6 +58,19 @@ export interface BaseLabelAttrs extends IGroupGraphicAttribute { /** 动画配置 */ animation?: ILabelAnimation | false; + + // 排序 or 删减 + dataFilter?: (data: LabelItem[]) => LabelItem[]; + + /** 自定义布局函数 + * @description 当配置了 customLayoutFunc 后,默认布局和防重叠逻辑将不再生效。(overlap/position/offset不生效) + */ + customLayoutFunc?: (data: LabelItem[], getRelatedGraphic: (data: LabelItem) => IGraphic) => Text[]; + + /** 自定义标签躲避函数 + * @description 当配置了 customOverlapFunc 后,会根据 position 和 offset 进行初始布局。配置的防重叠逻辑(overlap)不生效。 + */ + customOverlapFunc?: (label: Text[], getRelatedGraphic: (data: LabelItem) => IGraphic) => Text[]; } export interface OverlapAttrs { @@ -293,15 +309,6 @@ export interface DataLabelAttrs extends IGroupGraphicAttribute { export type Functional = T | ((data: any) => T); -export interface ILabelGraphicAttribute extends ITextGraphicAttribute { - _relatedIndex?: number; - - line?: any; - pointC?: IPoint; - pointB?: IPoint; - pointA?: IPoint; -} - export interface ILabelAnimation { mode?: 'same-time' | 'after' | 'after-all'; duration?: number; diff --git a/packages/vrender-components/src/poptip/contribution.ts b/packages/vrender-components/src/poptip/contribution.ts index 2231afa37..2234551ce 100644 --- a/packages/vrender-components/src/poptip/contribution.ts +++ b/packages/vrender-components/src/poptip/contribution.ts @@ -60,6 +60,7 @@ export class PopTipRenderContribution implements IInteractiveSubRenderContributi y: matrix.f }); // 添加到交互层中 + drawContext.stage.tryInitInteractiveLayer(); const interactiveLayer = drawContext.stage.getLayer('_builtin_interactive'); if (interactiveLayer) { interactiveLayer.add(this.poptipComponent); diff --git a/packages/vrender/src/core/stage.ts b/packages/vrender/src/core/stage.ts index e16e22dab..0db14affd 100644 --- a/packages/vrender/src/core/stage.ts +++ b/packages/vrender/src/core/stage.ts @@ -169,6 +169,7 @@ export class Stage extends Group implements IStage { protected lastRenderparams?: Partial; protected interactiveLayer?: ILayer; + protected supportInteractiveLayer: boolean; /** * 所有属性都具有默认值。 @@ -268,9 +269,7 @@ export class Stage extends Group implements IStage { this._beforeRender = params.beforeRender; this._afterRender = params.afterRender; this.ticker = params.ticker || defaultTicker; - if (params.interactiveLayer !== false) { - this.initInteractiveLayer(); - } + this.supportInteractiveLayer = params.interactiveLayer !== false; } get3dOptions(options: IOption3D) { @@ -489,10 +488,13 @@ export class Stage extends Group implements IStage { removeLayer(ILayerId: number): ILayer | false { return this.removeChild(this.findChildByUid(ILayerId) as IGraphic) as ILayer; } - protected initInteractiveLayer() { + tryInitInteractiveLayer() { // TODO:顺序可能会存在问题 - this.interactiveLayer = this.createLayer(); - this.interactiveLayer.name = '_builtin_interactive'; + // 支持交互层,且没有创建过,那就创建 + if (this.supportInteractiveLayer && !this.interactiveLayer) { + this.interactiveLayer = this.createLayer(); + this.interactiveLayer.name = '_builtin_interactive'; + } // this.interactiveLayer.afterDraw(l => { // l.removeAllChild(); // }); diff --git a/packages/vrender/src/graphic/graphic-service/graphic-service.ts b/packages/vrender/src/graphic/graphic-service/graphic-service.ts index 66f933e20..f67c81e58 100644 --- a/packages/vrender/src/graphic/graphic-service/graphic-service.ts +++ b/packages/vrender/src/graphic/graphic-service/graphic-service.ts @@ -891,6 +891,20 @@ export class DefaultGraphicService implements IGraphicService { tb1.setValue(aabbBounds.x1, aabbBounds.y1, aabbBounds.x2, aabbBounds.y2); tb2.setValue(aabbBounds.x1, aabbBounds.y1, aabbBounds.x2, aabbBounds.y2); + const { + scaleX = textTheme.scaleX, + scaleY = textTheme.scaleY, + shadowBlur = textTheme.shadowBlur, + strokeBoundsBuffer = textTheme.strokeBoundsBuffer + } = attribute; + if (shadowBlur) { + const shadowBlurHalfWidth = shadowBlur / Math.abs(scaleX + scaleY); + boundStroke(tb1, shadowBlurHalfWidth, true, strokeBoundsBuffer); + aabbBounds.union(tb1); + } + // 合并shadowRoot的bounds + this.combindShadowAABBBounds(aabbBounds, graphic); + transformBoundsWithMatrix(aabbBounds, aabbBounds, graphic.transMatrix); return aabbBounds; } diff --git a/packages/vrender/src/interface/stage.ts b/packages/vrender/src/interface/stage.ts index 37e2c3512..705a2dc3a 100644 --- a/packages/vrender/src/interface/stage.ts +++ b/packages/vrender/src/interface/stage.ts @@ -120,6 +120,7 @@ export interface IStage extends INode { render: (layers?: ILayer[], params?: Partial) => void; renderNextFrame: (layers?: ILayer[]) => void; + tryInitInteractiveLayer: () => void; // 画布操作 resize: (w: number, h: number, rerender?: boolean) => void; diff --git a/packages/vrender/src/render/contributions/render/area-render.ts b/packages/vrender/src/render/contributions/render/area-render.ts index eb645d02b..c23ed3cd3 100644 --- a/packages/vrender/src/render/contributions/render/area-render.ts +++ b/packages/vrender/src/render/contributions/render/area-render.ts @@ -407,24 +407,6 @@ export class DefaultCanvasAreaRender implements IGraphicRender { } } - if (stroke !== false) { - if (strokeCb) { - strokeCb(context, attribute, defaultAttribute); - } else { - const { stroke } = attribute; - if (isArray(stroke) && stroke[0] && stroke[1] === false) { - context.beginPath(); - drawSegments(context.camera ? context : context.nativeContext, cache.top, clipRange, 'auto', { - offsetX, - offsetY, - offsetZ - }); - } - context.setStrokeStyle(area, attribute, originX - offsetX, originY - offsetY, defaultAttribute); - context.stroke(); - } - } - if (!this._areaRenderContribitions) { this._areaRenderContribitions = this.areaRenderContribitions.getContributions() || []; } @@ -448,6 +430,30 @@ export class DefaultCanvasAreaRender implements IGraphicRender { } }); + if (stroke !== false) { + if (strokeCb) { + strokeCb(context, attribute, defaultAttribute); + } else { + const { stroke } = attribute; + if (isArray(stroke) && (stroke[0] || stroke[2]) && stroke[1] === false) { + context.beginPath(); + drawSegments( + context.camera ? context : context.nativeContext, + stroke[0] ? cache.top : cache.bottom, + clipRange, + 'auto', + { + offsetX, + offsetY, + offsetZ + } + ); + } + context.setStrokeStyle(area, attribute, originX - offsetX, originY - offsetY, defaultAttribute); + context.stroke(); + } + } + return ret; } } From bc4e9c7bf1774ecbf7f2d26827805d8ea33595ad Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Fri, 28 Jul 2023 14:48:15 +0800 Subject: [PATCH 10/29] feat: change textAlign & textBaseline type --- packages/vrender-components/src/label/arc.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts index 9e37b6a7b..4af1d627b 100644 --- a/packages/vrender-components/src/label/arc.ts +++ b/packages/vrender-components/src/label/arc.ts @@ -3,7 +3,7 @@ import { merge } from '@visactor/vutils'; import { LabelBase } from './base'; import type { ArcLabelAttrs, IPoint, Quadrant, TextAlign } from './type'; import type { BaseLabelAttrs } from './type'; -import type { ITextGraphicAttribute } from '@visactor/vrender'; +import type { ITextGraphicAttribute, TextAlignType, TextBaselineType } from '@visactor/vrender'; import { isValidNumber, isNil, isLess, isGreater, isNumberClose as isClose } from '@visactor/vutils'; import { circlePoint, @@ -45,8 +45,8 @@ export class ArcInfo { radian: number; middleAngle: number; k: number; - textAlign: string; - textBaseline: string; + textAlign: TextAlignType; + textBaseline: TextBaselineType; angle: number; constructor( From 1490cc0de3303bc14b386b50a398061c1656e537 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Fri, 28 Jul 2023 14:57:31 +0800 Subject: [PATCH 11/29] feat: change currentMarks type --- packages/vrender-components/src/label/arc.ts | 18 +++++++++--------- packages/vrender-components/src/label/base.ts | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts index 4af1d627b..6f8187020 100644 --- a/packages/vrender-components/src/label/arc.ts +++ b/packages/vrender-components/src/label/arc.ts @@ -140,7 +140,7 @@ export class ArcLabel extends LabelBase { width: number, height: number, attribute: any - ): Partial | undefined { + ): { x: number; y: number } | undefined { if (!textBounds || !graphicBounds) { return; } @@ -200,7 +200,7 @@ export class ArcLabel extends LabelBase { this._arcLeft.set(arc.refDatum, arc); } - return { arcRight: this._arcRight, arcLeft: this._arcLeft }; + // return { arcRight: this._arcRight, arcLeft: this._arcLeft }; } // layoutLabels : 执行内部/外部标签的布局计算 @@ -221,7 +221,7 @@ export class ArcLabel extends LabelBase { /** * 布局内部标签 */ - private _layoutInsideLabels(arcs: ArcInfo[], attribute: any, currentMarks: IGraphic[]) { + private _layoutInsideLabels(arcs: ArcInfo[], attribute: any, currentMarks: any[]) { const center = attribute.center ?? { x: 0, y: 0 }; const innerRadiusRatio = this.computeLayoutOuterRadius( currentMarks[0].attribute.innerRadius, @@ -279,7 +279,7 @@ export class ArcLabel extends LabelBase { /** * 布局外部标签 */ - private _layoutOutsideLabels(arcs: ArcInfo[], attribute: any, currentMarks: IGraphic[]) { + private _layoutOutsideLabels(arcs: ArcInfo[], attribute: any, currentMarks: any[]) { // const height = Math.min(attribute.center.x, attribute.center.y) * 2; const height = attribute.center.y * 2; const line2MinLength = attribute.line.line2MinLength as number; @@ -355,7 +355,7 @@ export class ArcLabel extends LabelBase { /** * 计算 pointC 以及 label limit 与 position */ - private _computeX(arc: ArcInfo, attribute: any, currentMarks: IGraphic[]) { + private _computeX(arc: ArcInfo, attribute: any, currentMarks: any[]) { const center = attribute.center ?? { x: 0, y: 0 }; const plotLayout = { width: attribute.center.x * 2, height: attribute.center.y * 2 }; const radiusRatio = this.computeLayoutOuterRadius( @@ -452,7 +452,7 @@ export class ArcLabel extends LabelBase { /** * 调整标签位置的 Y 值 */ - private _adjustY(arcs: ArcInfo[], maxLabels: number, attribute: any, currentMarks: any) { + private _adjustY(arcs: ArcInfo[], maxLabels: number, attribute: any, currentMarks: any[]) { const plotRect = { width: attribute.center.x * 2, height: attribute.center.y * 2 }; const labelLayout = attribute.layout; if (labelLayout.strategy === 'vertical') { @@ -615,7 +615,7 @@ export class ArcLabel extends LabelBase { /** * 计算 pointB,其 y 值在 adjustY 中确定,也即是 label 的 y 值 */ - private _computePointB(arc: ArcInfo, r: number, attribute: any, currentMarks: any) { + private _computePointB(arc: ArcInfo, r: number, attribute: any, currentMarks: any[]) { const labelConfig = attribute; const radiusRatio = this.computeLayoutOuterRadius( currentMarks[0].attribute.outerRadius, @@ -664,7 +664,7 @@ export class ArcLabel extends LabelBase { /** * 计算圆弧切线所限制的标签 Y 值范围 */ - private _computeYRange(arc: ArcInfo, attribute: any, currentMarks: any) { + private _computeYRange(arc: ArcInfo, attribute: any, currentMarks: any[]) { const plotRect = { width: attribute.center.x * 2, height: attribute.center.y * 2 }; const radiusRatio = this.computeLayoutOuterRadius( @@ -737,7 +737,7 @@ export class ArcLabel extends LabelBase { /** * 计算标签布局圆弧半径,即 pointB 所落在的圆弧 */ - private _computeLayoutRadius(halfYLength: number, attribute: any, currentMarks: any) { + private _computeLayoutRadius(halfYLength: number, attribute: any, currentMarks: any[]) { const labelConfig = attribute; const layoutArcGap = labelConfig.layoutArcGap as number; const line1MinLength = labelConfig.line.line1MinLength as number; diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 2ffc46b10..c088544ed 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -253,7 +253,7 @@ export abstract class LabelBase extends AbstractCompon const graphicAttributes = baseMark.attribute; const { width, height } = this.attribute as ArcLabelAttrs; - const arcRightLeft = this.labeling( + this.labeling( textBounds, graphicBounds, isFunction(position) ? position(textData) : position, From fb37e1b4e5cb00edf3fa30cd155e192dc28f5538 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Fri, 28 Jul 2023 15:07:15 +0800 Subject: [PATCH 12/29] feat: add type definition for text.attribute --- packages/vrender-components/src/label/base.ts | 14 ++------------ packages/vrender-components/src/label/type.ts | 1 + 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index c088544ed..02c013866 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -1,17 +1,7 @@ /** * @description Label 基类 */ -import type { - IGroup, - Text, - IGraphic, - IText, - FederatedPointerEvent, - IColor, - IPath, - Path, - ITextGraphicAttribute -} from '@visactor/vrender'; +import type { IGroup, Text, IGraphic, IText, FederatedPointerEvent, IColor, IPath, Path } from '@visactor/vrender'; import { createText, IncreaseCount, AttributeUpdateType, createPath } from '@visactor/vrender'; import type { IBoundsLike } from '@visactor/vutils'; import { isFunction, isValidNumber, isEmpty, isValid } from '@visactor/vutils'; @@ -464,7 +454,7 @@ export abstract class LabelBase extends AbstractCompon if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { labelLine = createPath({ visible: text.attribute?.visible ?? true, - stroke: text.attribute?.line?.stroke ?? text.attribute?.fill, + stroke: (text.attribute as ArcLabelAttrs)?.line?.stroke ?? text.attribute?.fill, lineWidth: 1, path: `M${Math.round(text.pointA.x)},${Math.round(text.pointA.y)}` + diff --git a/packages/vrender-components/src/label/type.ts b/packages/vrender-components/src/label/type.ts index 3d67cf72e..8b302ba05 100644 --- a/packages/vrender-components/src/label/type.ts +++ b/packages/vrender-components/src/label/type.ts @@ -258,6 +258,7 @@ export interface ArcLabelAttrs extends BaseLabelAttrs { } export interface IArcLabelLineSpec { + stroke: string; /** * 是否显示引导线 * @default true From 6ce808a42f82ad798feff04da5a7993f9714a29c Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Fri, 28 Jul 2023 15:34:20 +0800 Subject: [PATCH 13/29] feat: change stroke as optional attribute --- packages/vrender-components/src/label/type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-components/src/label/type.ts b/packages/vrender-components/src/label/type.ts index 8b302ba05..b5072f6d9 100644 --- a/packages/vrender-components/src/label/type.ts +++ b/packages/vrender-components/src/label/type.ts @@ -258,7 +258,7 @@ export interface ArcLabelAttrs extends BaseLabelAttrs { } export interface IArcLabelLineSpec { - stroke: string; + stroke?: string; /** * 是否显示引导线 * @default true From 39328732a0bdda37533915c0dfe390b715f70654 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Fri, 4 Aug 2023 17:06:27 +0800 Subject: [PATCH 14/29] feat: add calculation items such as middleArc, adjust the vertical position of the label --- .../__tests__/browser/examples/label-arc.ts | 380 ++++++++++-------- packages/vrender-components/src/label/arc.ts | 56 +-- packages/vrender-components/src/label/base.ts | 1 + .../vrender-components/src/label/constant.ts | 19 - packages/vrender-components/src/label/util.ts | 32 ++ 5 files changed, 276 insertions(+), 212 deletions(-) delete mode 100644 packages/vrender-components/src/label/constant.ts diff --git a/packages/vrender-components/__tests__/browser/examples/label-arc.ts b/packages/vrender-components/__tests__/browser/examples/label-arc.ts index 138612cac..4b57e4e41 100644 --- a/packages/vrender-components/__tests__/browser/examples/label-arc.ts +++ b/packages/vrender-components/__tests__/browser/examples/label-arc.ts @@ -17,173 +17,235 @@ const pieGenerator = () => { children: [ { attribute: { - visible: true, - lineWidth: 0, - fillOpacity: 1, - padAngle: 0, - x: 388, - y: 238, - startAngle: -1.5707963267948966, - endAngle: 1.357168026350791, - outerRadius: 190.4, + fill: '#1f77b4', + x: 100, + y: 100, + startAngle: 0, + endAngle: 1.0927278795094932, innerRadius: 0, - cornerRadius: 0, - fill: '#1664FF', - stroke: '#1664FF', - pickable: true + outerRadius: 80, + fillOpacity: 1 }, - _uid: 15, + _uid: 52, type: 'arc', children: [] }, { attribute: { - visible: true, - lineWidth: 0, - fillOpacity: 1, - padAngle: 0, - x: 388, - y: 238, - startAngle: 1.357168026350791, - endAngle: 3.0988669935009723, - outerRadius: 190.4, + fill: '#aec7e8', + x: 100, + y: 100, + startAngle: 1.0927278795094932, + endAngle: 2.731819698773733, innerRadius: 0, - cornerRadius: 0, - fill: '#1AC6FF', - stroke: '#1AC6FF', - pickable: true + outerRadius: 80, + fillOpacity: 1 }, - _uid: 16, + _uid: 53, type: 'arc', children: [] }, { attribute: { - visible: true, - lineWidth: 0, - fillOpacity: 1, - padAngle: 0, - x: 388, - y: 238, - startAngle: 3.0988669935009723, - endAngle: 3.609689958974673, - outerRadius: 190.4, + fill: '#ff7f0e', + x: 100, + y: 100, + startAngle: 2.731819698773733, + endAngle: 5.463639397547466, innerRadius: 0, - cornerRadius: 0, - fill: '#FF8A00', - stroke: '#FF8A00', - pickable: true + outerRadius: 80, + fillOpacity: 1 }, - _uid: 17, + _uid: 54, type: 'arc', children: [] }, { attribute: { - visible: true, - lineWidth: 0, - fillOpacity: 1, - padAngle: 0, - x: 388, - y: 238, - startAngle: 3.609689958974673, - endAngle: 3.9238492243336522, - outerRadius: 190.4, + fill: '#ffbb78', + x: 100, + y: 100, + startAngle: 5.463639397547466, + endAngle: 6.283185307179586, innerRadius: 0, - cornerRadius: 0, - fill: '#3CC780', - stroke: '#3CC780', - pickable: true + outerRadius: 80, + fillOpacity: 1 }, - _uid: 18, - type: 'arc', - children: [] - }, - { - attribute: { - visible: true, - lineWidth: 0, - fillOpacity: 1, - padAngle: 0, - x: 388, - y: 238, - startAngle: 3.9238492243336522, - endAngle: 4.151928850984271, - outerRadius: 190.4, - innerRadius: 0, - cornerRadius: 0, - fill: '#7442D4', - stroke: '#7442D4', - pickable: true - }, - _uid: 19, - type: 'arc', - children: [] - }, - { - attribute: { - visible: true, - lineWidth: 0, - fillOpacity: 1, - padAngle: 0, - x: 388, - y: 238, - startAngle: 4.151928850984271, - endAngle: 4.329742995177454, - outerRadius: 190.4, - innerRadius: 0, - cornerRadius: 0, - fill: '#FFC400', - stroke: '#FFC400', - pickable: true - }, - _uid: 20, - type: 'arc', - children: [] - }, - { - attribute: { - visible: true, - lineWidth: 0, - fillOpacity: 1, - padAngle: 0, - x: 388, - y: 238, - startAngle: 4.329742995177454, - endAngle: 4.492477494633405, - outerRadius: 190.4, - innerRadius: 0, - cornerRadius: 0, - fill: '#304D77', - stroke: '#304D77', - pickable: true - }, - _uid: 21, - type: 'arc', - children: [] - }, - { - attribute: { - visible: true, - lineWidth: 0, - fillOpacity: 1, - padAngle: 0, - x: 388, - y: 238, - startAngle: 4.492477494633405, - endAngle: 4.71238898038469, - outerRadius: 190.4, - innerRadius: 0, - cornerRadius: 0, - fill: '#B48DEB', - stroke: '#B48DEB', - pickable: true - }, - _uid: 22, + _uid: 55, type: 'arc', children: [] } ] + // children: [ + // { + // attribute: { + // visible: true, + // lineWidth: 0, + // fillOpacity: 1, + // padAngle: 0, + // x: 388, + // y: 238, + // startAngle: -1.5707963267948966, + // endAngle: 1.357168026350791, + // outerRadius: 190.4, + // innerRadius: 0, + // cornerRadius: 0, + // fill: '#1664FF', + // stroke: '#1664FF', + // pickable: true + // }, + // _uid: 15, + // type: 'arc', + // children: [] + // }, + // { + // attribute: { + // visible: true, + // lineWidth: 0, + // fillOpacity: 1, + // padAngle: 0, + // x: 388, + // y: 238, + // startAngle: 1.357168026350791, + // endAngle: 3.0988669935009723, + // outerRadius: 190.4, + // innerRadius: 0, + // cornerRadius: 0, + // fill: '#1AC6FF', + // stroke: '#1AC6FF', + // pickable: true + // }, + // _uid: 16, + // type: 'arc', + // children: [] + // }, + // { + // attribute: { + // visible: true, + // lineWidth: 0, + // fillOpacity: 1, + // padAngle: 0, + // x: 388, + // y: 238, + // startAngle: 3.0988669935009723, + // endAngle: 3.609689958974673, + // outerRadius: 190.4, + // innerRadius: 0, + // cornerRadius: 0, + // fill: '#FF8A00', + // stroke: '#FF8A00', + // pickable: true + // }, + // _uid: 17, + // type: 'arc', + // children: [] + // }, + // { + // attribute: { + // visible: true, + // lineWidth: 0, + // fillOpacity: 1, + // padAngle: 0, + // x: 388, + // y: 238, + // startAngle: 3.609689958974673, + // endAngle: 3.9238492243336522, + // outerRadius: 190.4, + // innerRadius: 0, + // cornerRadius: 0, + // fill: '#3CC780', + // stroke: '#3CC780', + // pickable: true + // }, + // _uid: 18, + // type: 'arc', + // children: [] + // }, + // { + // attribute: { + // visible: true, + // lineWidth: 0, + // fillOpacity: 1, + // padAngle: 0, + // x: 388, + // y: 238, + // startAngle: 3.9238492243336522, + // endAngle: 4.151928850984271, + // outerRadius: 190.4, + // innerRadius: 0, + // cornerRadius: 0, + // fill: '#7442D4', + // stroke: '#7442D4', + // pickable: true + // }, + // _uid: 19, + // type: 'arc', + // children: [] + // }, + // { + // attribute: { + // visible: true, + // lineWidth: 0, + // fillOpacity: 1, + // padAngle: 0, + // x: 388, + // y: 238, + // startAngle: 4.151928850984271, + // endAngle: 4.329742995177454, + // outerRadius: 190.4, + // innerRadius: 0, + // cornerRadius: 0, + // fill: '#FFC400', + // stroke: '#FFC400', + // pickable: true + // }, + // _uid: 20, + // type: 'arc', + // children: [] + // }, + // { + // attribute: { + // visible: true, + // lineWidth: 0, + // fillOpacity: 1, + // padAngle: 0, + // x: 388, + // y: 238, + // startAngle: 4.329742995177454, + // endAngle: 4.492477494633405, + // outerRadius: 190.4, + // innerRadius: 0, + // cornerRadius: 0, + // fill: '#304D77', + // stroke: '#304D77', + // pickable: true + // }, + // _uid: 21, + // type: 'arc', + // children: [] + // }, + // { + // attribute: { + // visible: true, + // lineWidth: 0, + // fillOpacity: 1, + // padAngle: 0, + // x: 388, + // y: 238, + // startAngle: 4.492477494633405, + // endAngle: 4.71238898038469, + // outerRadius: 190.4, + // innerRadius: 0, + // cornerRadius: 0, + // fill: '#B48DEB', + // stroke: '#B48DEB', + // pickable: true + // }, + // _uid: 22, + // type: 'arc', + // children: [] + // } + // ] }; return spec; }; @@ -191,17 +253,17 @@ const pieGenerator = () => { const latestData = [ { type: 'oxygen', - value: '46.60', - __VCHART_DEFAULT_DATA_INDEX: 0, - __VCHART_DEFAULT_DATA_KEY: 'oxygen_oxygen_0', - __VCHART_ARC_RATIO: 0.4660000000000001, - __VCHART_ARC_START_ANGLE: -1.5707963267948966, - __VCHART_ARC_END_ANGLE: 1.357168026350791, - __VCHART_ARC_MIDDLE_ANGLE: -0.10681415022205276, - __VCHART_ARC_RADIAN: 2.9279643531456876, - __VCHART_ARC_QUADRANT: 1, - __VCHART_ARC_K: 1, - VGRAMMAR_DATA_ID_KEY_16: 0 + value: '46.60' + // __VCHART_DEFAULT_DATA_INDEX: 0, + // __VCHART_DEFAULT_DATA_KEY: 'oxygen_oxygen_0', + // __VCHART_ARC_RATIO: 0.4660000000000001, + // __VCHART_ARC_START_ANGLE: -1.5707963267948966, + // __VCHART_ARC_END_ANGLE: 1.357168026350791, + // __VCHART_ARC_MIDDLE_ANGLE: -0.10681415022205276, + // __VCHART_ARC_RADIAN: 2.9279643531456876, + // __VCHART_ARC_QUADRANT: 1, + // __VCHART_ARC_K: 1, + // VGRAMMAR_DATA_ID_KEY_16: 0 }, { type: 'silicon', @@ -317,19 +379,19 @@ function createContent(stage: Stage) { baseMarkGroupName: pieSpec.name, data: pieSpec.children.map((c, index) => { return { - text: latestData[index].type, - fill: c.attribute.fill, - line: { - stroke: c.attribute.stroke - }, - lineWidth: 0, - ...latestData[index] + // text: latestData[index].type, + text: c._uid + // fill: c.attribute.fill, + // line: { + // stroke: c.attribute.stroke + // }, + // lineWidth: 0 + // ...latestData[index] }; }), type: 'arc', width: 800, height: 500, - center: { x: 388, y: 238 }, position: 'outside', // position: 'inside', diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts index 6f8187020..533b23fc4 100644 --- a/packages/vrender-components/src/label/arc.ts +++ b/packages/vrender-components/src/label/arc.ts @@ -12,9 +12,8 @@ import { lineCirclePoints, connectLineRadian, checkBoundsOverlap, - degrees + computeQuadrant } from './util'; -import { ARC_K, ARC_MIDDLE_ANGLE, ARC_QUADRANT, ARC_RADIAN } from './constant'; import type { IGraphic } from '@visactor/vrender'; export class ArcInfo { @@ -55,8 +54,7 @@ export class ArcInfo { outerCenter: IPoint, quadrant: Quadrant, radian: number, - middleAngle: number, - k: number + middleAngle: number ) { this.refDatum = refDatum; this.center = center; @@ -64,7 +62,6 @@ export class ArcInfo { this.quadrant = quadrant; this.radian = radian; this.middleAngle = middleAngle; - this.k = k; this.labelVisible = true; this.labelLimit = 0; } @@ -148,28 +145,17 @@ export class ArcLabel extends LabelBase { // setArcs : 根据 arc 设置 datum 中对应的标签数据 const radiusRatio = this.computeLayoutOuterRadius(graphicAttributes.outerRadius, width, height); const radius = this.computeRadius(radiusRatio, width, height); - const center = attribute.center ?? { x: 0, y: 0 }; + const center = { x: graphicAttributes?.x ?? 0, y: graphicAttributes?.y ?? 0 }; const item = textData; - const arcMiddle = circlePoint(center.x, center.y, radius * item[ARC_K], item[ARC_MIDDLE_ANGLE]); + const arcMiddleAngle = (graphicAttributes.startAngle + graphicAttributes.endAngle) / 2; + const intervalAngle = graphicAttributes.endAngle - graphicAttributes.startAngle; + const arcQuadrant = computeQuadrant(graphicAttributes.endAngle - intervalAngle / 2); - const outerArcMiddle = circlePoint( - center.x, - center.y, - radius + attribute.line.line1MinLength, - item[ARC_MIDDLE_ANGLE] - ); - - const arc = new ArcInfo( - item, - arcMiddle, - outerArcMiddle, - item[ARC_QUADRANT], - item[ARC_RADIAN], - item[ARC_MIDDLE_ANGLE], - item[ARC_K] - ); + const arcMiddle = circlePoint(center.x, center.y, graphicAttributes.outerRadius, arcMiddleAngle); + const outerArcMiddle = circlePoint(center.x, center.y, radius + attribute.line.line1MinLength, arcMiddleAngle); + const arc = new ArcInfo(item, arcMiddle, outerArcMiddle, arcQuadrant, intervalAngle, arcMiddleAngle); // refDatum: any, // center: IPoint, @@ -192,11 +178,11 @@ export class ArcLabel extends LabelBase { }; if (isQuadrantRight(arc.quadrant)) { arc.textAlign = 'left'; - arc.textBaseline = 'bottom'; + arc.textBaseline = 'middle'; this._arcRight.set(arc.refDatum, arc); } else if (isQuadrantLeft(arc.quadrant)) { arc.textAlign = 'right'; - arc.textBaseline = 'bottom'; + arc.textBaseline = 'middle'; this._arcLeft.set(arc.refDatum, arc); } @@ -222,7 +208,7 @@ export class ArcLabel extends LabelBase { * 布局内部标签 */ private _layoutInsideLabels(arcs: ArcInfo[], attribute: any, currentMarks: any[]) { - const center = attribute.center ?? { x: 0, y: 0 }; + const center = { x: currentMarks[0].attribute?.x ?? 0, y: currentMarks[0].attribute?.y ?? 0 }; const innerRadiusRatio = this.computeLayoutOuterRadius( currentMarks[0].attribute.innerRadius, attribute.width, @@ -280,8 +266,8 @@ export class ArcLabel extends LabelBase { * 布局外部标签 */ private _layoutOutsideLabels(arcs: ArcInfo[], attribute: any, currentMarks: any[]) { - // const height = Math.min(attribute.center.x, attribute.center.y) * 2; - const height = attribute.center.y * 2; + const center = { x: currentMarks[0].attribute?.x ?? 0, y: currentMarks[0].attribute?.y ?? 0 }; + const height = center.y * 2; const line2MinLength = attribute.line.line2MinLength as number; const labelLayout = attribute.layout; const spaceWidth = attribute.spaceWidth as number; @@ -338,7 +324,7 @@ export class ArcLabel extends LabelBase { this._computeX(arc, attribute, currentMarks); } } - const width = attribute.center.x * 2; + const width = center.x * 2; arcs.forEach(arc => { if ( arc.labelVisible && @@ -356,8 +342,8 @@ export class ArcLabel extends LabelBase { * 计算 pointC 以及 label limit 与 position */ private _computeX(arc: ArcInfo, attribute: any, currentMarks: any[]) { - const center = attribute.center ?? { x: 0, y: 0 }; - const plotLayout = { width: attribute.center.x * 2, height: attribute.center.y * 2 }; + const center = { x: currentMarks[0].attribute?.x ?? 0, y: currentMarks[0].attribute?.y ?? 0 }; + const plotLayout = { width: center.x * 2, height: center.y * 2 }; const radiusRatio = this.computeLayoutOuterRadius( currentMarks[0].attribute.outerRadius, attribute.width, @@ -453,7 +439,8 @@ export class ArcLabel extends LabelBase { * 调整标签位置的 Y 值 */ private _adjustY(arcs: ArcInfo[], maxLabels: number, attribute: any, currentMarks: any[]) { - const plotRect = { width: attribute.center.x * 2, height: attribute.center.y * 2 }; + const center = { x: currentMarks[0].attribute?.x ?? 0, y: currentMarks[0].attribute?.y ?? 0 }; + const plotRect = { width: center.x * 2, height: center.y * 2 }; const labelLayout = attribute.layout; if (labelLayout.strategy === 'vertical') { // vertical 策略类似 echarts 方案,没有切线限制策略,没有优先级,执行整体调整没有标签数量限制 @@ -632,7 +619,7 @@ export class ArcLabel extends LabelBase { y: arc.outerCenter.y }; } else { - const center = attribute.center ?? { x: 0, y: 0 }; + const center = { x: currentMarks[0].attribute?.x ?? 0, y: currentMarks[0].attribute?.y ?? 0 }; const radius = this.computeRadius(radiusRatio, attribute.width, attribute.height); const { labelPosition, quadrant } = arc; const outerR = Math.max(radius + line1MinLength, currentMarks[0].attribute.outerRadius); @@ -665,7 +652,8 @@ export class ArcLabel extends LabelBase { * 计算圆弧切线所限制的标签 Y 值范围 */ private _computeYRange(arc: ArcInfo, attribute: any, currentMarks: any[]) { - const plotRect = { width: attribute.center.x * 2, height: attribute.center.y * 2 }; + const center = { x: currentMarks[0].attribute?.x ?? 0, y: currentMarks[0].attribute?.y ?? 0 }; + const plotRect = { width: center.x * 2, height: center.y * 2 }; const radiusRatio = this.computeLayoutOuterRadius( currentMarks[0].attribute.outerRadius, diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 02c013866..0d884654a 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -232,6 +232,7 @@ export abstract class LabelBase extends AbstractCompon const baseMark = this._idToGraphic.get(textData.id); const labelAttribute = { + fill: baseMark.attribute.fill, ...textStyle, ...textData }; diff --git a/packages/vrender-components/src/label/constant.ts b/packages/vrender-components/src/label/constant.ts deleted file mode 100644 index c2a073ab5..000000000 --- a/packages/vrender-components/src/label/constant.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const PREFIX = '__VCHART'; - -export const ARC_RATIO = `${PREFIX}_ARC_RATIO`; -export const ARC_START_ANGLE = `${PREFIX}_ARC_START_ANGLE`; -export const ARC_END_ANGLE = `${PREFIX}_ARC_END_ANGLE`; -export const ARC_K = `${PREFIX}_ARC_K`; -export const ARC_LABEL_HOVER_AX = `${PREFIX}_ARC_LABEL_HOVER_AX`; -export const ARC_LABEL_HOVER_AY = `${PREFIX}_ARC_LABEL_HOVER_AY`; -export const ARC_LABEL_POINT_AX = `${PREFIX}_ARC_LABEL_POINT_AX`; -export const ARC_LABEL_POINT_AY = `${PREFIX}_ARC_LABEL_POINT_AY`; -export const ARC_LABEL_POINT_BX = `${PREFIX}_ARC_LABEL_POINT_BX`; -export const ARC_LABEL_POINT_BY = `${PREFIX}_ARC_LABEL_POINT_BY`; -export const ARC_LABEL_POINT_CX = `${PREFIX}_ARC_LABEL_POINT_CX`; -export const ARC_LABEL_POINT_CY = `${PREFIX}_ARC_LABEL_POINT_CY`; -export const ARC_LABEL_SELECTED_AX = `${PREFIX}_ARC_LABEL_SELECTED_AX`; -export const ARC_LABEL_SELECTED_AY = `${PREFIX}_ARC_LABEL_SELECTED_AY`; -export const ARC_MIDDLE_ANGLE = `${PREFIX}_ARC_MIDDLE_ANGLE`; -export const ARC_QUADRANT = `${PREFIX}_ARC_QUADRANT`; -export const ARC_RADIAN = `${PREFIX}_ARC_RADIAN`; diff --git a/packages/vrender-components/src/label/util.ts b/packages/vrender-components/src/label/util.ts index e54b46d06..e1a002799 100644 --- a/packages/vrender-components/src/label/util.ts +++ b/packages/vrender-components/src/label/util.ts @@ -35,6 +35,38 @@ export function circlePoint(x0: number, y0: number, radius: number, radian: numb }; } +/** + * 根据角度计算象限 + * 计算角度所在象限 + * @param angle + * @returns + */ +export function computeQuadrant(angle: number): Quadrant { + angle = normalizeAngle(angle); + if (angle > 0 && angle <= Math.PI / 2) { + return 2; + } else if (angle > Math.PI / 2 && angle <= Math.PI) { + return 3; + } else if (angle > Math.PI && angle <= (3 * Math.PI) / 2) { + return 4; + } + return 1; +} + +/** + * 角度标准化处理 + * @param angle 弧度角 + */ +export function normalizeAngle(angle: number): number { + while (angle < 0) { + angle += Math.PI * 2; + } + while (angle >= Math.PI * 2) { + angle -= Math.PI * 2; + } + return angle; +} + export function isQuadrantLeft(quadrant: Quadrant): boolean { return quadrant === 3 || quadrant === 4; } From 5655836d6be30b4221a4412e975dbf572f4ea4c7 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Fri, 4 Aug 2023 17:36:59 +0800 Subject: [PATCH 15/29] feat: arc label supports setting angle --- .../__tests__/browser/examples/label-arc.ts | 1 + packages/vrender-components/src/label/base.ts | 6 +++--- packages/vrender-components/src/label/type.ts | 12 ++++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/vrender-components/__tests__/browser/examples/label-arc.ts b/packages/vrender-components/__tests__/browser/examples/label-arc.ts index 4b57e4e41..0e71e3d87 100644 --- a/packages/vrender-components/__tests__/browser/examples/label-arc.ts +++ b/packages/vrender-components/__tests__/browser/examples/label-arc.ts @@ -400,6 +400,7 @@ function createContent(stage: Stage) { // strategy: 'none' // }, + // angle: 0, zIndex: 302 }); diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 0d884654a..0bc23ac17 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -283,9 +283,9 @@ export abstract class LabelBase extends AbstractCompon const labelAttribute = { x: basedArc.labelPosition.x, y: basedArc.labelPosition.y, - textAlign: basedArc.textAlign, - textBaseline: basedArc.textBaseline, - angle: basedArc.angle + textAlign: this.attribute.textAlign ?? basedArc.textAlign, + textBaseline: this.attribute.textBaseline ?? basedArc.textBaseline, + angle: this.attribute.angle ?? basedArc.angle }; labels[i].setAttributes(labelAttribute); diff --git a/packages/vrender-components/src/label/type.ts b/packages/vrender-components/src/label/type.ts index b5072f6d9..6a7ab043b 100644 --- a/packages/vrender-components/src/label/type.ts +++ b/packages/vrender-components/src/label/type.ts @@ -246,6 +246,18 @@ export interface ArcLabelAttrs extends BaseLabelAttrs { * @default 5 */ spaceWidth?: number; + /** + * 标签旋转角度 + */ + angle?: number; + /** + * 标签横向点对齐 + */ + textAlign?: string; + /** + * 标签纵向点对齐 + */ + textBaseline?: string; /** * 扇区间标签的间隔 * @default 6 From 1483b0439a9be1239322dfa6cc7717c3838ee298 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Fri, 4 Aug 2023 17:39:11 +0800 Subject: [PATCH 16/29] feat: add rush change log --- .../feat-arc-label_2023-08-04-09-38.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@visactor/vrender-components/feat-arc-label_2023-08-04-09-38.json diff --git a/common/changes/@visactor/vrender-components/feat-arc-label_2023-08-04-09-38.json b/common/changes/@visactor/vrender-components/feat-arc-label_2023-08-04-09-38.json new file mode 100644 index 000000000..1cef4f330 --- /dev/null +++ b/common/changes/@visactor/vrender-components/feat-arc-label_2023-08-04-09-38.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-components", + "comment": "feat: arc label sink", + "type": "patch" + } + ], + "packageName": "@visactor/vrender-components" +} \ No newline at end of file From c7c08b4fa9dae522ef1c5490a1c6fd1d6341a1e4 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Fri, 4 Aug 2023 17:41:53 +0800 Subject: [PATCH 17/29] feat: specify the attribute type --- packages/vrender-components/src/label/base.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 0bc23ac17..51f4890dd 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -283,9 +283,9 @@ export abstract class LabelBase extends AbstractCompon const labelAttribute = { x: basedArc.labelPosition.x, y: basedArc.labelPosition.y, - textAlign: this.attribute.textAlign ?? basedArc.textAlign, - textBaseline: this.attribute.textBaseline ?? basedArc.textBaseline, - angle: this.attribute.angle ?? basedArc.angle + textAlign: (this.attribute as ArcLabelAttrs).textAlign ?? basedArc.textAlign, + textBaseline: (this.attribute as ArcLabelAttrs).textBaseline ?? basedArc.textBaseline, + angle: (this.attribute as ArcLabelAttrs).angle ?? basedArc.angle }; labels[i].setAttributes(labelAttribute); From 826da884ac6f31d2a5389c9c3f0f0c36267da02b Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Fri, 4 Aug 2023 17:45:40 +0800 Subject: [PATCH 18/29] feat: modify TextAlignType, TextBaselineType as vrender type --- packages/vrender-components/src/label/arc.ts | 2 +- packages/vrender-components/src/label/type.ts | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts index 533b23fc4..ec74daca0 100644 --- a/packages/vrender-components/src/label/arc.ts +++ b/packages/vrender-components/src/label/arc.ts @@ -3,7 +3,7 @@ import { merge } from '@visactor/vutils'; import { LabelBase } from './base'; import type { ArcLabelAttrs, IPoint, Quadrant, TextAlign } from './type'; import type { BaseLabelAttrs } from './type'; -import type { ITextGraphicAttribute, TextAlignType, TextBaselineType } from '@visactor/vrender'; +import type { TextAlignType, TextBaselineType } from '@visactor/vrender'; import { isValidNumber, isNil, isLess, isGreater, isNumberClose as isClose } from '@visactor/vutils'; import { circlePoint, diff --git a/packages/vrender-components/src/label/type.ts b/packages/vrender-components/src/label/type.ts index 6a7ab043b..6e15686ae 100644 --- a/packages/vrender-components/src/label/type.ts +++ b/packages/vrender-components/src/label/type.ts @@ -1,4 +1,12 @@ -import type { EasingType, IGraphic, IGroupGraphicAttribute, ITextGraphicAttribute, Text } from '@visactor/vrender'; +import type { + EasingType, + IGraphic, + IGroupGraphicAttribute, + ITextGraphicAttribute, + Text, + TextAlignType, + TextBaselineType +} from '@visactor/vrender'; export type LabelItemStateStyle = { hover?: T; @@ -253,11 +261,11 @@ export interface ArcLabelAttrs extends BaseLabelAttrs { /** * 标签横向点对齐 */ - textAlign?: string; + textAlign?: TextAlignType; /** * 标签纵向点对齐 */ - textBaseline?: string; + textBaseline?: TextBaselineType; /** * 扇区间标签的间隔 * @default 6 From be33d9900209787513f5663d9303a92d1256d9fc Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Tue, 8 Aug 2023 19:19:43 +0800 Subject: [PATCH 19/29] feat: modify arcLabel position --- .../__tests__/browser/examples/label-arc.ts | 28 ++++++++++++++++++- packages/vrender-components/src/label/arc.ts | 15 +++++++++- packages/vrender-components/src/label/base.ts | 28 ++++++++++++------- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/packages/vrender-components/__tests__/browser/examples/label-arc.ts b/packages/vrender-components/__tests__/browser/examples/label-arc.ts index 0e71e3d87..43366d9b9 100644 --- a/packages/vrender-components/__tests__/browser/examples/label-arc.ts +++ b/packages/vrender-components/__tests__/browser/examples/label-arc.ts @@ -250,6 +250,25 @@ const pieGenerator = () => { return spec; }; +const originData = [ + { + id: 'AAA', + value: 4 + }, + { + id: 'BBB', + value: 6 + }, + { + id: 'CCC', + value: 10 + }, + { + id: 'DDD', + value: 3 + } +]; + const latestData = [ { type: 'oxygen', @@ -380,7 +399,7 @@ function createContent(stage: Stage) { data: pieSpec.children.map((c, index) => { return { // text: latestData[index].type, - text: c._uid + text: originData[index].id // fill: c.attribute.fill, // line: { // stroke: c.attribute.stroke @@ -390,9 +409,16 @@ function createContent(stage: Stage) { }; }), type: 'arc', + // animation: false, + animation: { + // mode: 'same-time', + // duration: 300, + // easing: 'linear' + }, width: 800, height: 500, position: 'outside', + // position: 'inside', // coverEnable: false, diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts index ec74daca0..35f690c51 100644 --- a/packages/vrender-components/src/label/arc.ts +++ b/packages/vrender-components/src/label/arc.ts @@ -102,7 +102,9 @@ export class ArcLabel extends LabelBase { visible: true, fontSize: 14, fontWeight: 'normal', - fillOpacity: 1 + fillOpacity: 1, + textAlign: 'center', + textBaseline: 'middle' }, position: 'outside', offset: 0, @@ -334,7 +336,18 @@ export class ArcLabel extends LabelBase { arc.labelVisible = false; } arc.angle = 0; + + // arc.labelPosition.x = isQuadrantLeft(arc.quadrant) + // ? arc.labelPosition.x - arc.labelSize.width + // : arc.labelPosition.x; + + // arc.labelPosition.y = arc.labelPosition.y + 0.25 * arc.labelSize.height; + + arc.labelPosition.x = isQuadrantLeft(arc.quadrant) + ? arc.labelPosition.x + : arc.labelPosition.x + 0.5 * arc.labelSize.width; }); + return arcs; } diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 51f4890dd..c07dba54c 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -79,14 +79,12 @@ export abstract class LabelBase extends AbstractCompon // 根据关联图元和配置的position计算标签坐标 labels = this.layout(data); - if (this.attribute.type !== 'arc') { - if (isFunction(customOverlapFunc)) { - labels = customOverlapFunc(labels as Text[], (d: LabelItem) => this._idToGraphic.get(d.id)); - } else { - // 防重叠逻辑 - if (overlap !== false) { - labels = this._overlapping(labels); - } + if (isFunction(customOverlapFunc)) { + labels = customOverlapFunc(labels as Text[], (d: LabelItem) => this._idToGraphic.get(d.id)); + } else { + // 防重叠逻辑 + if (overlap !== false && this.attribute.type !== 'arc') { + labels = this._overlapping(labels); } } } @@ -283,8 +281,10 @@ export abstract class LabelBase extends AbstractCompon const labelAttribute = { x: basedArc.labelPosition.x, y: basedArc.labelPosition.y, - textAlign: (this.attribute as ArcLabelAttrs).textAlign ?? basedArc.textAlign, - textBaseline: (this.attribute as ArcLabelAttrs).textBaseline ?? basedArc.textBaseline, + textAlign: 'center', + textBaseline: 'middle', + // textAlign: (this.attribute as ArcLabelAttrs).textAlign ?? basedArc.textAlign, + // textBaseline: (this.attribute as ArcLabelAttrs).textBaseline ?? basedArc.textBaseline, angle: (this.attribute as ArcLabelAttrs).angle ?? basedArc.angle }; @@ -297,6 +297,7 @@ export abstract class LabelBase extends AbstractCompon } } + // console.log('labels', labels); return labels; } @@ -465,6 +466,10 @@ export abstract class LabelBase extends AbstractCompon } const relatedGraphic = this._idToGraphic.get((text.attribute as LabelItem).id); const state = prevTextMap?.get(relatedGraphic) ? 'update' : 'enter'; + // console.log('relatedGraphic', relatedGraphic); + // console.log('prevTextMap', prevTextMap); + // console.log('state', state); + if (state === 'enter') { texts.push(text); if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { @@ -490,6 +495,9 @@ export abstract class LabelBase extends AbstractCompon }; } else { this.add(text); + if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { + this.add(labelLine); + } } } From 2d426f76a0119c4ebc9ed9bb65b02e1d8e077517 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Tue, 8 Aug 2023 19:20:43 +0800 Subject: [PATCH 20/29] feat: add rush change log --- .../feat-arc-label_2023-08-08-11-20.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@visactor/vrender-components/feat-arc-label_2023-08-08-11-20.json diff --git a/common/changes/@visactor/vrender-components/feat-arc-label_2023-08-08-11-20.json b/common/changes/@visactor/vrender-components/feat-arc-label_2023-08-08-11-20.json new file mode 100644 index 000000000..3163fb1d6 --- /dev/null +++ b/common/changes/@visactor/vrender-components/feat-arc-label_2023-08-08-11-20.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-components", + "comment": "feat: modify arcLabel position", + "type": "patch" + } + ], + "packageName": "@visactor/vrender-components" +} \ No newline at end of file From bf0c91d8229d3966c91af31156be66fb2a1d5f61 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Tue, 8 Aug 2023 19:59:26 +0800 Subject: [PATCH 21/29] feat: delete textAlign&textBaseline of arc label attribute --- packages/vrender-components/src/label/base.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index c07dba54c..6a514f1c8 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -281,8 +281,6 @@ export abstract class LabelBase extends AbstractCompon const labelAttribute = { x: basedArc.labelPosition.x, y: basedArc.labelPosition.y, - textAlign: 'center', - textBaseline: 'middle', // textAlign: (this.attribute as ArcLabelAttrs).textAlign ?? basedArc.textAlign, // textBaseline: (this.attribute as ArcLabelAttrs).textBaseline ?? basedArc.textBaseline, angle: (this.attribute as ArcLabelAttrs).angle ?? basedArc.angle From 0aba8980b755e7fb6392d9ef35abfa6ff1c4f098 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Tue, 8 Aug 2023 23:43:24 +0800 Subject: [PATCH 22/29] feat: optimize arc label --- packages/vrender-components/src/label/arc.ts | 6 +++ packages/vrender-components/src/label/base.ts | 46 ++++++++----------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts index 35f690c51..4d2e80b84 100644 --- a/packages/vrender-components/src/label/arc.ts +++ b/packages/vrender-components/src/label/arc.ts @@ -37,6 +37,7 @@ export class ArcInfo { pointA!: IPoint; pointB!: IPoint; pointC!: IPoint; + labelLinePath!: string; /** * 象限 */ @@ -346,6 +347,11 @@ export class ArcLabel extends LabelBase { arc.labelPosition.x = isQuadrantLeft(arc.quadrant) ? arc.labelPosition.x : arc.labelPosition.x + 0.5 * arc.labelSize.width; + + arc.labelLinePath = + `M${Math.round(arc.pointA.x)},${Math.round(arc.pointA.y)}` + + ` L${Math.round(arc.pointB.x)},${Math.round(arc.pointB.y)}` + + ` L${Math.round(arc.pointC.x)},${Math.round(arc.pointC.y)}`; }); return arcs; diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 6a514f1c8..56206cb15 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -281,17 +281,16 @@ export abstract class LabelBase extends AbstractCompon const labelAttribute = { x: basedArc.labelPosition.x, y: basedArc.labelPosition.y, - // textAlign: (this.attribute as ArcLabelAttrs).textAlign ?? basedArc.textAlign, - // textBaseline: (this.attribute as ArcLabelAttrs).textBaseline ?? basedArc.textBaseline, - angle: (this.attribute as ArcLabelAttrs).angle ?? basedArc.angle + angle: (this.attribute as ArcLabelAttrs).angle ?? basedArc.angle, + labelLinePath: basedArc.labelLinePath }; labels[i].setAttributes(labelAttribute); // 用于做labelLine - labels[i].pointA = basedArc.pointA; - labels[i].pointB = basedArc.pointB; - labels[i].pointC = basedArc.pointC; + // labels[i].pointA = basedArc.pointA; + // labels[i].pointB = basedArc.pointB; + // labels[i].pointC = basedArc.pointC; } } @@ -448,36 +447,31 @@ export abstract class LabelBase extends AbstractCompon const labelLines = [] as IPath[]; labels.forEach((text, index) => { - // const text = this._createLabelText(label); - let labelLine: IPath; - - if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { - labelLine = createPath({ - visible: text.attribute?.visible ?? true, - stroke: (text.attribute as ArcLabelAttrs)?.line?.stroke ?? text.attribute?.fill, - lineWidth: 1, - path: - `M${Math.round(text.pointA.x)},${Math.round(text.pointA.y)}` + - ` L${Math.round(text.pointB.x)},${Math.round(text.pointB.y)}` + - ` L${Math.round(text.pointC.x)},${Math.round(text.pointC.y)}` - }) as Path; - } + // let labelLine: IPath; + + // if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { + const labelLine: IPath = text.attribute?.labelLinePath + ? (createPath({ + visible: text.attribute?.visible ?? true, + stroke: (text.attribute as ArcLabelAttrs)?.line?.stroke ?? text.attribute?.fill, + lineWidth: 1, + path: text.attribute?.labelLinePath + }) as Path) + : undefined; + // } const relatedGraphic = this._idToGraphic.get((text.attribute as LabelItem).id); const state = prevTextMap?.get(relatedGraphic) ? 'update' : 'enter'; - // console.log('relatedGraphic', relatedGraphic); - // console.log('prevTextMap', prevTextMap); - // console.log('state', state); if (state === 'enter') { texts.push(text); - if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { + if (labelLine) { labelLines.push(labelLine); } currentTextMap.set(relatedGraphic, text); if (!disableAnimation && relatedGraphic) { const { from, to } = getAnimationAttributes(text.attribute, 'fadeIn'); this.add(text); - if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { + if (labelLine) { this.add(labelLine); } relatedGraphic.onAnimateBind = () => { @@ -493,7 +487,7 @@ export abstract class LabelBase extends AbstractCompon }; } else { this.add(text); - if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { + if (labelLine) { this.add(labelLine); } } From 26f3d4dadcf07e7977932df7ebc2a34656f8319f Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Wed, 9 Aug 2023 00:21:58 +0800 Subject: [PATCH 23/29] feat: decoupling arcLabel and base --- packages/vrender-components/src/label/arc.ts | 29 ++++----- packages/vrender-components/src/label/base.ts | 63 +++++-------------- 2 files changed, 29 insertions(+), 63 deletions(-) diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts index 4d2e80b84..7d9c7450b 100644 --- a/packages/vrender-components/src/label/arc.ts +++ b/packages/vrender-components/src/label/arc.ts @@ -135,10 +135,10 @@ export class ArcLabel extends LabelBase { graphicBounds: IBoundsLike, position = 'outside', offset = 0, - graphicAttributes: any, + graphicAttribute: any, textData: any, - width: number, - height: number, + // width: number, + // height: number, attribute: any ): { x: number; y: number } | undefined { if (!textBounds || !graphicBounds) { @@ -146,17 +146,18 @@ export class ArcLabel extends LabelBase { } // setArcs : 根据 arc 设置 datum 中对应的标签数据 - const radiusRatio = this.computeLayoutOuterRadius(graphicAttributes.outerRadius, width, height); + const { width, height } = attribute as ArcLabelAttrs; + const radiusRatio = this.computeLayoutOuterRadius(graphicAttribute.outerRadius, width, height); const radius = this.computeRadius(radiusRatio, width, height); - const center = { x: graphicAttributes?.x ?? 0, y: graphicAttributes?.y ?? 0 }; + const center = { x: graphicAttribute?.x ?? 0, y: graphicAttribute?.y ?? 0 }; const item = textData; - const arcMiddleAngle = (graphicAttributes.startAngle + graphicAttributes.endAngle) / 2; - const intervalAngle = graphicAttributes.endAngle - graphicAttributes.startAngle; - const arcQuadrant = computeQuadrant(graphicAttributes.endAngle - intervalAngle / 2); + const arcMiddleAngle = (graphicAttribute.startAngle + graphicAttribute.endAngle) / 2; + const intervalAngle = graphicAttribute.endAngle - graphicAttribute.startAngle; + const arcQuadrant = computeQuadrant(graphicAttribute.endAngle - intervalAngle / 2); - const arcMiddle = circlePoint(center.x, center.y, graphicAttributes.outerRadius, arcMiddleAngle); + const arcMiddle = circlePoint(center.x, center.y, graphicAttribute.outerRadius, arcMiddleAngle); const outerArcMiddle = circlePoint(center.x, center.y, radius + attribute.line.line1MinLength, arcMiddleAngle); const arc = new ArcInfo(item, arcMiddle, outerArcMiddle, arcQuadrant, intervalAngle, arcMiddleAngle); @@ -171,7 +172,7 @@ export class ArcLabel extends LabelBase { arc.pointA = circlePoint( (center as IPoint).x, (center as IPoint).y, - this.computeDatumRadius(center.x * 2, center.y * 2, graphicAttributes.outerRadius), + this.computeDatumRadius(center.x * 2, center.y * 2, graphicAttribute.outerRadius), arc.middleAngle ); @@ -180,16 +181,12 @@ export class ArcLabel extends LabelBase { height: textBounds.y2 - textBounds.y1 }; if (isQuadrantRight(arc.quadrant)) { - arc.textAlign = 'left'; - arc.textBaseline = 'middle'; this._arcRight.set(arc.refDatum, arc); } else if (isQuadrantLeft(arc.quadrant)) { - arc.textAlign = 'right'; - arc.textBaseline = 'middle'; this._arcLeft.set(arc.refDatum, arc); } - // return { arcRight: this._arcRight, arcLeft: this._arcLeft }; + return { x: 0, y: 0 }; } // layoutLabels : 执行内部/外部标签的布局计算 @@ -257,7 +254,7 @@ export class ArcLabel extends LabelBase { if (!isGreater(labelWidth, 0)) { arc.labelVisible = false; } - (arc.textAlign = 'center'), (arc.textBaseline = 'middle'); + // (arc.textAlign = 'center'), (arc.textBaseline = 'middle'); // arc.angle = degrees(arc.middleAngle); arc.angle = arc.middleAngle; diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 56206cb15..961d56e20 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -51,8 +51,6 @@ export abstract class LabelBase extends AbstractCompon graphicAttributes?: any, textData?: any, - width?: number, - height?: number, attribute?: any ): { x: number; y: number } | undefined; @@ -238,38 +236,23 @@ export abstract class LabelBase extends AbstractCompon const textBounds = this.getGraphicBounds(text); const graphicBounds = this.getGraphicBounds(baseMark, { x: textData.x as number, y: textData.y as number }); - if (this.attribute.type === 'arc') { - const graphicAttributes = baseMark.attribute; - const { width, height } = this.attribute as ArcLabelAttrs; - - this.labeling( - textBounds, - graphicBounds, - isFunction(position) ? position(textData) : position, - offset, - graphicAttributes, - textData, - width, - height, - this.attribute - ); - labels.push(text); - } else { - const textLocation = this.labeling( - textBounds, - graphicBounds, - isFunction(position) ? position(textData) : position, - offset - ); - if (!textLocation) { - continue; - } - labelAttribute.x = textLocation.x; - labelAttribute.y = textLocation.y; - - text.setAttributes(textLocation); - labels.push(text); + const textLocation = this.labeling( + textBounds, + graphicBounds, + isFunction(position) ? position(textData) : position, + offset, + baseMark.attribute, + textData, + this.attribute + ); + if (!textLocation) { + continue; } + labelAttribute.x = textLocation.x; + labelAttribute.y = textLocation.y; + + text.setAttributes(textLocation); + labels.push(text); } if (this.attribute.type === 'arc') { @@ -286,15 +269,9 @@ export abstract class LabelBase extends AbstractCompon }; labels[i].setAttributes(labelAttribute); - - // 用于做labelLine - // labels[i].pointA = basedArc.pointA; - // labels[i].pointB = basedArc.pointB; - // labels[i].pointC = basedArc.pointC; } } - // console.log('labels', labels); return labels; } @@ -444,12 +421,8 @@ export abstract class LabelBase extends AbstractCompon const currentTextMap = new Map(); const prevTextMap = this._graphicToText || new Map(); const texts = [] as IText[]; - const labelLines = [] as IPath[]; labels.forEach((text, index) => { - // let labelLine: IPath; - - // if (this.attribute.type === 'arc' && this.attribute.position === 'outside') { const labelLine: IPath = text.attribute?.labelLinePath ? (createPath({ visible: text.attribute?.visible ?? true, @@ -458,15 +431,11 @@ export abstract class LabelBase extends AbstractCompon path: text.attribute?.labelLinePath }) as Path) : undefined; - // } const relatedGraphic = this._idToGraphic.get((text.attribute as LabelItem).id); const state = prevTextMap?.get(relatedGraphic) ? 'update' : 'enter'; if (state === 'enter') { texts.push(text); - if (labelLine) { - labelLines.push(labelLine); - } currentTextMap.set(relatedGraphic, text); if (!disableAnimation && relatedGraphic) { const { from, to } = getAnimationAttributes(text.attribute, 'fadeIn'); From 8acab1a1c4185760d5f8da5547a011c34f952cb2 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Wed, 9 Aug 2023 00:33:02 +0800 Subject: [PATCH 24/29] feat: add type of labelLine path --- packages/vrender-components/src/label/arc.ts | 2 -- packages/vrender-components/src/label/base.ts | 2 +- packages/vrender-components/src/label/type.ts | 4 +++- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts index 7d9c7450b..f79b2277d 100644 --- a/packages/vrender-components/src/label/arc.ts +++ b/packages/vrender-components/src/label/arc.ts @@ -137,8 +137,6 @@ export class ArcLabel extends LabelBase { offset = 0, graphicAttribute: any, textData: any, - // width: number, - // height: number, attribute: any ): { x: number; y: number } | undefined { if (!textBounds || !graphicBounds) { diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 961d56e20..4f031735b 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -423,7 +423,7 @@ export abstract class LabelBase extends AbstractCompon const texts = [] as IText[]; labels.forEach((text, index) => { - const labelLine: IPath = text.attribute?.labelLinePath + const labelLine: IPath = (text.attribute as ArcLabelAttrs)?.labelLinePath ? (createPath({ visible: text.attribute?.visible ?? true, stroke: (text.attribute as ArcLabelAttrs)?.line?.stroke ?? text.attribute?.fill, diff --git a/packages/vrender-components/src/label/type.ts b/packages/vrender-components/src/label/type.ts index 6e15686ae..f897a84af 100644 --- a/packages/vrender-components/src/label/type.ts +++ b/packages/vrender-components/src/label/type.ts @@ -275,6 +275,8 @@ export interface ArcLabelAttrs extends BaseLabelAttrs { line?: IArcLabelLineSpec; /** 标签布局配置 */ layout?: IArcLabelLayoutSpec; + /** 标签引导线path */ + labelLinePath?: string; } export interface IArcLabelLineSpec { @@ -321,7 +323,7 @@ export interface IArcLabelLayoutSpec { } export interface DataLabelAttrs extends IGroupGraphicAttribute { - dataLabels: (RectLabelAttrs | SymbolLabelAttrs)[]; + dataLabels: (RectLabelAttrs | SymbolLabelAttrs | ArcLabelAttrs)[]; /** * 防重叠的区域大小 */ From cebe701579cece03525c601f05a8d2b86e724422 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Wed, 9 Aug 2023 00:38:18 +0800 Subject: [PATCH 25/29] feat: specify text.attribute type --- packages/vrender-components/src/label/base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 4f031735b..874136865 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -428,7 +428,7 @@ export abstract class LabelBase extends AbstractCompon visible: text.attribute?.visible ?? true, stroke: (text.attribute as ArcLabelAttrs)?.line?.stroke ?? text.attribute?.fill, lineWidth: 1, - path: text.attribute?.labelLinePath + path: (text.attribute as ArcLabelAttrs)?.labelLinePath }) as Path) : undefined; const relatedGraphic = this._idToGraphic.get((text.attribute as LabelItem).id); From a34cde35d789ae140678a1917128dc3b7778d60c Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Wed, 9 Aug 2023 11:30:23 +0800 Subject: [PATCH 26/29] feat: rewrite overlapping in arc label --- packages/vrender-components/src/label/arc.ts | 6 +++++- packages/vrender-components/src/label/base.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts index f79b2277d..3a323042e 100644 --- a/packages/vrender-components/src/label/arc.ts +++ b/packages/vrender-components/src/label/arc.ts @@ -3,7 +3,7 @@ import { merge } from '@visactor/vutils'; import { LabelBase } from './base'; import type { ArcLabelAttrs, IPoint, Quadrant, TextAlign } from './type'; import type { BaseLabelAttrs } from './type'; -import type { TextAlignType, TextBaselineType } from '@visactor/vrender'; +import type { TextAlignType, TextBaselineType, IText } from '@visactor/vrender'; import { isValidNumber, isNil, isLess, isGreater, isNumberClose as isClose } from '@visactor/vutils'; import { circlePoint, @@ -130,6 +130,10 @@ export class ArcLabel extends LabelBase { super(merge({}, ArcLabel.defaultAttributes, attributes)); } + protected _overlapping(labels: IText[]) { + return labels; + } + protected labeling( textBounds: IBoundsLike, graphicBounds: IBoundsLike, diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 874136865..b8e9fee5d 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -81,7 +81,7 @@ export abstract class LabelBase extends AbstractCompon labels = customOverlapFunc(labels as Text[], (d: LabelItem) => this._idToGraphic.get(d.id)); } else { // 防重叠逻辑 - if (overlap !== false && this.attribute.type !== 'arc') { + if (overlap !== false) { labels = this._overlapping(labels); } } From 94272feb920fccb8bc7d3ff5227c12fb80fdb9dd Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Wed, 9 Aug 2023 16:53:33 +0800 Subject: [PATCH 27/29] feat: add the clearing logic of arcLeft and arcRight, migrate the lableling content of arc to layoutArcLabels --- packages/vrender-components/src/label/arc.ts | 100 ++++++++++-------- packages/vrender-components/src/label/base.ts | 29 +++-- 2 files changed, 73 insertions(+), 56 deletions(-) diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts index 3a323042e..ee0daf82b 100644 --- a/packages/vrender-components/src/label/arc.ts +++ b/packages/vrender-components/src/label/arc.ts @@ -138,61 +138,71 @@ export class ArcLabel extends LabelBase { textBounds: IBoundsLike, graphicBounds: IBoundsLike, position = 'outside', - offset = 0, - graphicAttribute: any, - textData: any, - attribute: any + offset = 0 ): { x: number; y: number } | undefined { if (!textBounds || !graphicBounds) { return; } + return { x: 0, y: 0 }; + } + + protected layoutArcLabels( + position: BaseLabelAttrs['position'], + attribute: any, + currentMarks?: IGraphic[], + data?: any, + textBoundsArray?: any + ) { // setArcs : 根据 arc 设置 datum 中对应的标签数据 + this._arcLeft.clear(); + this._arcRight.clear(); const { width, height } = attribute as ArcLabelAttrs; - const radiusRatio = this.computeLayoutOuterRadius(graphicAttribute.outerRadius, width, height); - const radius = this.computeRadius(radiusRatio, width, height); - const center = { x: graphicAttribute?.x ?? 0, y: graphicAttribute?.y ?? 0 }; - - const item = textData; - - const arcMiddleAngle = (graphicAttribute.startAngle + graphicAttribute.endAngle) / 2; - const intervalAngle = graphicAttribute.endAngle - graphicAttribute.startAngle; - const arcQuadrant = computeQuadrant(graphicAttribute.endAngle - intervalAngle / 2); - - const arcMiddle = circlePoint(center.x, center.y, graphicAttribute.outerRadius, arcMiddleAngle); - const outerArcMiddle = circlePoint(center.x, center.y, radius + attribute.line.line1MinLength, arcMiddleAngle); - const arc = new ArcInfo(item, arcMiddle, outerArcMiddle, arcQuadrant, intervalAngle, arcMiddleAngle); - - // refDatum: any, - // center: IPoint, - // outerCenter: IPoint, - // quadrant: Quadrant, - // radian: number, - // middleAngle: number, - // k: number - - arc.pointA = circlePoint( - (center as IPoint).x, - (center as IPoint).y, - this.computeDatumRadius(center.x * 2, center.y * 2, graphicAttribute.outerRadius), - arc.middleAngle - ); + currentMarks.forEach((currentMark, index) => { + const graphicAttribute = currentMark.attribute; + const radiusRatio = this.computeLayoutOuterRadius(graphicAttribute.outerRadius, width, height); + const radius = this.computeRadius(radiusRatio, width, height); + const center = { x: graphicAttribute?.x ?? 0, y: graphicAttribute?.y ?? 0 }; + + const item = data[index]; + const textBounds = textBoundsArray[index]; + + const arcMiddleAngle = (graphicAttribute.startAngle + graphicAttribute.endAngle) / 2; + const intervalAngle = graphicAttribute.endAngle - graphicAttribute.startAngle; + const arcQuadrant = computeQuadrant(graphicAttribute.endAngle - intervalAngle / 2); + + const arcMiddle = circlePoint(center.x, center.y, graphicAttribute.outerRadius, arcMiddleAngle); + const outerArcMiddle = circlePoint(center.x, center.y, radius + attribute.line.line1MinLength, arcMiddleAngle); + const arc = new ArcInfo(item, arcMiddle, outerArcMiddle, arcQuadrant, intervalAngle, arcMiddleAngle); + + // refDatum: any, + // center: IPoint, + // outerCenter: IPoint, + // quadrant: Quadrant, + // radian: number, + // middleAngle: number, + // k: number + + arc.pointA = circlePoint( + (center as IPoint).x, + (center as IPoint).y, + this.computeDatumRadius(center.x * 2, center.y * 2, graphicAttribute.outerRadius), + arc.middleAngle + ); - arc.labelSize = { - width: textBounds.x2 - textBounds.x1, - height: textBounds.y2 - textBounds.y1 - }; - if (isQuadrantRight(arc.quadrant)) { - this._arcRight.set(arc.refDatum, arc); - } else if (isQuadrantLeft(arc.quadrant)) { - this._arcLeft.set(arc.refDatum, arc); - } + arc.labelSize = { + width: textBounds.x2 - textBounds.x1, + height: textBounds.y2 - textBounds.y1 + }; - return { x: 0, y: 0 }; - } + if (isQuadrantRight(arc.quadrant)) { + this._arcRight.set(arc.refDatum, arc); + } else if (isQuadrantLeft(arc.quadrant)) { + this._arcLeft.set(arc.refDatum, arc); + } + }); - // layoutLabels : 执行内部/外部标签的布局计算 - protected layoutArcLabels(position: BaseLabelAttrs['position'], attribute: any, currentMarks?: IGraphic[]) { + // layoutLabels : 执行内部/外部标签的布局计算 const leftArcs = Array.from(this._arcLeft.values()); const rightArcs = Array.from(this._arcRight.values()); const arcs: ArcInfo[] = []; diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index b8e9fee5d..a3d4ebd47 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -47,14 +47,16 @@ export abstract class LabelBase extends AbstractCompon textBounds: IBoundsLike, graphicBounds: IBoundsLike, position?: BaseLabelAttrs['position'], - offset?: number, - - graphicAttributes?: any, - textData?: any, - attribute?: any + offset?: number ): { x: number; y: number } | undefined; - protected layoutArcLabels(position?: BaseLabelAttrs['position'], attribute?: any, currentMarks?: IGraphic[]): any { + protected layoutArcLabels( + position?: BaseLabelAttrs['position'], + attribute?: any, + currentMarks?: IGraphic[], + data?: any[], + textBoundsArray?: any[] + ): any { const arcs: ArcInfo[] = []; return arcs; } @@ -222,6 +224,7 @@ export abstract class LabelBase extends AbstractCompon protected layout(data: LabelItem[] = []): IText[] { const { textStyle = {}, position, offset } = this.attribute; const labels = []; + const textBoundsArray = []; for (let i = 0; i < data.length; i++) { const textData = data[i]; @@ -234,16 +237,14 @@ export abstract class LabelBase extends AbstractCompon }; const text = this._createLabelText(labelAttribute); const textBounds = this.getGraphicBounds(text); + textBoundsArray.push(textBounds); const graphicBounds = this.getGraphicBounds(baseMark, { x: textData.x as number, y: textData.y as number }); const textLocation = this.labeling( textBounds, graphicBounds, isFunction(position) ? position(textData) : position, - offset, - baseMark.attribute, - textData, - this.attribute + offset ); if (!textLocation) { continue; @@ -256,7 +257,13 @@ export abstract class LabelBase extends AbstractCompon } if (this.attribute.type === 'arc') { - const arcs: ArcInfo[] = this.layoutArcLabels(position, this.attribute, Array.from(this._idToGraphic.values())); + const arcs: ArcInfo[] = this.layoutArcLabels( + position, + this.attribute, + Array.from(this._idToGraphic.values()), + data, + textBoundsArray + ); for (let i = 0; i < data.length; i++) { const textData = data[i]; const basedArc = arcs.find(arc => arc.labelText === textData.text); From 2153861c2bf118fbfc789c998f7233fdf7405e22 Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Wed, 9 Aug 2023 16:58:30 +0800 Subject: [PATCH 28/29] feat: spetify graphicAttribute type as ArcAttrs --- packages/vrender-components/src/label/arc.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts index ee0daf82b..40f9dfa9d 100644 --- a/packages/vrender-components/src/label/arc.ts +++ b/packages/vrender-components/src/label/arc.ts @@ -3,7 +3,7 @@ import { merge } from '@visactor/vutils'; import { LabelBase } from './base'; import type { ArcLabelAttrs, IPoint, Quadrant, TextAlign } from './type'; import type { BaseLabelAttrs } from './type'; -import type { TextAlignType, TextBaselineType, IText } from '@visactor/vrender'; +import type { TextAlignType, TextBaselineType, IText, ArcAttrs } from '@visactor/vrender'; import { isValidNumber, isNil, isLess, isGreater, isNumberClose as isClose } from '@visactor/vutils'; import { circlePoint, @@ -159,7 +159,7 @@ export class ArcLabel extends LabelBase { this._arcRight.clear(); const { width, height } = attribute as ArcLabelAttrs; currentMarks.forEach((currentMark, index) => { - const graphicAttribute = currentMark.attribute; + const graphicAttribute = currentMark.attribute as ArcAttrs; const radiusRatio = this.computeLayoutOuterRadius(graphicAttribute.outerRadius, width, height); const radius = this.computeRadius(radiusRatio, width, height); const center = { x: graphicAttribute?.x ?? 0, y: graphicAttribute?.y ?? 0 }; From 6f60b59d2dc2a39c3d005084ab9c1a0e33201b3b Mon Sep 17 00:00:00 2001 From: pairone <1063258712@qq.com> Date: Wed, 9 Aug 2023 17:01:49 +0800 Subject: [PATCH 29/29] feat: spetify graphicAttribute type as IArcGraphicAttribute --- packages/vrender-components/src/label/arc.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts index 40f9dfa9d..35939dd30 100644 --- a/packages/vrender-components/src/label/arc.ts +++ b/packages/vrender-components/src/label/arc.ts @@ -3,7 +3,7 @@ import { merge } from '@visactor/vutils'; import { LabelBase } from './base'; import type { ArcLabelAttrs, IPoint, Quadrant, TextAlign } from './type'; import type { BaseLabelAttrs } from './type'; -import type { TextAlignType, TextBaselineType, IText, ArcAttrs } from '@visactor/vrender'; +import type { TextAlignType, TextBaselineType, IText, IArcGraphicAttribute } from '@visactor/vrender'; import { isValidNumber, isNil, isLess, isGreater, isNumberClose as isClose } from '@visactor/vutils'; import { circlePoint, @@ -159,7 +159,7 @@ export class ArcLabel extends LabelBase { this._arcRight.clear(); const { width, height } = attribute as ArcLabelAttrs; currentMarks.forEach((currentMark, index) => { - const graphicAttribute = currentMark.attribute as ArcAttrs; + const graphicAttribute = currentMark.attribute as IArcGraphicAttribute; const radiusRatio = this.computeLayoutOuterRadius(graphicAttribute.outerRadius, width, height); const radius = this.computeRadius(radiusRatio, width, height); const center = { x: graphicAttribute?.x ?? 0, y: graphicAttribute?.y ?? 0 };