Loading...
G6 provides a series of built-in nodes, including circle, diamond, donut, ellipse, hexagon, html, image, rect, star, and triangle. These built-in nodes can meet most basic scene requirements.
However, in actual projects, you may encounter needs that these basic nodes cannot meet. In this case, you need to create custom nodes. Don't worry, it's simpler than you think!
In G6, a complete node is usually composed of multiple parts, like building blocks, these parts are combined to form a feature-rich node.
Main components:
key
: The main shape of the node, such as basic shapes like rectangles and circleslabel
: Text label, usually used to display the name or description of the nodeicon
: Icon, showing the type or status of the nodebadge
: Badge, a small mark located at the corner of the nodehalo
: Halo effect displayed around the main shape of the nodeport
: Connection point, a point where edges can connectThere are mainly two ways to create custom nodes:
This is the most common way, you can choose to inherit one of the following types:
BaseNode
- The most basic node class, providing core functions of nodesCircle
- Circle nodeRect
- Rectangle nodeEllipse
- Ellipse nodeDiamond
- Diamond nodeTriangle
- Triangle nodeStar
- Star nodeImage
- Image nodeDonut
- Donut nodeHexagon
- Hexagon nodeWhy choose this way?
If existing node types do not meet the needs, you can create nodes from scratch based on the underlying graphics system of G.
Why choose this way?
Custom nodes developed from scratch need to handle all details by themselves, including graphic drawing, event response, state changes, etc., which is more difficult to develop. You can directly refer to the source code for implementation.
Let's start with a simple example - create a rectangle node with a main and subtitle:
(() => {const { Graph, register, Rect, ExtensionCategory } = g6;// Step 1: Create a custom node classclass DualLabelNode extends Rect {// Subtitle stylegetSubtitleStyle(attributes) {return {x: 0,y: 45, // Placed below the main titletext: attributes.subtitle || '',fontSize: 12,fill: '#666',textAlign: 'center',textBaseline: 'middle',};}// Draw subtitledrawSubtitleShape(attributes, container) {const subtitleStyle = this.getSubtitleStyle(attributes);this.upsert('subtitle', 'text', subtitleStyle, container);}// Render methodrender(attributes = this.parsedAttributes, container) {// 1. Render the basic rectangle and main titlesuper.render(attributes, container);// 2. Add subtitlethis.drawSubtitleShape(attributes, container);}}// Step 2: Register custom noderegister(ExtensionCategory.NODE, 'dual-label-node', DualLabelNode);// Step 3: Use custom nodeconst container = createContainer({ height: 200 });const graph = new Graph({container,data: {nodes: [{id: 'node1',style: { x: 100, y: 100 },data: {title: 'Node A', // Main titlesubtitle: 'Your first custom node', // Subtitle},},],},node: {type: 'dual-label-node',style: {fill: '#7FFFD4',stroke: '#5CACEE',lineWidth: 2,radius: 5,// Main title stylelabelText: (d) => d.data.title,labelFill: '#222',labelFontSize: 14,labelFontWeight: 500,// Subtitlesubtitle: (d) => d.data.subtitle,},},});graph.render();return container;})();
Inherit G6's Rect
(rectangle node) and add a subtitle:
import { Rect, register, Graph, ExtensionCategory } from '@antv/g6';// Create custom node, inheriting from Rectclass DualLabelNode extends Rect {// Subtitle stylegetSubtitleStyle(attributes) {return {x: 0,y: 45, // Placed below the main titletext: attributes.subtitle || '',fontSize: 12,fill: '#666',textAlign: 'center',textBaseline: 'middle',};}// Draw subtitledrawSubtitleShape(attributes, container) {const subtitleStyle = this.getSubtitleStyle(attributes);this.upsert('subtitle', 'text', subtitleStyle, container);}// Render methodrender(attributes = this.parsedAttributes, container) {// 1. Render the basic rectangle and main titlesuper.render(attributes, container);// 2. Add subtitlethis.drawSubtitleShape(attributes, container);}}
Use the register
method to register the node type so that G6 can recognize your custom node:
register(ExtensionCategory.NODE, 'dual-label-node', DualLabelNode);
The register
method requires three parameters:
ExtensionCategory.NODE
indicates this is a node typedual-label-node
is the name we give to this custom node, which will be used in the configuration laterDualLabelNode
is the node class we just createdUse the custom node in the graph configuration:
const graph = new Graph({data: {nodes: [{id: 'node1',style: { x: 100, y: 100 },data: {title: 'Node A', // Main titlesubtitle: 'Your first custom node', // Subtitle},},],},node: {type: 'dual-label-node',style: {fill: '#7FFFD4',stroke: '#5CACEE',lineWidth: 2,radius: 8,// Main title stylelabelText: (d) => d.data.title,labelFill: '#222',labelFontSize: 14,labelFontWeight: 500,// Subtitlesubtitle: (d) => d.data.subtitle,},},});graph.render();
🎉 Congratulations! You have created your first custom node. It looks simple, but this process contains the core idea of custom nodes: inherit a basic node type, then override the render
method to add custom content.
Creating custom nodes in G6 is essentially drawing various graphics on the Canvas. We use a series of "atomic graphics" as basic building blocks, like different shapes in Lego blocks.
G6 nodes are drawn using graphic atomic units provided by the G Graphics System. Here are common graphic elements and their uses:
Graphic Element | Type | Description |
---|---|---|
Circle | circle | Suitable for representing states, avatars, circular buttons, etc. Refer to the SVG |
Ellipse | ellipse | Similar to circle, but supports different horizontal and vertical axes. Refer to the SVG |
Image | image | Used to display icons, user avatars, logos, etc. Refer to the SVG |
Line | line | Used for decoration, auxiliary connections, etc. Refer to the SVG |
Path | path | Supports complex graphics such as arrows, arcs, curves, Bezier paths, etc. Paths contain a set of commands and parameters with different semantics, usage details |
Polygon | polygon | Supports custom graphics such as stars, arrows. Refer to the SVG |
Polyline | polyline | Multi-point polyline, suitable for complex connection structures. Refer to the SVG |
Rectangle | rect | The most commonly used graphic, suitable as a container, card, button, etc. Refer to the SVG |
Text | text | Displays names, descriptions, labels, etc. Provides simple single-line/multi-line text layout capabilities, single-line supports horizontal alignment, character spacing; multi-line supports explicit line breaks and automatic line breaks, vertical alignment |
For more atomic graphics and detailed properties, please refer to Element - Graphics (Optional)
All these graphics can be dynamically created or updated through upsert()
and automatically manage graphic states and lifecycles.
Before customizing elements, you need to understand some important properties and methods in the G6 element base class:
Property | Type | Description |
---|---|---|
shapeMap | Record<string, DisplayObject> | Mapping table of all graphics under the current element |
animateMap | Record<string, IAnimation> | Mapping table of all animations under the current element |
upsert(name, Ctor, style, container, hooks)
: Graphic Creation/UpdateWhen creating custom elements, you will frequently use the upsert
method. It is an abbreviation for "update or insert", responsible for adding or updating graphics in elements:
upsert(key: string, Ctor: { new (...args: any[]): DisplayObject }, style: Record<string, any>, container: DisplayObject);
Parameter | Type | Description |
---|---|---|
key | string | The key of the graphic, corresponding to the key in shapeMap . Built-in keys include 'key' 'label' 'halo' 'icon' 'port' 'badge' The key should not use special symbols, it will be converted to camel case to call getXxxStyle and drawXxxShape methods (see Element Conventions) |
Ctor | { new (...args: any[]): DisplayObject } | Graphic class |
style | Record<string, any> | Graphic style |
container | DisplayObject | Container to mount the graphic |
For example, insert a fixed-position purple circle:
this.upsert('element-key', // Unique identifier of the element'circle', // Graphic type, such as 'rect', 'circle', etc.{ x: 100, y: 100, fill: '#a975f3' }, // Style configuration objectcontainer, // Parent container);
Why use upsert
instead of directly creating graphics through container.appendChild()
? Because:
upsert
will be recorded in the node's shapeMap
, you can easily get it through this.getShape(key)
render(attributes, container)
: Main Entry for Rendering NodesEach custom node class must implement the render(attributes, container)
method, which defines how the node is "drawn". You can use various atomic graphics here to create the structure you want.
render(style: Record<string, any>, container: Group): void;
Parameter | Type | Description |
---|---|---|
style | Record<string, any> | Element style |
container | Group | Container |
getShape(name)
: Get Created GraphicsSometimes, you need to modify the properties of a sub-graphic after creation, or have interactions between sub-graphics. At this time, the getShape
method can help you get any graphics previously created through upsert
:
⚠️ Note: The order of graphics is important, if graphic B depends on the position of graphic A, A must be created first
Currently, the convention element properties include:
Get the size of the element through this.getSize()
Use getXxxStyle
and drawXxxShape
pairing to draw graphics
getXxxStyle
is used to get the graphic style, and drawXxxShape
is used to draw the graphic. Graphics created in this way support automatic animation execution.
Where
Xxx
is the camel case form of the key passed when calling the upsert method.
this.context
The following lifecycle hook functions are provided, you can override these methods in custom nodes to execute specific logic at key moments:
Hook Function | Trigger Timing | Typical Use |
---|---|---|
onCreate | After the node is created and the entrance animation is completed | Bind interaction events, initialize node states, add external listeners |
onUpdate | After the node is updated and the update animation is completed | Update dependent data, adjust related elements, trigger linkage effects |
onDestroy | After the node is destroyed and the exit animation is completed | Clean up resources, remove external listeners, execute destruction notifications |
The most powerful point in the design of G6 elements is the ability to separate "state response" from "drawing logic".
You can define styles for each state in the node configuration:
node: {type: 'custom-node',style: { fill: '#fff' },state: {selected: {fill: '#f00',},hover: {lineWidth: 3,stroke: '#1890ff',},},}
Method to switch states:
graph.setElementState(nodeId, ['selected']);
This state will be passed into the render()
method's attributes
, and the result merged by the internal system will be automatically applied to the graphics.
You can also customize the rendering logic based on the state:
protected getKeyStyle(attributes: Required<BaseNodeStyleProps>) {const style = super.getKeyStyle(attributes);// Adjust style based on stateif (attributes.states?.includes('selected')) {return {...style,stroke: '#1890ff',lineWidth: 2,shadowColor: 'rgba(24,144,255,0.2)',shadowBlur: 15,};}return style;}
Let's gradually increase the complexity and functionality of nodes through practical examples.
Place an icon and label text in the upper left corner of the node.
👇 Step Description:
- Inherit Rect node
- Add icon (image)
- Add label (text)
(() => {const { Graph, register, Rect, ExtensionCategory } = g6;class IconNode extends Rect {get data() {return this.context.graph.getNodeData(this.id).data;}getCustomIconStyle(attributes) {const [width, height] = this.getSize(attributes);const { icon } = this.data;return {x: -width / 2 + 4, // 15px from the lefty: -height / 2 + 4,width: 20,height: 20,src: icon,};}drawCustomIconShape(attributes, container) {const iconStyle = this.getCustomIconStyle(attributes);this.upsert('custom-icon', 'image', iconStyle, container);}getCustomLabelStyle(attributes) {const [width, height] = this.getSize(attributes);const { label } = this.data;return {x: -width / 2 + 26, // 10px to the right of the icony: -height / 2 + 14,text: label || '',fontSize: 10,fill: '#333',textAlign: 'left',textBaseline: 'middle',};}drawCustomLabelShape(attributes, container) {const labelStyle = this.getCustomLabelStyle(attributes);this.upsert('custom-label', 'text', labelStyle, container);}render(attributes, container) {// Render basic rectanglesuper.render(attributes, container);// Add iconthis.drawCustomIconShape(attributes, container);// Add label (to the right of the icon)this.drawCustomLabelShape(attributes, container);}}register(ExtensionCategory.NODE, 'custom-icon-node', IconNode);const container = createContainer({ height: 200 });const graph = new Graph({container,data: {nodes: [{id: 'node1',style: { x: 100, y: 100 },data: {icon: 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png',label: 'AntV',},},],},node: {type: 'custom-icon-node',style: {size: [120, 60],fill: '#fff',stroke: '#873bf4',lineWidth: 2,radius: 2,labelText: 'G6',labelPlacement: 'middle',labelFontSize: 16,labelOffsetY: 6,},},});graph.render();return container;})();
Add a blue button to the node, which triggers an event (logs or executes a callback) when clicked.
(() => {const { Graph, register, Rect, ExtensionCategory } = g6;class ClickableNode extends Rect {getButtonStyle(attributes) {return {x: 40,y: -10,width: 20,height: 20,radius: 10,fill: '#1890ff',cursor: 'pointer', // Mouse pointer becomes a hand};}drawButtonShape(attributes, container) {const btnStyle = this.getButtonStyle(attributes, container);const btn = this.upsert('button', 'rect', btnStyle, container);// Add click event to the buttonif (!btn.__clickBound) {btn.addEventListener('click', (e) => {// Prevent event bubbling to avoid triggering the node's click evente.stopPropagation();// Execute business logicconsole.log('Button clicked on node:', this.id);// If there is a callback function in the data, call itif (typeof attributes.onButtonClick === 'function') {attributes.onButtonClick(this.id, this.data);}});btn.__clickBound = true; // Mark event as bound to avoid duplicate binding}}render(attributes, container) {super.render(attributes, container);// Add a buttonthis.drawButtonShape(attributes, container);}}register(ExtensionCategory.NODE, 'clickable-node', ClickableNode);const container = createContainer({ height: 200 });const graph = new Graph({container,data: {nodes: [{id: 'node1',style: { x: 100, y: 100 },},],},node: {type: 'clickable-node', // Specify using our custom nodestyle: {size: [60, 30],fill: '#7FFFD4',stroke: '#5CACEE',lineWidth: 2,radius: 5,onButtonClick: (id, data) => {},},},});graph.render();return container;})();
Common interactions require nodes and edges to provide feedback through style changes, such as moving the mouse over a node, clicking to select a node/edge, activating interactions on the edge, etc., all require changing the style of nodes and edges. There are two ways to achieve this effect:
data.states
and handle state changes in the custom node class;We recommend users use the second method to achieve node state adjustment, which can be achieved in the following way:
graph.setElementState()
method to set node states.Extend a hole graphic based on rect, with a default fill color of white, which turns orange when clicked. The sample code to achieve this effect is as follows:
(() => {const { Rect, register, Graph, ExtensionCategory } = g6;// 1. Define node classclass SelectableNode extends Rect {getHoleStyle(attributes) {return {x: 20,y: -10,radius: 10,width: 20,height: 20,fill: attributes.holeFill,};}drawHoleShape(attributes, container) {const holeStyle = this.getHoleStyle(attributes, container);this.upsert('hole', 'rect', holeStyle, container);}render(attributes, container) {super.render(attributes, container);this.drawHoleShape(attributes, container);}}// 2. Register noderegister(ExtensionCategory.NODE, 'selectable-node', SelectableNode, true);// 3. Create graph instanceconst container = createContainer({ height: 200 });const graph = new Graph({container,data: {nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }],},node: {type: 'selectable-node',style: {size: [120, 60],radius: 6,fill: '#7FFFD4',stroke: '#5CACEE',lineWidth: 2,holeFill: '#fff',},state: {// Mouse selected stateselected: {holeFill: 'orange',},},},});// 4. Add node interactiongraph.on('node:click', (evt) => {const nodeId = evt.target.id;graph.setElementState(nodeId, ['selected']);});graph.render();return container;})();