logo

G6

  • Docs
  • API
  • Playground
  • Community
  • Productsantv logo arrow
  • 5.0.49
  • Introduction
  • Data
  • Getting Started
    • Quick Start
    • Installation
    • Integration
      • react
      • vue
      • angular
    • Step-by-step guide
  • Graph
    • Extensions En
    • Graph
    • Options
    • extension
  • Element
    • Element Overview
    • Element State
    • Node
      • Node Overview
      • Common Node Configuration
      • Circle Node
      • Diamond Node
      • Donut Node
      • Ellipse Node
      • Hexagon Node
      • HTML Node
      • Image Node
      • Rect Node
      • Star Node
      • Triangle Node
      • Custom Node
      • Define Nodes with React
    • Edge
      • Edge Overview
      • Edge Common Configuration
      • Cubic Bezier Curve Edge
      • CubicHorizontal Bezier Curve Edge
      • CubicVertical Bezier Curve Edge
      • Line Edge
      • Polyline Edge
      • Quadratic Bezier Curve Edge
      • Custom Edge
    • Combo
      • Combo Overview
      • Combo Common Options
      • Circle Combo
      • Rect Combo
      • Custom Combo
    • Shape
      • Shape and KeyShape
      • Atomic Shapes and Their Properties
      • Design and Implementation of Composite Shape
  • Layout
    • Layout Overview
    • Common Layout Configuration Options
    • AntvDagre Layout
    • Circular Layout
    • ComboCombined Layout
    • CompactBox Layout
    • Concentric Layout
    • 3D Force-Directed Layout
    • D3 Force-Directed Layout
    • Dagre Layout
    • Dendrogram Layout
    • Fishbone Layout
    • ForceAtlas2 Force-directed Layout
    • Force-directed Layout
    • Fruchterman Force-directed Layout
    • Grid Layout
    • Indented Tree
    • MDS High-dimensional Data Dimensionality Reduction Layout
    • Mindmap Tree
    • Radial Layout
    • Random Layout
    • Snake Layout
    • Custom Layout
  • Behavior
    • Behavior Overview
    • ZoomCanvas
    • AutoAdaptLabel
    • BrushSelect
    • ClickSelect
    • CollapseExpand
    • CreateEdge
    • DragCanvas
    • DragElement
    • DragElementForce
    • FixElementSize
    • FocusElement
    • HoverActivate
    • LassoSelect
    • OptimizeViewportTransform
    • ScrollCanvas
    • Custom Behavior
  • Plugin
    • Plugin Overview
    • Background
    • BubbleSets
    • Contextmenu
    • EdgeBundling
    • EdgeFilterLens
    • Fisheye
    • Fullscreen
    • GridLine
    • History
    • Hull
    • Legend
    • Minimap
    • Snapline
    • Timebar
    • Toolbar
    • Tooltip
    • Watermark
    • Custom Plugin
  • Transform
    • Data Transformation Overview
    • MapNodeSize
    • PlaceRadialLabels
    • ProcessParallelEdges
    • Custom Transform
  • Theme
    • Theme Overview
    • Custom Theme
    • Palette
    • Custom Palette
  • Animation
    • Animation Overview
    • Custom Animation
  • Further Reading
    • Event
    • renderer
    • coordinate
    • download-image
    • Using Iconfont
    • Use 3D
    • Bundle Project
  • What's new
    • Feature
    • Upgrade To 5.0
  • FAQ
  • contribute

Custom Node

Previous
Triangle Node
Next
Define Nodes with React

Resource

Ant Design
Galacea Effects
Umi-React Application Framework
Dumi-Component doc generator
ahooks-React Hooks Library

Community

Ant Financial Experience Tech
seeconfSEE Conf-Experience Tech Conference

Help

GitHub
StackOverflow

more productsMore Productions

Ant DesignAnt Design-Enterprise UI design language
yuqueYuque-Knowledge creation and Sharing tool
EggEgg-Enterprise-class Node development framework
kitchenKitchen-Sketch Tool set
GalaceanGalacean-Interactive solution
xtechLiven Experience technology
© Copyright 2025 Ant Group Co., Ltd..备案号:京ICP备15032932号-38

