import { deepMix, each, get, isArray, isFunction, isNil, isNumber, isString, isUndefined } from '@antv/util'; import { FIELD_ORIGIN } from '../../constant'; import { Scale } from '../../dependents'; import { Datum, LabelOption, MappingDatum, Point } from '../../interface'; import { LabelCfg, LabelItem, LabelPointCfg, TextAlign } from './interface'; import { getDefaultAnimateCfg } from '../../animate'; import { getPolygonCentroid } from '../../util/graphics'; import Labels from '../../component/labels'; import Geometry from '../base'; import Element from '../element'; export type GeometryLabelConstructor = new (cfg: any) => GeometryLabel; function avg(arr: number[]) { let sum = 0; each(arr, (value: number) => { sum += value; }); return sum / arr.length; } /** * Geometry Label 基类,用于生成 Geometry 下所有 label 的配置项信息 */ export default class GeometryLabel { /** geometry 实例 */ public readonly geometry: Geometry; public labelsRenderer: Labels; /** 默认的布局 */ public defaultLayout: string; constructor(geometry: Geometry) { this.geometry = geometry; } public getLabelItems(mapppingArray: MappingDatum[]): LabelItem[] { const items = []; const labelCfgs = this.getLabelCfgs(mapppingArray); // 获取 label 相关的 x,y 的值,获取具体的 x, y,防止存在数组 each(mapppingArray, (mappingData: MappingDatum, index: number) => { const labelCfg = labelCfgs[index]; if (!labelCfg || isNil(mappingData.x) || isNil(mappingData.y)) { items.push(null); return; } const labelContent = !isArray(labelCfg.content) ? [labelCfg.content] : labelCfg.content; labelCfg.content = labelContent; const total = labelContent.length; each(labelContent, (content, subIndex) => { if (isNil(content) || content === '') { items.push(null); return; } const item = { ...labelCfg, ...this.getLabelPoint(labelCfg, mappingData, subIndex), }; if (!item.textAlign) { item.textAlign = this.getLabelAlign(item, subIndex, total); } if (item.offset <= 0) { item.labelLine = null; } items.push(item); }); }); return items; } public async render(mappingArray: MappingDatum[], isUpdate: boolean = false) { const labelItems = this.getLabelItems(mappingArray); const labelsRenderer = this.getLabelsRenderer(); const shapes = this.getGeometryShapes(); // 渲染文本 await labelsRenderer.render(labelItems, shapes, isUpdate); } public clear() { const labelsRenderer = this.labelsRenderer; if (labelsRenderer) { labelsRenderer.clear(); } } public destroy() { const labelsRenderer = this.labelsRenderer; if (labelsRenderer) { labelsRenderer.destroy(); } this.labelsRenderer = null; } // geometry 更新之后,对应的 Coordinate 也会更新,为了获取到最新鲜的 Coordinate,故使用方法获取 public getCoordinate() { return this.geometry.coordinate; } /** * 获取 label 的默认配置 */ protected getDefaultLabelCfg(offset?: number, position?: string) { const geometry = this.geometry; const { type, theme } = geometry; if ( type === 'polygon' || (type === 'interval' && position === 'middle') || (offset < 0 && !['line', 'point', 'path'].includes(type)) ) { // polygon 或者 (interval 且 middle) 或者 offset 小于 0 时,文本展示在图形内部,将其颜色设置为 白色 return get(theme, 'innerLabels', {}); } return get(theme, 'labels', {}); } /** * 获取当前 label 的最终配置 * @param labelCfg */ protected getThemedLabelCfg(labelCfg: LabelCfg) { 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); } return themedLabelCfg; } /** * 设置 label 位置 * @param labelPointCfg * @param mappingData * @param index * @param position */ protected setLabelPosition( labelPointCfg: LabelPointCfg, mappingData: MappingDatum, index: number, position: string ) {} /** * @desc 获取 label offset */ protected getLabelOffset(offset: number | string): number { const coordinate = this.getCoordinate(); const vector = this.getOffsetVector(offset); return coordinate.isTransposed ? vector[0] : vector[1]; } /** * 获取每个 label 的偏移量 (矢量) * @param labelCfg * @param index * @param total * @return {Point} offsetPoint */ 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'; const factor = transposed ? 1 : -1; // y 方向上越大,像素的坐标越小,所以transposed时将系数变成 const offsetPoint = { x: 0, y: 0, }; if (index > 0 || total === 1) { // 判断是否小于0 offsetPoint[dim] = offset * factor; } else { offsetPoint[dim] = offset * factor * -1; } return offsetPoint; } /** * 获取每个 label 的位置 * @param labelCfg * @param mappingData * @param index * @returns label point */ protected getLabelPoint(labelCfg: LabelCfg, mappingData: MappingDatum, index: number): LabelPointCfg { const coordinate = this.getCoordinate(); const total = labelCfg.content.length; function getDimValue(value: number | number[], idx: number, isAvg = false) { let v = value; if (isArray(v)) { if (labelCfg.content.length === 1) { if (isAvg) { v = avg(v); } else { // 如果仅一个 label,多个 y, 取最后一个 y if (v.length <= 2) { v = v[(value as number[]).length - 1]; } else { v = avg(v); } } } else { v = v[idx]; } } return v; } const label = { content: labelCfg.content[index], x: 0, y: 0, start: { x: 0, y: 0 }, color: '#fff', }; const shape = isArray(mappingData.shape) ? mappingData.shape[0] : mappingData.shape; const isFunnel = shape === 'funnel' || shape === 'pyramid'; // 多边形场景,多用于地图 if (this.geometry.type === 'polygon') { const centroid = getPolygonCentroid(mappingData.x, mappingData.y); label.x = centroid[0]; label.y = centroid[1]; } else if (this.geometry.type === 'interval' && !isFunnel) { // 对直方图的label X 方向的位置居中 label.x = getDimValue(mappingData.x, index, true); label.y = getDimValue(mappingData.y, index); } else { label.x = getDimValue(mappingData.x, index); label.y = getDimValue(mappingData.y, index); } // 处理漏斗图文本位置 if (isFunnel) { const nextPoints = get(mappingData, 'nextPoints'); const points = get(mappingData, 'points'); if (nextPoints) { // 非漏斗图底部 const point1 = coordinate.convert(points[1] as Point); const point2 = coordinate.convert(nextPoints[1] as Point); label.x = (point1.x + point2.x) / 2; label.y = (point1.y + point2.y) / 2; } else if (shape === 'pyramid') { const point1 = coordinate.convert(points[1] as Point); const point2 = coordinate.convert(points[2] as Point); label.x = (point1.x + point2.x) / 2; label.y = (point1.y + point2.y) / 2; } } if (labelCfg.position) { // 如果 label 支持 position 属性 this.setLabelPosition(label, mappingData, index, labelCfg.position); } const offsetPoint = this.getLabelOffsetPoint(labelCfg, index, total); label.start = { x: label.x, y: label.y }; label.x += offsetPoint.x; label.y += offsetPoint.y; label.color = mappingData.color; return label; } /** * 获取文本的对齐方式 * @param item * @param index * @param total * @returns */ protected getLabelAlign(item: LabelItem, index: number, total: number): TextAlign { let align: TextAlign = 'center'; const coordinate = this.getCoordinate(); if (coordinate.isTransposed) { const offset = item.offset; if (offset < 0) { align = 'right'; } else if (offset === 0) { align = 'center'; } else { align = 'left'; } if (total > 1 && index === 0) { if (align === 'right') { align = 'left'; } else if (align === 'left') { align = 'right'; } } } return align; } /** * 获取每一个 label 的唯一 id * @param mappingData label 对应的图形的绘制数据 */ protected getLabelId(mappingData: MappingDatum) { const geometry = this.geometry; const type = geometry.type; const xScale = geometry.getXScale(); const yScale = geometry.getYScale(); const origin = mappingData[FIELD_ORIGIN]; // 原始数据 let labelId = geometry.getElementId(mappingData); if (type === 'line' || type === 'area') { // 折线图以及区域图,一条线会对应一组数据,即多个 labels,为了区分这些 labels,需要在 line id 的前提下加上 x 字段值 labelId += ` ${origin[xScale.field]}`; } else if (type === 'path') { // path 路径图,无序,有可能存在相同 x 不同 y 的情况,需要通过 x y 来确定唯一 id labelId += ` ${origin[xScale.field]}-${origin[yScale.field]}`; } return labelId; } // 获取 labels 组件 private getLabelsRenderer() { const { labelsContainer, labelOption, canvasRegion, animateOption } = this.geometry; const coordinate = this.geometry.coordinate; let labelsRenderer = this.labelsRenderer; if (!labelsRenderer) { labelsRenderer = new Labels({ container: labelsContainer, layout: get(labelOption, ['cfg', 'layout'], { type: this.defaultLayout, }), }); this.labelsRenderer = labelsRenderer; } labelsRenderer.region = canvasRegion; // 设置动画配置,如果 geometry 的动画关闭了,那么 label 的动画也会关闭 labelsRenderer.animate = animateOption ? getDefaultAnimateCfg('label', coordinate) : false; return labelsRenderer; } private getLabelCfgs(mapppingArray: MappingDatum[]): LabelCfg[] { const geometry = this.geometry; const { labelOption, scales, coordinate } = geometry; const { fields, callback, cfg } = labelOption as LabelOption; const labelScales = fields.map((field: string) => { return scales[field]; }); const labelCfgs: LabelCfg[] = []; each(mapppingArray, (mappingData: MappingDatum, index: number) => { const origin = mappingData[FIELD_ORIGIN]; // 原始数据 const originText = this.getLabelText(origin, labelScales); let callbackCfg; if (callback) { // 当同时配置了 callback 和 cfg 时,以 callback 为准 const originValues = fields.map((field: string) => origin[field]); callbackCfg = callback(...originValues); if (isNil(callbackCfg)) { labelCfgs.push(null); return; } } let labelCfg = { id: this.getLabelId(mappingData), // 进行 ID 标记 elementId: this.geometry.getElementId(mappingData), // label 对应 Element 的 ID data: origin, // 存储原始数据 mappingData, // 存储映射后的数据, coordinate, // 坐标系 ...cfg, ...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); } else if (isUndefined(content)) { // 用户未配置 content,则默认为映射的第一个字段的值 labelCfg.content = originText[0]; } labelCfgs.push(labelCfg); }); return labelCfgs; } private getLabelText(origin: Datum, scales: Scale[]) { const labelTexts = []; each(scales, (scale: Scale) => { let value = origin[scale.field]; if (isArray(value)) { value = value.map((subVal) => { return scale.getText(subVal); }); } else { value = scale.getText(value); } if (isNil(value) || value === '') { labelTexts.push(null); } else { labelTexts.push(value); } }); return labelTexts; } 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(actualOffset, 0) : coordinate.applyMatrix(0, actualOffset); } private getGeometryShapes() { const geometry = this.geometry; const shapes = {}; each(geometry.elementsMap, (element: Element, id: string) => { shapes[id] = element.shape; }); // 因为有可能 shape 还在进行动画,导致 shape.getBBox() 获取到的值不是最终态,所以需要从 offscreenGroup 获取 each(geometry.getOffscreenGroup().getChildren(), (child) => { const id = geometry.getElementId(child.get('origin').mappingData); shapes[id] = child; }); return shapes; } }