@antv/g2
Version:
the Grammar of Graphics in Javascript
331 lines (304 loc) • 10.9 kB
text/typescript
import { deepMix, each, get, isArray, isNull } from '@antv/util';
import { BBox, Coordinate, IGroup, IShape } from '../dependents';
import { LabelItem } from '../geometry/label/interface';
import { AnimateOption, GeometryLabelLayoutCfg } from '../interface';
import { doAnimate } from '../animate';
import { getGeometryLabelLayout } from '../geometry/label';
import { getLabelBackgroundInfo } from '../geometry/label/util';
import { polarToCartesian } from '../util/graphics';
import { rotate, translate } from '../util/transform';
import { FIELD_ORIGIN } from '../constant';
import { updateLabel } from './update-label';
/**
* Labels 实例创建时,传入构造函数的参数定义
*/
export interface LabelsGroupCfg {
/** label 容器 */
container: IGroup;
/** label 布局配置 */
layout?: GeometryLabelLayoutCfg | GeometryLabelLayoutCfg[];
}
/**
* Geometry labels 渲染组件
*/
export default class Labels {
/** 用于指定 labels 布局的类型 */
public layout: GeometryLabelLayoutCfg | GeometryLabelLayoutCfg[];
/** 图形容器 */
public container: IGroup;
/** 动画配置 */
public animate: AnimateOption | false;
/** label 绘制的区域 */
public region: BBox;
/** 存储当前 shape 的映射表,键值为 shape id */
public shapesMap: Record<string, IGroup> = {};
constructor(cfg: LabelsGroupCfg) {
const { layout, container } = cfg;
this.layout = layout;
this.container = container;
}
/**
* 渲染文本
*/
public async render(items: LabelItem[], shapes: Record<string, IShape | IGroup>, isUpdate: boolean = false) {
const shapesMap = {};
const offscreenGroup = this.createOffscreenGroup(); // 创建虚拟分组
if (items.length) {
// 如果 items 空的话就不进行绘制调整操作
// step 1: 在虚拟 group 中创建 shapes
for (const item of items) {
if (item) {
shapesMap[item.id] = this.renderLabel(item, offscreenGroup);
}
}
// [todo] Move layout into Worker.
// step 2: 根据布局,调整 labels
await this.doLayout(items, shapes, shapesMap);
// step 3.1: 绘制 labelLine
this.renderLabelLine(items, shapesMap);
// step 3.2: 绘制 labelBackground
this.renderLabelBackground(items, shapesMap);
// step 4: 根据用户设置的偏移量调整 label
this.adjustLabel(items, shapesMap);
}
// 进行添加、更新、销毁操作
const lastShapesMap = this.shapesMap;
each(shapesMap, (shape, id) => {
if (shape.destroyed) {
// label 在布局调整环节被删除了(doLayout)
delete shapesMap[id];
} else {
if (lastShapesMap[id]) {
// 图形发生更新
const data = shape.get('data');
const origin = shape.get('origin');
const coordinate = shape.get('coordinate');
const currentAnimateCfg = shape.get('animateCfg');
const currentShape = lastShapesMap[id]; // 已经在渲染树上的 shape
updateLabel(currentShape, shapesMap[id], {
data,
origin,
animateCfg: currentAnimateCfg,
coordinate,
});
shapesMap[id] = currentShape; // 保存引用
} else {
// 新生成的 shape
// If container has been destroyed, no need to render labels.
if (this.container.destroyed) return;
this.container.add(shape);
const animateCfg = get(shape.get('animateCfg'), isUpdate ? 'enter' : 'appear');
if (animateCfg) {
doAnimate(shape, animateCfg, {
toAttrs: {
...shape.attr(),
},
coordinate: shape.get('coordinate'),
});
}
}
delete lastShapesMap[id];
}
});
// 移除
each(lastShapesMap, (deleteShape) => {
const animateCfg = get(deleteShape.get('animateCfg'), 'leave');
if (animateCfg) {
doAnimate(deleteShape, animateCfg, {
toAttrs: null,
coordinate: deleteShape.get('coordinate'),
});
} else {
deleteShape.remove(true); // 移除
}
});
this.shapesMap = shapesMap;
offscreenGroup.destroy();
}
/** 清除当前 labels */
public clear() {
this.container.clear();
this.shapesMap = {};
}
/** 销毁 */
public destroy() {
this.container.destroy();
this.shapesMap = null;
}
private renderLabel(cfg: LabelItem, container: IGroup): IGroup {
const { id, elementId, data, mappingData, coordinate, animate, content } = cfg;
const shapeAppendCfg = {
id,
elementId,
data,
origin: {
...mappingData,
data: mappingData[FIELD_ORIGIN],
},
coordinate,
};
const labelGroup = container.addGroup({
name: 'label',
// 如果 this.animate === false 或者 cfg.animate === false/null 则不进行动画,否则进行动画配置的合并
animateCfg:
this.animate === false || animate === null || animate === false ? false : deepMix({}, this.animate, animate),
...shapeAppendCfg,
});
let labelShape;
if ((content.isGroup && content.isGroup()) || (content.isShape && content.isShape())) {
// 如果 content 是 Group 或者 Shape,根据 textAlign 调整位置后,直接将其加入 labelGroup
const { width, height } = content.getCanvasBBox();
const textAlign = get(cfg, 'textAlign', 'left');
let x = cfg.x;
const y = cfg.y - height / 2;
if (textAlign === 'center') {
x = x - width / 2;
} else if (textAlign === 'right' || textAlign === 'end') {
x = x - width;
}
translate(content, x, y); // 将 label 平移至 x, y 指定的位置
labelShape = content;
labelGroup.add(content);
} else {
const fill = get(cfg, ['style', 'fill']);
labelShape = labelGroup.addShape('text', {
attrs: {
x: cfg.x,
y: cfg.y,
textAlign: cfg.textAlign,
textBaseline: get(cfg, 'textBaseline', 'middle'),
text: cfg.content,
...cfg.style,
fill: isNull(fill) ? cfg.color : fill,
},
...shapeAppendCfg,
});
}
if (cfg.rotate) {
rotate(labelShape, cfg.rotate);
}
return labelGroup;
}
// 根据type对label布局
private async doLayout(items: LabelItem[], shapes: Record<string, IShape | IGroup>, shapesMap: Record<string, IGroup>) {
if (this.layout) {
const layouts = isArray(this.layout) ? this.layout : [this.layout];
await Promise.all(layouts.map((layout: GeometryLabelLayoutCfg) => {
const layoutFn = getGeometryLabelLayout(get(layout, 'type', ''));
if (!layoutFn) return;
const labelShapes = [];
const geometryShapes = [];
each(shapesMap, (labelShape, id) => {
labelShapes.push(labelShape);
geometryShapes.push(shapes[labelShape.get('elementId')]);
});
// [todo] Refactor more layout into Worker.
return layoutFn(items, labelShapes, geometryShapes, this.region, layout.cfg);
}));
}
}
private renderLabelLine(labelItems: LabelItem[], shapesMap) {
each(labelItems, (labelItem) => {
const coordinate: Coordinate = get(labelItem, 'coordinate');
if (!labelItem || !coordinate) {
return;
}
const center = coordinate.getCenter();
const radius = coordinate.getRadius();
if (!labelItem.labelLine) {
// labelLine: null | false,关闭 label 对应的 labelLine
return;
}
const labelLineCfg = get(labelItem, 'labelLine', {});
const id = labelItem.id;
let path = labelLineCfg.path;
if (!path) {
const start = polarToCartesian(center.x, center.y, radius, labelItem.angle);
path = [
['M', start.x, start.y],
['L', labelItem.x, labelItem.y],
];
}
const labelGroup = shapesMap[id];
if (!labelGroup.destroyed) {
labelGroup.addShape('path', {
capture: false, // labelLine 默认不参与事件捕获
attrs: {
path,
stroke: labelItem.color ? labelItem.color : get(labelItem, ['style', 'fill'], '#000'),
fill: null,
...labelLineCfg.style,
},
id,
origin: labelItem.mappingData,
data: labelItem.data,
coordinate: labelItem.coordinate,
});
}
});
}
/**
* 绘制标签背景
* @param labelItems
*/
private renderLabelBackground(labelItems: LabelItem[], shapesMap) {
each(labelItems, (labelItem) => {
const coordinate: Coordinate = get(labelItem, 'coordinate');
const background: LabelItem['background'] = get(labelItem, 'background');
if (!background || !coordinate) {
return;
}
const id = labelItem.id;
const labelGroup = shapesMap[id];
if (!labelGroup.destroyed) {
const labelContentShape = labelGroup.getChildren()[0];
if (labelContentShape) {
const { rotation, ...box } = getLabelBackgroundInfo(labelGroup, labelItem, background.padding);
const backgroundShape = labelGroup.addShape('rect', {
attrs: {
...box,
...(background.style || {}),
},
id,
origin: labelItem.mappingData,
data: labelItem.data,
coordinate: labelItem.coordinate,
});
backgroundShape.setZIndex(-1);
if (rotation) {
const matrix = labelContentShape.getMatrix();
backgroundShape.setMatrix(matrix);
}
}
}
});
}
private createOffscreenGroup() {
const container = this.container;
const GroupClass = container.getGroupBase(); // 获取分组的构造函数
const newGroup = new GroupClass({});
return newGroup;
}
private adjustLabel(items: LabelItem[], shapesMap) {
each(items, (item) => {
if (item) {
const id = item.id;
const labelGroup = shapesMap[id];
if (!labelGroup.destroyed) {
// fix: 如果说开发者的 label content 是一个 group,此处的偏移无法对 整个 content group 生效;场景类似 饼图 spider label 是一个含 2 个 textShape 的 gorup
const labelShapes = labelGroup.findAll((ele) => ele.get('type') !== 'path');
each(labelShapes, (labelShape) => {
if (labelShape) {
if (item.offsetX) {
labelShape.attr('x', labelShape.attr('x') + item.offsetX);
}
if (item.offsetY) {
labelShape.attr('y', labelShape.attr('y') + item.offsetY);
}
}
});
}
}
});
}
}