Loading...

G6 provides a series of built-in nodes, including circle (Circle Node), diamond (Diamond Node), donut (Donut Node), ellipse (Ellipse Node), hexagon (Hexagon Node), html (HTML Node), image (Image Node), rect (Rectangle Node), star (Star Node), and triangle (Triangle Node). These built-in nodes can meet most basic scenario requirements.

However, in actual projects, you may encounter needs that these basic nodes cannot satisfy. In such cases, you need to create custom nodes. Don't worry, this is simpler than you might think!

Ways to Create Custom Nodes Choose the Right Approach

There are mainly two approaches to creating custom nodes:

1. Inherit from Existing Node Types Recommended

This is the most commonly used approach, where you can choose to inherit from one of the following types:

  • BaseNode - The most basic node class, providing core node functionality
  • Circle - Circle node
  • Rect - Rectangle node
  • Ellipse - Ellipse node
  • Diamond - Diamond node
  • Triangle - Triangle node
  • Star - Star node
  • Image - Image node
  • Donut - Donut node
  • Hexagon - Hexagon node

Why choose this approach?

  • 📌 Less Code: Reuse properties and methods of existing nodes, only focus on new features
  • 📌 Rapid Development: Suitable for most project needs, quickly achieve business goals
  • 📌 Easy Maintenance: Clear code structure with well-defined inheritance relationships

If you choose to inherit from existing node types (recommended), you can jump directly to Create Your First Custom Node in Three Steps to start practicing. Most users will choose this approach!

2. Build from Scratch Based on G Graphics System Advanced Usage

If existing node types don't meet your requirements, you can create nodes from scratch based on G's underlying graphics system.

Why choose this approach?

  • 📌 Maximum Freedom: Complete control over every detail of the node, achieving any complex effects
  • 📌 Special Requirements: Highly customized scenarios that existing node types cannot satisfy
  • 📌 Performance Optimization: Performance optimization for specific scenarios

Important

Custom nodes built from scratch require handling all details yourself, including graphics rendering, event response, state changes, etc., with higher development difficulty. You can refer directly to the source code for implementation.

Create Your First Custom Node in Three Steps

Let's start with a simple example - creating a rectangle node with main and subtitle:

import { Graph, register, Rect, ExtensionCategory } from '@antv/g6';
// Step 1: Create custom node class
class DualLabelNode extends Rect {
// Subtitle style
getSubtitleStyle(attributes) {
return {
x: 0,
y: 45, // Place below the main title
text: attributes.subtitle || '',
fontSize: 12,
fill: '#666',
textAlign: 'center',
textBaseline: 'middle',
};
}
// Draw subtitle
drawSubtitleShape(attributes, container) {
const subtitleStyle = this.getSubtitleStyle(attributes);
this.upsert('subtitle', 'text', subtitleStyle, container);
}
// Render method
render(attributes = this.parsedAttributes, container) {
// 1. Render basic rectangle and main title
super.render(attributes, container);
// 2. Add subtitle
this.drawSubtitleShape(attributes, container);
}
}
// Step 2: Register custom node
register(ExtensionCategory.NODE, 'dual-label-node', DualLabelNode);
// Step 3: Use custom node
const graph = new Graph({
container: 'container',
height: 200,
data: {
nodes: [
{
id: 'node1',
style: { x: 100, y: 100 },
data: {
title: 'Node A', // Main title
subtitle: 'Your First Custom Node', // Subtitle
},
},
],
},
node: {
type: 'dual-label-node',
style: {
fill: '#7FFFD4',
stroke: '#5CACEE',
lineWidth: 2,
radius: 5,
// Main title style
labelText: (d) => d.data.title,
labelFill: '#222',
labelFontSize: 14,
labelFontWeight: 500,
// Subtitle
subtitle: (d) => d.data.subtitle,
},
},
});
graph.render();

