复合 Shape 的设计与实现
上一篇
原子 Shape 以及其属性
下一篇
布局总览
Loading...
G6 提供了灵活的 Shape 机制,支持开发者自定义各种图形,并在节点、边、Combo 等元素中高效复用。本文将以 Label(标签)为例,讲解如何自定义 Shape、如何在元素中应用。
所有 Shape 都继承自 BaseShape
,它统一管理了 Shape 的生命周期(创建、更新、销毁)、属性解析、动画、事件绑定等。你只需关注如何实现 render
方法即可。
核心抽象:
import { CustomElement } from '@antv/g';abstract class BaseShape extends CustomElement {// 生命周期管理、属性解析、动画等...public abstract render(attributes, container): void;}
一个节点通常包含多个子 Shape,例如:
节点(Node)├── keyShape(主图形)├── label(标签,辅助信息)│ ├── text(文本)│ └── rect(背景)├── icon(图标)│ ├── text(文本)│ └── image(图片)├── badge(徽标)│ ├── text(文本)│ └── rect(背景)└── port(锚点)│ ├── circle(圆形)
Label 是一个典型的复合 Shape,由文本(Text)和可选的背景(Rect)组成。其实现思路如下:
upsert
方法自动管理子 Shape 的创建、更新、销毁。Label 主要代码片段:
import { Text, Rect } from '@antv/g'; // 引入原子图形export class Label extends BaseShape {public render(attributes = this.parsedAttributes, container= this): void {this.upsert('text', Text, this.getTextStyle(attributes), container);this.upsert('background', Rect, this.getBackgroundStyle(attributes), container);}// ... 省略样式提取方法}
getTextStyle
、getBackgroundStyle
分别提取文本和背景的样式属性,避免相互干扰。upsert
方法保证了 Shape 的自动增删改查,极大提升了复用性和健壮性。下面以自定义一个带特殊装饰的标签为例,演示 Shape 的完整定义、注册与使用:
import { BaseShape, ExtensionCategory, Circle } from 'g6';import { Text, Rect, Circle } from '@antv/g';class FancyLabel extends BaseShape {render(attributes = this.parsedAttributes, container = this) {// 主文本this.upsert('text', Text, this.getTextStyle(attributes), container);// 背景this.upsert('background', Rect, this.getBackgroundStyle(attributes), container);// 额外装饰:左侧小圆点this.upsert('dot', Circle, {x: -8, y: 0, r: 3, fill: '#faad14',}, container);}// ...实现 getTextStyle/getBackgroundStyle}// 注册自定义 Shaperegister(ExtensionCategory.SHAPE, 'fancy-label-shape', FancyLabel);// 定义自定义节点class CustomCircle extends Circle {public drawFancyLabelShape(attributes, container) {this.upsert('fancy-label', 'fancy-label-shape', this.getFancyLabelStyle(attributes), container);}render(attributes = this.parsedAttributes, container) {super.render(attributes, container);this.drawFancyLabelShape(attributes, container);}}// 注册自定义节点register(ExtensionCategory.Node, 'fancy-label-node', CustomCircle);
G6 中节点、边、Combo 等元素往往包含多个子 Shape(如主图形、标签、徽标、锚点等)。为了让每个子 Shape 的样式互不干扰,G6 采用了样式属性前缀分离的设计。
以 Label 为例:
import { RectStyleProps, TextStyleProps } from '@antv/g';type PrefixKey<P extends string = string, K extends string = string> = `${P}${Capitalize<K>}`;type Prefix<P extends string, T extends object> = {[K in keyof T as K extends string ? PrefixKey<P, K> : never]?: T[K];};interface LabelStyleProps extends TextStyleProps, Prefix<'background', RectStyleProps> {background?: boolean;}
Prefix<'background', RectStyleProps>
表示所有以 background
开头的属性都属于标签背景样式。subStyleProps
、subObject
等工具函数,自动提取带前缀的样式,传递给对应的 Shape。Label 背景样式提取示例
protected getBackgroundStyle(attributes: Required<LabelStyleProps>) {if (attributes.background === false) return false;const style = this.getGraphicStyle(attributes);const backgroundStyle = subStyleProps<RectStyleProps>(style, 'background');// ...省略布局计算return backgroundStyle;}
样式配置示例
{"text": "label","fontSize": 12,"fontFamily": "system-ui, sans-serif","wordWrap": true,"maxLines": 1,"wordWrapWidth": 128,"textOverflow": "...","textBaseline": "middle","background": true,"backgroundOpacity": 0.75,"backgroundZIndex": -1,"backgroundLineWidth": 0}
drawKeyShape
或类似方法指定 keyShape,Label 只负责展示文本信息,不影响节点的交互主控。以节点为例,节点基类 BaseNode
已经内置了对多种子 Shape 的支持(keyShape、label、icon、badge、port、halo 等)。你只需专注于 keyShape 的绘制,其他子 Shape 可以通过配置和样式前缀自动管理。
protected drawLabelShape(attributes: Required<S>, container: Group): void {const style = this.getLabelStyle(attributes);this.upsert('label', Label, style, container);}public render(attributes = this.parsedAttributes, container: Group = this) {// 1. 绘制 keyShape(主图形)this._drawKeyShape(attributes, container);if (!this.getShape('key')) return;// 2. 绘制 halothis.drawHaloShape(attributes, container);// 3. 绘制 iconthis.drawIconShape(attributes, container);// 4. 绘制 badgesthis.drawBadgeShapes(attributes, container);// 5. 绘制 labelthis.drawLabelShape(attributes, container);// 6. 绘制 portsthis.drawPortShapes(attributes, container);}
假设你要为节点添加带背景的标签,只需在节点数据中配置 label 相关属性:
{label: true,labelText: '我是标签',labelFill: '#333',labelFontSize: 14,labelBackground: true,labelBackgroundFill: '#fffbe6',labelBackgroundRadius: 6,labelPadding: [4, 8],}
labelText
、labelFill
、labelFontSize
等会被自动提取并传递给 Label 的文本部分。labelBackground
、labelBackgroundFill
、labelBackgroundRadius
、labelPadding
等会被自动提取并传递给 Label 的背景部分。你无需手动管理 Label 的创建、更新、销毁,G6 会自动完成。
labelFill
、labelBackgroundFill
)。label
配置为 true
,且 labelText
已设置。shapeMap
,确认各子 Shape 是否被正确创建。const graph = new Graph({node: {style: {label: false,},state: {hover: {label: true,labelText: 'show when hovered',},},},});
如需更深入了解,建议阅读源码 base-shape.ts
、base-node.ts
。