From 49051d4cf5bbd0716b330e219fa7324b06941247 Mon Sep 17 00:00:00 2001 From: Kasmine <736929286@qq.com> Date: Mon, 9 Nov 2020 09:38:15 +0800 Subject: [PATCH] refactor: walkthrough label (#2981) Co-authored-by: xinming --- docs/api-zh/legend.md | 16 +- examples/gallery/area/demo/area4.ts | 4 + examples/pie/basic/demo/innerlabel.ts | 3 +- examples/pie/basic/demo/pie-size.ts | 2 +- src/component/labels.ts | 32 +--- src/component/update-label.ts | 68 +++++++++ src/geometry/label/base.ts | 76 +++++----- src/geometry/label/interface.ts | 69 +++------ src/geometry/label/interval.ts | 20 ++- src/geometry/label/layout/pie/distribute.ts | 3 + src/geometry/label/pie/index.ts | 18 ++- src/geometry/label/polar.ts | 31 +++- src/interface.ts | 21 ++- src/util/theme.ts | 2 +- tests/unit/geometry/label/default-cfg-spec.ts | 57 ++++++++ .../unit/geometry/label/label-offset-spec.ts | 138 ++++++++++++++++++ 16 files changed, 418 insertions(+), 142 deletions(-) create mode 100644 src/component/update-label.ts create mode 100644 tests/unit/geometry/label/default-cfg-spec.ts create mode 100644 tests/unit/geometry/label/label-offset-spec.ts diff --git a/docs/api-zh/legend.md b/docs/api-zh/legend.md index 8d02579180..3acf46cb56 100644 --- a/docs/api-zh/legend.md +++ b/docs/api-zh/legend.md @@ -64,10 +64,10 @@ _legendOption_ 配置如下: 背景框配置项。_LegendBackgroundCfg_ 配置如下: -| 参数名 | 类型 | 是否必选 | 默认值 | 描述 | -| ------- | ------------------- | -------- | ------ | -------------- | -| padding | number \| number[] | | - | 背景的留白 | -| style | [ShapeAttrs](shape) | | - | 背景样式配置项 | +| 参数名 | 类型 | 默认值 | 描述 | +| ------- | ------------------- | ------ | -------------- | +| padding | number \| number[] | - | 背景的留白 | +| style | [ShapeAttrs](shape) | - | 背景样式配置项 | ### legendOption.flipPage @@ -81,10 +81,10 @@ _legendOption_ 配置如下: 适用于 连续图例,滑块的配置项。_ContinueLegendHandlerCfg_ 配置如下: -| 参数名 | 类型 | 是否必选 | 默认值 | 描述 | -| ------ | ------------------- | -------- | ------ | -------------- | -| size | number | | - | 滑块的大小 | -| style | [ShapeAttrs](shape) | | - | 滑块的样式设置 | +| 参数名 | 类型 | 默认值 | 描述 | +| ------ | ------------------- | ------ | -------------- | +| size | number | - | 滑块的大小 | +| style | [ShapeAttrs](shape) | - | 滑块的样式设置 | ### legendOption.itemHeight diff --git a/examples/gallery/area/demo/area4.ts b/examples/gallery/area/demo/area4.ts index 2b9f61e37a..e29ed2e8b0 100644 --- a/examples/gallery/area/demo/area4.ts +++ b/examples/gallery/area/demo/area4.ts @@ -36,6 +36,7 @@ fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/gas-import-export.js position: [2040, 6.3], content: '出口至墨西哥', style: { + fill: '#eee', fontWeight: 300, textAlign: 'end', textBaseline: 'center' @@ -47,6 +48,7 @@ fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/gas-import-export.js position: [2040, 5], content: '出口至加拿大', style: { + fill: '#eee', fontWeight: 300, textAlign: 'end', textBaseline: 'center' @@ -59,6 +61,7 @@ fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/gas-import-export.js position: [2040, 2], content: '来自40个州的液化天然气出口', style: { + fill: '#eee', fontWeight: 300, textAlign: 'end', textBaseline: 'center' @@ -70,6 +73,7 @@ fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/gas-import-export.js position: [2015, -1.5], content: '从加拿大进口', style: { + fill: '#eee', fontWeight: 300, textAlign: 'start', textBaseline: 'center' diff --git a/examples/pie/basic/demo/innerlabel.ts b/examples/pie/basic/demo/innerlabel.ts index 867f82a65e..6460780639 100644 --- a/examples/pie/basic/demo/innerlabel.ts +++ b/examples/pie/basic/demo/innerlabel.ts @@ -38,13 +38,12 @@ chart .position('percent') .color('item') .label('percent', { - offset: -40, + offset: '-30%', style: { textAlign: 'center', fontSize: 16, shadowBlur: 2, shadowColor: 'rgba(0, 0, 0, .45)', - fill: '#fff', }, }) .tooltip('item*percent', (item, percent) => { diff --git a/examples/pie/basic/demo/pie-size.ts b/examples/pie/basic/demo/pie-size.ts index d8adf60484..132961be08 100644 --- a/examples/pie/basic/demo/pie-size.ts +++ b/examples/pie/basic/demo/pie-size.ts @@ -64,7 +64,7 @@ chart .color('type') .shape('slice-shape') .label('type', { - offset: -130, + offset: '-70%', layout: { type: 'limit-in-shape', }, diff --git a/src/component/labels.ts b/src/component/labels.ts index ac83a63779..80b56c376c 100644 --- a/src/component/labels.ts +++ b/src/component/labels.ts @@ -5,9 +5,10 @@ import { AnimateOption, GeometryLabelLayoutCfg } from '../interface'; import { doAnimate } from '../animate'; import { getGeometryLabelLayout } from '../geometry/label'; import { getlLabelBackgroundInfo } from '../geometry/label/util'; -import { getReplaceAttrs, polarToCartesian } from '../util/graphics'; +import { polarToCartesian } from '../util/graphics'; import { rotate, translate } from '../util/transform'; import { FIELD_ORIGIN } from '../constant'; +import { updateLabel } from './update-label'; /** * Labels 实例创建时,传入构造函数的参数定义 @@ -81,31 +82,14 @@ export default class Labels { const data = shape.get('data'); const origin = shape.get('origin'); const coordinate = shape.get('coordinate'); - const currentShape = lastShapesMap[id]; // 已经在渲染树上的 shape const currentAnimateCfg = shape.get('animateCfg'); - currentShape.set('data', data); - currentShape.set('origin', origin); - currentShape.set('animateCfg', currentAnimateCfg); - currentShape.set('coordinate', coordinate); - - const updateAnimateCfg = get(currentAnimateCfg, 'update'); - const currentChildren = currentShape.getChildren(); - shape.getChildren().map((child, index) => { - const currentChild = currentChildren[index] as IShape; - currentChild.set('data', data); - currentChild.set('origin', origin); - currentChild.set('animateCfg', currentAnimateCfg); - currentChild.set('coordinate', coordinate); - const newAttrs = getReplaceAttrs(currentChild, child); - if (updateAnimateCfg) { - doAnimate(currentChild, updateAnimateCfg, { - toAttrs: newAttrs, - coordinate, - }); - } else { - currentChild.attr(newAttrs); - } + const currentShape = lastShapesMap[id]; // 已经在渲染树上的 shape + updateLabel(currentShape, shapesMap[id], { + data, + origin, + animateCfg: currentAnimateCfg, + coordinate, }); this.shapesMap[id] = currentShape; // 保存引用 diff --git a/src/component/update-label.ts b/src/component/update-label.ts new file mode 100644 index 0000000000..6972de0926 --- /dev/null +++ b/src/component/update-label.ts @@ -0,0 +1,68 @@ +import { Coordinate } from '@antv/coord'; +import { IGroup, IShape } from '@antv/g-base'; +import { each, get } from '@antv/util'; +import { doAnimate } from '../animate'; +import { getReplaceAttrs } from '../util/graphics'; + +/** label 的必要配置 */ +type Cfg = { + data: any; + origin: any; + animateCfg: any; + coordinate: Coordinate; +}; + +/** + * @desc 更新 label (目前没有根据 id 索引,还是会存在一点小问题的,只能根据 idx 索引) + * @done shape 属性更新 + * @done shape delete + * @done shape append + * + * @param fromShape old labelShape + * @param toShape new labelShape + * @param cfg + */ +export function updateLabel(fromShape: IGroup, toShape: IGroup, cfg: Cfg): void { + const { data, origin, animateCfg, coordinate } = cfg; + const updateAnimateCfg = get(animateCfg, 'update'); + + fromShape.set('data', data); + fromShape.set('origin', origin); + fromShape.set('animateCfg', animateCfg); + fromShape.set('coordinate', coordinate); + + fromShape.getChildren().forEach((fromChild, idx) => { + const toChild = toShape.getChildByIndex(idx) as IShape; + if (!toChild) { + fromShape.removeChild(fromChild); + fromChild.remove(true); + } else { + fromChild.set('data', data); + fromChild.set('origin', origin); + fromChild.set('animateCfg', animateCfg); + fromChild.set('coordinate', coordinate); + + const newAttrs = getReplaceAttrs(fromChild as IShape, toChild); + if (updateAnimateCfg) { + doAnimate(fromChild as IShape, updateAnimateCfg, { + toAttrs: newAttrs, + coordinate, + }); + } else { + fromChild.attr(newAttrs); + } + if (toChild.isGroup()) { + updateLabel(fromChild as any, toChild as any, cfg); + } + } + }); + + // append + each(toShape.getChildren(), (child, idx) => { + if (idx >= fromShape.getCount()) { + if (!child.destroyed) { + fromShape.add(child); + } + } + }); +} diff --git a/src/geometry/label/base.ts b/src/geometry/label/base.ts index 7edc3a1357..93bd1482e6 100644 --- a/src/geometry/label/base.ts +++ b/src/geometry/label/base.ts @@ -1,4 +1,4 @@ -import { deepMix, each, get, isArray, isFunction, isNil, isUndefined } from '@antv/util'; +import { deepMix, each, get, isArray, isFunction, isNil, isNumber, isString, isUndefined } from '@antv/util'; import { FIELD_ORIGIN } from '../../constant'; import { Scale } from '../../dependents'; @@ -76,7 +76,6 @@ export default class GeometryLabel { public render(mapppingArray: MappingDatum[], isUpdate: boolean = false) { const labelItems = this.getLabelItems(mapppingArray); - const labelsRenderer = this.getLabelsRenderer(); const shapes = this.getGeometryShapes(); // 渲染文本 @@ -106,28 +105,20 @@ export default class GeometryLabel { /** * 获取 label 的默认配置 */ - protected getDefaultLabelCfg() { - return get(this.geometry.theme, 'labels', {}); - } - - /** - * 获取当前 label 的最终配置 - * @param labelCfg - */ - protected getThemedLabelCfg(labelCfg: LabelCfg) { + protected getDefaultLabelCfg(offset?: number, position?: string) { const geometry = this.geometry; - const defaultLabelCfg = this.getDefaultLabelCfg(); const { type, theme } = geometry; - let themedLabelCfg; - if (type === 'polygon' || (labelCfg.offset < 0 && !['line', 'point', 'path'].includes(type))) { - // polygon 或者 offset 小于 0 时,文本展示在图形内部,将其颜色设置为 白色 - themedLabelCfg = deepMix({}, defaultLabelCfg, theme.innerLabels, labelCfg); - } else { - themedLabelCfg = deepMix({}, defaultLabelCfg, theme.labels, labelCfg); + if ( + type === 'polygon' || + (type === 'interval' && position === 'middle') || + (offset < 0 && !['line', 'point', 'path'].includes(type)) + ) { + // polygon 或者 (interval 且 middle) 或者 offset 小于 0 时,文本展示在图形内部,将其颜色设置为 白色 + return get(theme, 'innerLabels', {}); } - return themedLabelCfg; + return get(theme, 'labels', {}); } /** @@ -145,25 +136,23 @@ export default class GeometryLabel { ) {} /** - * 获取文本默认偏移量 - * @param offset - * @returns + * @desc 获取 label offset */ - protected getDefaultOffset(offset: number) { + protected getLabelOffset(offset: number | string): number { const coordinate = this.getCoordinate(); const vector = this.getOffsetVector(offset); return coordinate.isTransposed ? vector[0] : vector[1]; } /** - * 获取每个 label 的偏移量 + * 获取每个 label 的偏移量 (矢量) * @param labelCfg * @param index * @param total - * @returns + * @return {Point} offsetPoint */ - protected getLabelOffset(labelCfg: LabelCfg, index: number, total: number) { - const offset = this.getDefaultOffset(labelCfg.offset); + protected getLabelOffsetPoint(labelCfg: LabelCfg, index: number, total: number): Point { + const offset = labelCfg.offset; const coordinate = this.getCoordinate(); const transposed = coordinate.isTransposed; const dim = transposed ? 'x' : 'y'; @@ -249,7 +238,7 @@ export default class GeometryLabel { // 如果 label 支持 position 属性 this.setLabelPosition(label, mappingData, index, labelCfg.position); } - const offsetPoint = this.getLabelOffset(labelCfg, index, total); + const offsetPoint = this.getLabelOffsetPoint(labelCfg, index, total); label.start = { x: label.x, y: label.y }; label.x += offsetPoint.x; label.y += offsetPoint.y; @@ -268,7 +257,7 @@ export default class GeometryLabel { let align: TextAlign = 'center'; const coordinate = this.getCoordinate(); if (coordinate.isTransposed) { - const offset = this.getDefaultOffset(item.offset); + const offset = item.offset; if (offset < 0) { align = 'right'; } else if (offset === 0) { @@ -334,8 +323,7 @@ export default class GeometryLabel { private getLabelCfgs(mapppingArray: MappingDatum[]): LabelCfg[] { const geometry = this.geometry; - const defaultLabelCfg = this.getDefaultLabelCfg(); - const { type, theme, labelOption, scales, coordinate } = geometry; + const { labelOption, scales, coordinate } = geometry; const { fields, callback, cfg } = labelOption as LabelOption; const labelScales = fields.map((field: string) => { return scales[field]; @@ -365,6 +353,18 @@ export default class GeometryLabel { ...callbackCfg, }; + if (isFunction(labelCfg.position)) { + labelCfg.position = labelCfg.position(origin, mappingData, index); + } + + const offset = this.getLabelOffset(labelCfg.offset || 0); + // defaultCfg 需要判断 innerLabels & labels + const defaultLabelCfg = this.getDefaultLabelCfg(offset, labelCfg.position); + // labelCfg priority: defaultCfg < cfg < callbackCfg + labelCfg = deepMix({}, defaultLabelCfg, labelCfg); + // 获取最终的 offset + labelCfg.offset = this.getLabelOffset(labelCfg.offset || 0); + const content = labelCfg.content; if (isFunction(content)) { labelCfg.content = content(origin, mappingData, index); @@ -373,12 +373,6 @@ export default class GeometryLabel { labelCfg.content = originText[0]; } - if (isFunction(labelCfg.position)) { - labelCfg.position = labelCfg.position(origin, mappingData, index); - } - - labelCfg = this.getThemedLabelCfg(labelCfg); - labelCfgs.push(labelCfg); }); @@ -406,10 +400,14 @@ export default class GeometryLabel { return labelTexts; } - private getOffsetVector(offset = 0) { + private getOffsetVector(offset: number | string = 0) { const coordinate = this.getCoordinate(); + let actualOffset = 0; + if (isNumber(offset)) { + actualOffset = offset; + } // 如果 x,y 翻转,则偏移 x,否则偏移 y - return coordinate.isTransposed ? coordinate.applyMatrix(offset, 0) : coordinate.applyMatrix(0, offset); + return coordinate.isTransposed ? coordinate.applyMatrix(actualOffset, 0) : coordinate.applyMatrix(0, actualOffset); } private getGeometryShapes() { diff --git a/src/geometry/label/interface.ts b/src/geometry/label/interface.ts index 341c06c827..1299ac72e4 100644 --- a/src/geometry/label/interface.ts +++ b/src/geometry/label/interface.ts @@ -1,61 +1,40 @@ import { Coordinate, ShapeAttrs } from '../../dependents'; import { Datum, GeometryLabelCfg, MappingDatum, Point } from '../../interface'; + export type TextAlign = 'start' | 'center' | 'end' | 'left' | 'right'; -export interface LabelCfg extends GeometryLabelCfg { +/** 去除 readonly 修饰 */ +export type Writeable = { -readonly [P in keyof T]: T[P] }; +export interface LabelCfg extends Omit { content?: any; - position?: 'top' | 'bottom' | 'middle' | 'left' | 'right'; - id: string; - data: Datum; - mappingData: MappingDatum; - coordinate: Coordinate; + readonly position?: 'top' | 'bottom' | 'middle' | 'left' | 'right'; + readonly offset?: number; + readonly id: string; + readonly data: Datum; + readonly mappingData: MappingDatum; + readonly coordinate: Coordinate; } export interface LabelPointCfg { + /** labelPoint.x */ x?: number; + /** labelPoint.y */ y?: number; - start?: Point; - color?: string; + readonly start?: Point; + readonly color?: string; + readonly textAlign?: TextAlign; + readonly textBaseline?: string; + readonly angle?: number; + readonly r?: number; content?: any; - textAlign?: TextAlign; - textBaseline?: string; rotate?: number; - angle?: number; - r?: number; } -export interface LabelItem extends GeometryLabelCfg { - id: string; - data: Datum; - mappingData: MappingDatum; - coordinate: Coordinate; - x?: number; - y?: number; - start?: Point; - color?: string; - content?: any; - textAlign?: TextAlign; - textBaseline?: string; - rotate?: number; - angle?: number; - r?: number; +/** + * 绘制 label 的 item + */ +export interface LabelItem extends LabelCfg, LabelPointCfg { /** 牵引线 */ labelLine?: null | boolean | { style?: object; path?: string }; - - /** - * label 背景 - */ - background?: { - /** - * 背景框 图形属性配置 - * - fill?: string; 背景框 填充色 - * - stroke?: string; 背景框 描边色 - * - lineWidth?: string; 背景框 描边宽度 - * - radius?: number | number[]; 背景框圆角,支持整数或数组形式 - */ - style?: ShapeAttrs; - /** 背景框 内边距 */ - padding?: number | number[]; - }; } /** @@ -63,7 +42,7 @@ export interface LabelItem extends GeometryLabelCfg { */ export interface PolarLabelItem extends LabelItem { /** 占比 */ - percent?: number; + readonly percent?: number; /** 是否不可见 */ invisible?: boolean; -} \ No newline at end of file +} diff --git a/src/geometry/label/interval.ts b/src/geometry/label/interval.ts index 4f4d9e595c..31c3641d99 100644 --- a/src/geometry/label/interval.ts +++ b/src/geometry/label/interval.ts @@ -1,5 +1,5 @@ import { get, deepMix, isArray } from '@antv/util'; - +import { Writeable } from 'src/util/types'; import { MappingDatum, Point } from '../../interface'; import GeometryLabel from './base'; import { LabelCfg, LabelPointCfg } from './interface'; @@ -26,15 +26,16 @@ export default class IntervalLabel extends GeometryLabel { * @param index * @param total */ - protected getLabelOffset(labelCfg: LabelCfg, index: number, total: number) { - const point = super.getLabelOffset(labelCfg, index, total); + protected getLabelOffsetPoint(labelCfg: LabelCfg, index: number, total: number) { + const point = super.getLabelOffsetPoint(labelCfg, index, total); const transposed = this.getCoordinate().isTransposed; const dim = transposed ? 'x' : 'y'; const dir = this.getLabelValueDir(labelCfg.mappingData); - point[dim] *= dir; - - return point; + return { + ...point, + [dim]: point[dim] * dir, + }; } /** @@ -50,7 +51,12 @@ export default class IntervalLabel extends GeometryLabel { return deepMix({}, defaultLabelCfg, theme.labels, labelCfg.position === 'middle' ? { offset: 0 } : {}, labelCfg); } - protected setLabelPosition(labelPointCfg: LabelPointCfg, mappingData: MappingDatum, index: number, position: string) { + protected setLabelPosition( + labelPointCfg: Writeable, + mappingData: MappingDatum, + index: number, + position: string + ) { const coordinate = this.getCoordinate(); const transposed = coordinate.isTransposed; const shapePoints = mappingData.points as Point[]; diff --git a/src/geometry/label/layout/pie/distribute.ts b/src/geometry/label/layout/pie/distribute.ts index d909293807..d35b3f9dc6 100644 --- a/src/geometry/label/layout/pie/distribute.ts +++ b/src/geometry/label/layout/pie/distribute.ts @@ -117,6 +117,9 @@ function antiCollision(labelShapes, labels, lineHeight, plotRange, center, isRig } export function distribute(items: LabelItem[], labels: IGroup[], shapes: IShape[] | IGroup[], region: BBox) { + if (!items.length || !labels.length) { + return; + } const offset = items[0] ? items[0].offset : 0; const coordinate = labels[0].get('coordinate'); const radius = coordinate.getRadius(); diff --git a/src/geometry/label/pie/index.ts b/src/geometry/label/pie/index.ts index b8710ccc05..65673b5fff 100644 --- a/src/geometry/label/pie/index.ts +++ b/src/geometry/label/pie/index.ts @@ -1,4 +1,4 @@ -import { get, isArray } from '@antv/util'; +import { deepMix, get, isArray } from '@antv/util'; import { getAngleByPoint } from '../../../util/coordinate'; import { polarToCartesian } from '../../../util/graphics'; import { LabelItem } from '../interface'; @@ -10,12 +10,14 @@ import PolarLabel from '../polar'; export default class PieLabel extends PolarLabel { public defaultLayout = 'distribute'; - protected getDefaultLabelCfg() { - return get(this.geometry.theme, 'pieLabels', {}); + protected getDefaultLabelCfg(offset?: number, position?: string) { + const cfg = super.getDefaultLabelCfg(offset, position); + return deepMix({}, cfg, get(this.geometry.theme, 'pieLabels', {})); } - protected getDefaultOffset(offset) { - return offset || 0; + /** @override */ + protected getLabelOffset(offset: string | number): number { + return super.getLabelOffset(offset) || 0; } protected getLabelRotate(angle: number, offset: number, isLabelLimit: boolean) { @@ -42,8 +44,7 @@ export default class PieLabel extends PolarLabel { } else { align = 'right'; } - const offset = this.getDefaultOffset(point.offset); - if (offset <= 0) { + if (point.offset <= 0) { if (align === 'right') { align = 'left'; } else { @@ -82,7 +83,8 @@ export default class PieLabel extends PolarLabel { return angle; } - protected getCirclePoint(angle, offset, p?) { + /** @override */ + protected getCirclePoint(angle: number, offset: number) { const coordinate = this.getCoordinate(); const center = coordinate.getCenter(); const r = coordinate.getRadius() + offset; diff --git a/src/geometry/label/polar.ts b/src/geometry/label/polar.ts index 9cbc06a21a..13759c7090 100644 --- a/src/geometry/label/polar.ts +++ b/src/geometry/label/polar.ts @@ -1,9 +1,9 @@ -import { each, get, isArray, map } from '@antv/util'; +import { each, get, isArray, map, isNumber, isString } from '@antv/util'; import { MappingDatum, Point } from '../../interface'; import { getDistanceToCenter } from '../../util/coordinate'; import { getAngleByPoint } from '../../util/coordinate'; import GeometryLabel from './base'; -import { LabelCfg, LabelItem, PolarLabelItem, LabelPointCfg } from './interface'; +import { LabelCfg, LabelItem, PolarLabelItem, LabelPointCfg, Writeable } from './interface'; const HALF_PI = Math.PI / 2; @@ -11,6 +11,27 @@ const HALF_PI = Math.PI / 2; * 极坐标下的图形 label */ export default class PolarLabel extends GeometryLabel { + /** + * @override + * @desc 获取 label offset + * polar & theta coordinate support「string」type, should transform to 「number」 + */ + protected getLabelOffset(offset: number | string): number { + const coordinate = this.getCoordinate(); + let actualOffset = 0; + if (isNumber(offset)) { + actualOffset = offset; + } else if (isString(offset) && offset.indexOf('%') !== -1) { + let r = coordinate.getRadius(); + if (coordinate.innerRadius > 0) { + r = r * (1 - coordinate.innerRadius); + } + actualOffset = parseFloat(offset) * 0.01 * r; + } + + return actualOffset; + } + /** * @override * 获取 labelItems, 增加切片 percent @@ -45,7 +66,7 @@ export default class PolarLabel extends GeometryLabel { align = 'center'; } else { const center = coordinate.getCenter(); - const offset = this.getDefaultOffset(point.offset); + const offset = point.offset; if (Math.abs(point.x - center.x) < 1) { align = 'center'; } else if (point.angle > Math.PI || point.angle <= 0) { @@ -79,10 +100,10 @@ export default class PolarLabel extends GeometryLabel { arcPoint = this.getArcPoint(mappingData, index); } - const offset = this.getDefaultOffset(labelCfg.offset) * factor; + const offset = labelCfg.offset * factor; const middleAngle = this.getPointAngle(arcPoint); const isLabelEmit = labelCfg.labelEmit; - const labelPositionCfg: LabelPointCfg = this.getCirclePoint(middleAngle, offset, arcPoint, isLabelEmit); + const labelPositionCfg: Writeable = this.getCirclePoint(middleAngle, offset, arcPoint, isLabelEmit); if (labelPositionCfg.r === 0) { // 如果文本位置位于圆心,则不展示 labelPositionCfg.content = ''; diff --git a/src/interface.ts b/src/interface.ts index 9866a24d52..7019b07495 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -219,8 +219,8 @@ export interface GeometryLabelCfg { * 当用户使用了自定义的 label 类型,需要声明具体的 type 类型,否则会使用默认的 label 类型渲染。 */ type?: string; - /** 相对数据点的偏移距离。 */ - offset?: number; + /** 相对数据点的偏移距离, polar 和 theta 坐标系下可使用百分比字符串。 */ + offset?: number | string; /** label 相对于数据点在 X 方向的偏移距离。 */ offsetX?: number; /** label 相对于数据点在 Y 方向的偏移距离。 */ @@ -239,6 +239,8 @@ export interface GeometryLabelCfg { * 当且仅当 `autoRotate` 为 false 时生效,用于设置文本的旋转角度,**弧度制**。 */ rotate?: number; + /** 标签高度设置,仅当标签类型 type 为 pie 时生效;也可在主题中设置 pieLabels.labelHeight */ + labelHeight?: number; /** * 用于设置文本连接线的样式属性,null 表示不展示。 */ @@ -261,6 +263,21 @@ export interface GeometryLabelCfg { * ``` */ layout?: GeometryLabelLayoutCfg | GeometryLabelLayoutCfg[]; + /** + * 用于绘制 label 背景 + */ + background?: { + /** + * 背景框 图形属性配置 + * - fill?: string; 背景框 填充色 + * - stroke?: string; 背景框 描边色 + * - lineWidth?: string; 背景框 描边宽度 + * - radius?: number | number[]; 背景框圆角,支持整数或数组形式 + */ + style?: ShapeAttrs; + /** 背景框 内边距 */ + padding?: number | number[]; + }; /** * 仅当 geometry 为 interval 时生效,指定当前 label 与当前图形的相对位置。 */ diff --git a/src/util/theme.ts b/src/util/theme.ts index 6b957157b7..c5f2a96a60 100644 --- a/src/util/theme.ts +++ b/src/util/theme.ts @@ -1262,7 +1262,7 @@ export function createThemeByStylesheet(styleSheet: StyleSheet): LooseObject { }, pieLabels: { labelHeight: 14, - offset: 30, + offset: 10, labelLine: { style: { lineWidth: styleSheet.labelLineBorder, diff --git a/tests/unit/geometry/label/default-cfg-spec.ts b/tests/unit/geometry/label/default-cfg-spec.ts new file mode 100644 index 0000000000..b2f064a726 --- /dev/null +++ b/tests/unit/geometry/label/default-cfg-spec.ts @@ -0,0 +1,57 @@ +import { flatten } from 'lodash'; +import { getTheme } from '../../../../src/theme/'; +import { Chart } from '../../../../src'; +import { createDiv } from '../../../util/dom'; + +const Theme = getTheme('default'); + +describe('GeometryLabel default ThemeCfg', () => { + const div = createDiv(); + const data = [ + { type: 'item1', value: 5 }, + { type: 'item2', value: 5 }, + { type: 'item3', value: 5 }, + { type: 'item4', value: 5 }, + ]; + const chart = new Chart({ + container: div, + autoFit: true, + height: 400, + }); + chart.coordinate('theta', { radius: 0.8 }); + chart.data(data); + const pie = chart.interval().position('value'); + const geometry = pie.color('type').adjust('stack').label('type'); + chart.render(); + + // @ts-ignore + const geometryLabel = geometry.geometryLabel; + + it('default use `theme.labels`, when offset >= 0', () => { + let labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].style).toMatchObject(Theme.labels.style); + + geometry.label('type', { offset: 0 }); + labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].style).toMatchObject(Theme.labels.style); + }); + + it('default use `theme.innerLabels`, when offset < 0', () => { + geometry.label('type', { offset: '-10%' }); + let labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].labelLine).toBeNull(); + expect(labelItems[0].style).toMatchObject(Theme.innerLabels.style); + + geometry.label('type', { offset: '10%' }); + labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].style).toMatchObject(Theme.labels.style); + + geometry.label('type', { offset: -40 }); + labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].style).toMatchObject(Theme.innerLabels.style); + }); + + afterAll(() => { + chart.destroy(); + }); +}); diff --git a/tests/unit/geometry/label/label-offset-spec.ts b/tests/unit/geometry/label/label-offset-spec.ts new file mode 100644 index 0000000000..4d677f38d0 --- /dev/null +++ b/tests/unit/geometry/label/label-offset-spec.ts @@ -0,0 +1,138 @@ +import { flatten } from 'lodash'; +import { Chart } from '../../../../src'; +import { createDiv } from '../../../util/dom'; + +describe('Pie GeometryLabel offset', () => { + const div = createDiv(); + const data = [ + { type: 'item1', value: 5 }, + { type: 'item2', value: 5 }, + { type: 'item3', value: 5 }, + { type: 'item4', value: 5 }, + ]; + const chart = new Chart({ + container: div, + autoFit: true, + height: 400, + }); + chart.coordinate('theta', { radius: 0.8 }); + chart.data(data); + const pie = chart.interval().position('value'); + const geometry = pie.color('type').adjust('stack').label('type'); + chart.render(); + + // @ts-ignore + const geometryLabel = geometry.geometryLabel; + + it('default label layout', () => { + expect(geometryLabel.defaultLayout).toBe('distribute'); + }); + + it('no offset declaration', () => { + const labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems.length).toBe(data.length); + expect(labelItems[0].offset).not.toBeLessThanOrEqual(0); + // @ts-ignore + expect(labelItems[0].labelLine).toEqual(geometryLabel.getDefaultLabelCfg().labelLine); + }); + + it('declare "offset" specific number', () => { + geometry.label('type', { offset: -10 }); + let labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].offset).toBe(-10); + + geometry.label('type', { offset: 10 }); + labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].offset).toBe(10); + }); + + it('declare "offset" percentage', () => { + geometry.label('type', { offset: '-10%' }); + let labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].labelLine).toBeNull(); + expect(labelItems[0].offset).toBe(chart.getCoordinate().getRadius() * -0.1); + + geometry.label('type', { offset: '10%' }); + labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].labelLine).not.toBeNull(); + expect(labelItems[0].offset).toBe(chart.getCoordinate().getRadius() * 0.1); + }); + + it('labelLine not to be shown, when offset <= 0', () => { + geometry.label('type', { offset: '-10%' }); + let labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].labelLine).toBeNull(); + expect(labelItems[0].offset).toBe(chart.getCoordinate().getRadius() * -0.1); + chart.render(); + expect(geometryLabel.labelsRenderer.shapesMap[labelItems[0].id].getChildren().length).toBe(1); + + geometry.label('type', { offset: 10 }); + labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + chart.render(); + expect(geometryLabel.labelsRenderer.shapesMap[labelItems[0].id].getChildren().length).toBe(2); + }); + + it('declare "offset" percentage, with innerRadius', () => { + chart.coordinate('polar', { radius: 0.9, innerRadius: 0.6 }); + geometry.label('type', { offset: '-50%' }); + chart.render(); + const coordinate = chart.getCoordinate(); + + let labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].offset).not.toEqual(coordinate.getRadius() * -0.5); + expect(labelItems[0].offset).toBeCloseTo(coordinate.getRadius() * (1 - 0.6) * -0.5); + + geometry.label('type', { offset: -10 }); + labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].offset).toEqual(-10); + + geometry.label('type', { offset: 10 }); + labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].offset).toEqual(10); + + geometry.label('type', { offset: '10%' }); + labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].offset).not.toEqual(coordinate.getRadius() * 0.1); + expect(labelItems[0].offset).toBeCloseTo((coordinate.getRadius() * (1 - 0.6) * 0.1)); + }); + + afterAll(() => { + chart.destroy(); + }); +}); + +describe('Interval GeometryLabel offset', () => { + const div = createDiv(); + const data = [ + { type: 'item1', value: 5 }, + { type: 'item2', value: 5 }, + { type: 'item3', value: 5 }, + { type: 'item4', value: 5 }, + ]; + const chart = new Chart({ + container: div, + autoFit: true, + height: 400, + }); + chart.data(data); + const interval = chart.interval().position('type*value'); + const geometry = interval.label('type', { offset: -10 }); + chart.render(); + + // @ts-ignore + const geometryLabel = geometry.geometryLabel; + + it('offset < 0', () => { + const labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].labelLine).toBeNull(); + expect(labelItems[0].offset).toBe(-10); + chart.render(); + expect(geometryLabel.labelsRenderer.shapesMap[labelItems[0].id].getChildren().length).toBe(1); + }); + + it('declare "offset" percentage', () => { + geometry.label('type', { offset: '-10%' }); + const labelItems = geometryLabel.getLabelItems(flatten(geometry.dataArray)); + expect(labelItems[0].offset).toBe(0); + }); +});