Step 1: Write Custom Node Class

Inherit from G6's Rect (rectangle node) and add a subtitle:

import { Rect, register, Graph, ExtensionCategory } from '@antv/g6';
// Create custom node, inheriting from Rect
class DualLabelNode extends Rect {
// Subtitle style
getSubtitleStyle(attributes) {
return {
x: 0,
y: 45, // Place below the main title
text: attributes.subtitle || '',
fontSize: 12,
fill: '#666',
textAlign: 'center',
textBaseline: 'middle',
};
}
// Draw subtitle
drawSubtitleShape(attributes, container) {
const subtitleStyle = this.getSubtitleStyle(attributes);
this.upsert('subtitle', 'text', subtitleStyle, container);
}
// Render method
render(attributes = this.parsedAttributes, container) {
// 1. Render basic rectangle and main title
super.render(attributes, container);
// 2. Add subtitle
this.drawSubtitleShape(attributes, container);
}
}

Step 2: Register Custom Node

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:

  • Extension category: ExtensionCategory.NODE indicates this is a node type
  • Type name: dual-label-node is the name we give to this custom node, which will be used in configuration later
  • Class definition: DualLabelNode is the node class we just created

Step 3: Apply Custom Node

Use the custom node in graph configuration:

const graph = new Graph({
data: {
nodes: [
{
id: 'node1',
style: { x: 100, y: 100 },
data: {
title: 'Node A', // Main title
subtitle: 'Your First Custom Node', // Subtitle
},
},
],
},
node: {
type: 'dual-label-node',
style: {
fill: '#7FFFD4',
stroke: '#5CACEE',
lineWidth: 2,
radius: 8,
// Main title style
labelText: (d) => d.data.title,
labelFill: '#222',
labelFontSize: 14,
labelFontWeight: 500,
// Subtitle
subtitle: (d) => d.data.subtitle,
},
},
});
graph.render();

🎉 Congratulations! You have created your first custom node. It looks simple, but this process contains the core concept of custom nodes: inherit from a basic node type, then override the render method to add custom content.

Understanding Data Flow: How to Access Data in Custom Nodes

Before creating complex custom nodes, understanding how data flows into custom nodes is very important. G6 provides multiple ways to access data for custom nodes:

Method 1: Through attributes Parameter (Recommended)

The first parameter attributes of the render method contains processed style attributes, including data-driven styles:

class CustomNode extends Rect {
render(attributes, container) {
// attributes contains all style attributes, including data-driven styles
console.log('All properties of current node:', attributes);
// If customData: (d) => d.data.someValue is defined in style
// Then you can access it through attributes.customData
const customValue = attributes.customData;
super.render(attributes, container);
}
}

Method 2: Through this.context.graph to Access Raw Data

When you need to access the node's raw data, you can get it through the graph instance:

class CustomNode extends Rect {
// Convenient data access method
get nodeData() {
return this.context.graph.getNodeData(this.id);
}
get data() {
return this.nodeData.data || {};
}
render(attributes, container) {
// Get complete node data
const nodeData = this.nodeData;
console.log('Complete node data:', nodeData);
// Get business data from data field
const businessData = this.data;
console.log('Business data:', businessData);
super.render(attributes, container);
}
}

Complete Data Flow Process

Let's understand how data flows from graph data to custom nodes through a specific example:

