Loading...
G6 provides a flexible Shape mechanism, allowing developers to customize various graphics and efficiently reuse them in elements such as nodes, edges, and combos. This article uses Label as an example to explain how to customize a Shape and how to apply it in elements.
All Shapes inherit from BaseShape
, which centrally manages the lifecycle (creation, update, destruction), property parsing, animation, event binding, etc. You only need to focus on implementing the render
method.
Core Abstraction:
import { CustomElement } from '@antv/g';abstract class BaseShape extends CustomElement {// Lifecycle management, property parsing, animation, etc...public abstract render(attributes, container): void;}
A node usually contains multiple child Shapes, for example:
Node├── keyShape (main shape)├── label (label, auxiliary information)│ ├── text│ └── rect├── icon│ ├── text│ └── image├── badge│ ├── text│ └── rect└── port│ ├── circle
Label is a typical composite Shape, consisting of text (Text) and an optional background (Rect). The implementation idea is as follows:
upsert
method is used to automatically manage the creation, update, and destruction of child Shapes.Main code snippet of Label:
import { Text, Rect } from '@antv/g'; // Import atomic graphicsexport 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);}// ... Omitted style extraction methods}
getTextStyle
and getBackgroundStyle
extract the style properties for text and background respectively to avoid interference.upsert
method ensures automatic CRUD of Shapes, greatly improving reusability and robustness.Below is an example of customizing a label with special decoration, demonstrating the complete definition, registration, and usage of a Shape:
import { BaseShape, ExtensionCategory, Circle } from 'g6';import { Text, Rect, Circle } from '@antv/g';class FancyLabel extends BaseShape {render(attributes = this.parsedAttributes, container = this) {// Main textthis.upsert('text', Text, this.getTextStyle(attributes), container);// Backgroundthis.upsert('background', Rect, this.getBackgroundStyle(attributes), container);// Extra decoration: small dot on the leftthis.upsert('dot', Circle, {x: -8, y: 0, r: 3, fill: '#faad14',}, container);}// ...implement getTextStyle/getBackgroundStyle}// Register custom Shaperegister(ExtensionCategory.SHAPE, 'fancy-label-shape', FancyLabel);// Define custom nodeclass 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 custom noderegister(ExtensionCategory.Node, 'fancy-label-node', CustomCircle);
In G6, elements such as nodes, edges, and combos often contain multiple child Shapes (such as main shape, label, badge, port, etc.). To ensure that the style of each child Shape does not interfere with each other, G6 adopts a prefix separation design for style properties.
Take Label as an example:
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>
means all properties starting with background
belong to the label background style.subStyleProps
and subObject
are used to automatically extract prefixed styles and pass them to the corresponding Shape.Label background style extraction example
protected getBackgroundStyle(attributes: Required<LabelStyleProps>) {if (attributes.background === false) return false;const style = this.getGraphicStyle(attributes);const backgroundStyle = subStyleProps<RectStyleProps>(style, 'background');// ...Omitted layout calculationreturn backgroundStyle;}
Style configuration example
{"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
or similar methods. Label is only responsible for displaying text information and does not affect the main interaction control of the node.Take nodes as an example. The node base class BaseNode
has built-in support for multiple child Shapes (keyShape, label, icon, badge, port, halo, etc.). You only need to focus on drawing the keyShape, and other child Shapes can be automatically managed through configuration and style prefixing.
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. Draw keyShape (main shape)this._drawKeyShape(attributes, container);if (!this.getShape('key')) return;// 2. Draw halothis.drawHaloShape(attributes, container);// 3. Draw iconthis.drawIconShape(attributes, container);// 4. Draw badgesthis.drawBadgeShapes(attributes, container);// 5. Draw labelthis.drawLabelShape(attributes, container);// 6. Draw portsthis.drawPortShapes(attributes, container);}
Suppose you want to add a label with a background to a node, just configure the label-related properties in the node data:
{label: true,labelText: 'I am a label',labelFill: '#333',labelFontSize: 14,labelBackground: true,labelBackgroundFill: '#fffbe6',labelBackgroundRadius: 6,labelPadding: [4, 8],}
labelText
, labelFill
, labelFontSize
, etc. will be automatically extracted and passed to the text part of the Label.labelBackground
, labelBackgroundFill
, labelBackgroundRadius
, labelPadding
, etc. will be automatically extracted and passed to the background part of the Label.You do not need to manually manage the creation, update, or destruction of the Label. G6 will handle it automatically.
labelFill
, labelBackgroundFill
).label
configuration of the node/edge/combo is true
and labelText
is set.shapeMap
and confirm whether each child Shape is created correctly.const graph = new Graph({node: {style: {label: false,},state: {hover: {label: true,labelText: 'show when hovered',},},},});
For more details, it is recommended to read the source code base-shape.ts
, base-node.ts
.