import { Graph, register, Rect, ExtensionCategory } from '@antv/g6';
class DataFlowNode extends Rect {
// Method 2: Get raw data through graph
get nodeData() {
return this.context.graph.getNodeData(this.id);
}
get data() {
return this.nodeData.data || {};
}
render(attributes, container) {
// Method 1: Get processed styles from attributes
console.log('Get from attributes:', {
iconUrl: attributes.iconUrl,
userName: attributes.userName,
});
// Method 2: Get from raw data
console.log('Get from raw data:', {
icon: this.data.icon,
name: this.data.name,
role: this.data.role,
});
// Render basic rectangle
super.render(attributes, container);
// Use data to render custom content
if (attributes.iconUrl) {
this.upsert(
'icon',
'image',
{
x: -25,
y: -12,
width: 20,
height: 20,
src: attributes.iconUrl,
},
container,
);
}
if (attributes.userName) {
this.upsert(
'username',
'text',
{
x: 10,
y: 0,
text: attributes.userName,
fontSize: 10,
fill: '#666',
textAlign: 'center',
textBaseline: 'middle',
},
container,
);
}
}
}
register(ExtensionCategory.NODE, 'data-flow-node', DataFlowNode);
const graph = new Graph({
container: 'container',
height: 200,
data: {
nodes: [
{
id: 'user1',
style: { x: 100, y: 100 },
// This is the node's business data
data: {
name: 'Zhang San',
role: 'Developer',
icon: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
},
},
],
},
node: {
type: 'data-flow-node',
style: {
size: [80, 40],
fill: '#f0f9ff',
stroke: '#0ea5e9',
lineWidth: 1,
radius: 4,
// Map data from data field to style attributes
iconUrl: (d) => d.data.icon, // This becomes attributes.iconUrl
userName: (d) => d.data.name, // This becomes attributes.userName
// Main title uses role information
labelText: (d) => d.data.role,
labelFontSize: 12,
labelFill: '#0369a1',
},
},
});
graph.render();
  1. Graph Data Definition: Define business data in data.nodes[].data
  2. Style Mapping: Use functions in node.style to map data to style attributes
  3. Node Access: Access data in custom nodes through attributes or this.context.graph
  4. Rendering Usage: Use the obtained data to render custom graphics

From Simple to Complex: Gradually Building Feature-Rich Nodes

Let's gradually increase the complexity and functionality of nodes through practical examples.

Example 1: User Card Node with Icon and Badge

This example shows how to create a user card node containing avatar, name, and status badge:

import { Graph, register, Rect, ExtensionCategory } from '@antv/g6';
class UserCardNode extends Rect {
get nodeData() {
return this.context.graph.getNodeData(this.id);
}
get data() {
return this.nodeData.data || {};
}
// Avatar style
getAvatarStyle(attributes) {
const [width, height] = this.getSize(attributes);
return {
x: -width / 2 + 20,
y: -height / 2 + 15,
width: 30,
height: 30,
src: attributes.avatarUrl || '',
radius: 15, // Circular avatar
};
}
drawAvatarShape(attributes, container) {
if (!attributes.avatarUrl) return;
const avatarStyle = this.getAvatarStyle(attributes);
this.upsert('avatar', 'image', avatarStyle, container);
}
// Status badge style
getBadgeStyle(attributes) {
const [width, height] = this.getSize(attributes);
const status = this.data.status || 'offline';
const colorMap = {
online: '#52c41a',
busy: '#faad14',
offline: '#8c8c8c',
};
return {
x: width / 2 - 8,
y: -height / 2 + 8,
r: 4,
fill: colorMap[status],
stroke: '#fff',
lineWidth: 2,
};
}
drawBadgeShape(attributes, container) {
const badgeStyle = this.getBadgeStyle(attributes);
this.upsert('badge', 'circle', badgeStyle, container);
}
// Username style
getUsernameStyle(attributes) {
const [width, height] = this.getSize(attributes);
return {
x: -width / 2 + 55,
y: -height / 2 + 20,
text: attributes.username || '',
fontSize: 14,
fill: '#262626',
fontWeight: 'bold',
textAlign: 'left',
textBaseline: 'middle',
};
}
drawUsernameShape(attributes, container) {
if (!attributes.username) return;
const usernameStyle = this.getUsernameStyle(attributes);
this.upsert('username', 'text', usernameStyle, container);
}
// Role label style
getRoleStyle(attributes) {
const [width, height] = this.getSize(attributes);
return {
x: -width / 2 + 55,
y: -height / 2 + 35,
text: attributes.userRole || '',
fontSize: 11,
fill: '#8c8c8c',
textAlign: 'left',
textBaseline: 'middle',
};
}
drawRoleShape(attributes, container) {
if (!attributes.userRole) return;
const roleStyle = this.getRoleStyle(attributes);
this.upsert('role', 'text', roleStyle, container);
}
render(attributes, container) {
// Render basic rectangle
super.render(attributes, container);
// Add various components
this.drawAvatarShape(attributes, container);
this.drawBadgeShape(attributes, container);
this.drawUsernameShape(attributes, container);
this.drawRoleShape(attributes, container);
}
}
register(ExtensionCategory.NODE, 'user-card-node', UserCardNode);
const graph = new Graph({
container: 'container',
height: 200,
data: {
nodes: [
{
id: 'user1',
style: { x: 100, y: 100 },
data: {
name: 'Zhang Xiaoming',
role: 'Frontend Engineer',
status: 'online',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Zhang',
},
},
],
},
node: {
type: 'user-card-node',
style: {
size: [140, 50],
fill: '#ffffff',
stroke: '#d9d9d9',
lineWidth: 1,
radius: 6,
// Data mapping
avatarUrl: (d) => d.data.avatar,
username: (d) => d.data.name,
userRole: (d) => d.data.role,
},
},
});
graph.render();

Example 2: Node with Clickable Action Button

Add a blue button to the node that triggers events (prints logs or executes callbacks) when clicked.

import { Graph, register, Rect, ExtensionCategory } from '@antv/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 hand
};
}
drawButtonShape(attributes, container) {
const btnStyle = this.getButtonStyle(attributes, container);
const btn = this.upsert('button', 'rect', btnStyle, container);
// Add click event to button
if (!btn.__clickBound) {
btn.addEventListener('click', (e) => {
// Prevent event bubbling to avoid triggering node click event
e.stopPropagation();
// Execute business logic
console.log('Button clicked on node:', this.id);
// If there's a callback function in data, call it
if (typeof attributes.onButtonClick === 'function') {
attributes.onButtonClick(this.id, this.data);
}
});
btn.__clickBound = true; // Mark as bound to avoid duplicate binding
}
}
render(attributes, container) {
super.render(attributes, container);
// Add a button
this.drawButtonShape(attributes, container);
}
}
register(ExtensionCategory.NODE, 'clickable-node', ClickableNode);
const graph = new Graph({
container: 'container',
height: 200,
data: {
nodes: [
{
id: 'node1',
style: { x: 100, y: 100 },
},
],
},
node: {
type: 'clickable-node', // Specify using our custom node
style: {
size: [60, 30],
fill: '#7FFFD4',
stroke: '#5CACEE',
lineWidth: 2,
radius: 5,
onButtonClick: (id, data) => {},
},
},
});
graph.render();

Example 3: Node Responding to State Changes (Click to Change Color)

Common interactions require nodes and edges to provide feedback through style changes, such as when the mouse moves over a node, clicking to select nodes/edges, or activating interactions on edges through interaction. All these require changing the styles of nodes and edges. There are two ways to achieve this effect:

  1. Get the current state from data.states and handle state changes in the custom node class;
  2. Separate interaction state from raw data and node drawing logic, only update the node.

We recommend users use the second approach to implement node state adjustments, which can be achieved through the following steps:

  1. Implement custom node;
  2. Configure node state styles in graph configuration;
  3. Set node state through the graph.setElementState() method.

Based on rect, extend a hole shape with default white fill color that turns orange when clicked. The sample code to achieve this effect is as follows:

import { Rect, register, Graph, ExtensionCategory } from '@antv/g6';
// 1. Define node class
class 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 node
register(ExtensionCategory.NODE, 'selectable-node', SelectableNode, true);
// 3. Create graph instance
const graph = new Graph({
container: 'container',
height: 200,
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 state
selected: {
holeFill: 'orange',
},
},
},
});
// 4. Add node interaction
graph.on('node:click', (evt) => {
const nodeId = evt.target.id;
graph.setElementState(nodeId, ['selected']);
});
graph.render();