---
title: Custom Combo
order: 4
---
G6 provides two types of [built-in combos](/en/manual/element/combo/base-combo): circular combos and rectangular combos. However, in complex business scenarios, you may need to create custom combos with specific styles, interactive effects, or behavior logic.
## Before You Start: Understanding the Basic Composition of Combos
In G6, a complete combo typically consists of the following parts:
- `key`: The main graphic of the combo, representing the main shape of the combo, such as a circle, rectangle, etc.
- `label`: Text label, usually used to display the name or description of the combo.
- `halo`: A graphic that displays a halo effect around the main graphic.
### Special Characteristics of Combos
Combos differ from ordinary nodes and have the following characteristics:
1. **Containment**: Combos can contain nodes and other combos, forming a hierarchical structure.
2. **Two States**: Expanded and Collapsed states.
3. **Adaptive Size**: Automatically adjusts size based on internal elements.
4. **Drag Behavior**: Supports overall dragging and dragging elements in/out.
## Ways to Customize Combos Choose the Right Way
There are two ways to create custom combos:
### 1. Inherit Existing Combo Types Recommended
This is the most common way, and you can choose to inherit one of the following types:
- [`BaseCombo`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/combos/base-combo.ts) - The most basic combo class, providing core functionality for combos.
- [`Circle`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/combos/circle.ts) - Circular combo.
- [`Rect`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/combos/rect.ts) - Rectangular combo.
**Why choose this way?**
- 📌 **Less Code**: Reuse existing combo properties and methods, focusing only on new features.
- 📌 **Fast Development**: Suitable for most project needs, quickly achieving business goals.
- 📌 **Easy Maintenance**: Clear code structure and clear inheritance relationships.
:::tip{title=Get Started Now}
If you choose to inherit from existing combo types (recommended), you can jump directly to [Create Your First Custom Combo in Three Steps](#create-your-first-custom-combo-in-three-steps) to start practicing. Most users will choose this approach!
:::
### 2. Develop from Scratch Based on the G Graphics System Advanced Usage
If existing combo types do not meet your needs, you can create combos from scratch based on the underlying graphics system of G.
**Why choose this way?**
- 📌 **Maximum Freedom**: Full control over every detail of the combo, achieving any complex effect.
- 📌 **Special Needs**: Highly customized scenarios that existing combo types cannot meet.
- 📌 **Performance Optimization**: Performance optimization for specific scenarios.
:::warning{title=Note}
Developing custom combos from scratch requires handling all details yourself, including graphic drawing, event response, state changes, expand/collapse logic, etc., which is quite challenging. You can directly refer to the [source code](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/combos/base-combo.ts) for implementation.
:::
## Create Your First Custom Combo in Three Steps
Let's start by inheriting `BaseCombo` to implement a custom hexagon combo:
```js | ob { pin:false, inject: true }
import { Graph, register, BaseCombo, ExtensionCategory } from '@antv/g6';
// Define the path for the collapsed state button
const collapse = (x, y, r) => {
return [
['M', x - r, y],
['a', r, r, 0, 1, 0, r * 2, 0],
['a', r, r, 0, 1, 0, -r * 2, 0],
['M', x - r + 4, y],
['L', x + r - 4, y],
];
};
// Define the path for the expanded state button
const expand = (x, y, r) => {
return [
['M', x - r, y],
['a', r, r, 0, 1, 0, r * 2, 0],
['a', r, r, 0, 1, 0, -r * 2, 0],
['M', x - r + 4, y],
['L', x - r + 2 * r - 4, y],
['M', x - r + r, y - r + 4],
['L', x, y + r - 4],
];
};
class HexagonCombo extends BaseCombo {
// Get the path of the hexagon
getKeyPath(attributes) {
const [width, height] = this.getKeySize(attributes);
const padding = 10;
const size = Math.min(width, height) + padding;
// Calculate the vertices of the hexagon
const points = [];
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i;
const x = (size / 2) * Math.cos(angle);
const y = (size / 2) * Math.sin(angle);
points.push([x, y]);
}
// Construct the SVG path
const path = [['M', points[0][0], points[0][1]]];
for (let i = 1; i < 6; i++) {
path.push(['L', points[i][0], points[i][1]]);
}
path.push(['Z']);
return path;
}
// Get the style of the main graphic
getKeyStyle(attributes) {
const style = super.getKeyStyle(attributes);
return {
...style,
d: this.getKeyPath(attributes),
fill: attributes.collapsed ? '#FF9900' : '#F04864',
fillOpacity: attributes.collapsed ? 0.5 : 0.2,
stroke: '#54BECC',
lineWidth: 2,
};
}
// Draw the main graphic
drawKeyShape(attributes, container) {
return this.upsert('key', 'path', this.getKeyStyle(attributes), container);
}
// Draw the expand/collapse button, using paths for finer control
drawCollapseButton(attributes) {
const { collapsed } = attributes;
const [width] = this.getKeySize(attributes);
const btnR = 8;
const x = width / 2 + btnR;
const d = collapsed ? expand(x, 0, btnR) : collapse(x, 0, btnR);
// Create the clickable area and button graphic
const hitArea = this.upsert('hit-area', 'circle', { cx: x, r: 8, fill: '#fff', cursor: 'pointer' }, this);
this.upsert('button', 'path', { stroke: '#54BECC', d, cursor: 'pointer', lineWidth: 1.4 }, hitArea);
}
// Override the render method to add more custom graphics
render(attributes, container) {
super.render(attributes, container);
this.drawCollapseButton(attributes, container);
}
// Use lifecycle hooks to add event listeners
onCreate() {
this.shapeMap['hit-area'].addEventListener('click', () => {
const id = this.id;
const collapsed = !this.attributes.collapsed;
const { graph } = this.context;
if (collapsed) graph.collapseElement(id);
else graph.expandElement(id);
});
}
}
// Register the custom combo
register(ExtensionCategory.COMBO, 'hexagon-combo', HexagonCombo);
// Create a graph instance and use the custom combo
const graph = new Graph({
container: 'container',
height: 250,
data: {
nodes: [
{ id: 'node1', combo: 'combo1', style: { x: 100, y: 100 } },
{ id: 'node2', combo: 'combo1', style: { x: 150, y: 150 } },
{ id: 'node3', combo: 'combo2', style: { x: 300, y: 100 } },
{ id: 'node4', combo: 'combo2', style: { x: 350, y: 150 } },
],
combos: [
{ id: 'combo1', data: { label: 'Hexagon 1' } },
{ id: 'combo2', data: { label: 'Hexagon 2' }, style: { collapsed: true } },
],
},
node: {
style: {
fill: '#91d5ff',
stroke: '#1890ff',
lineWidth: 1,
},
},
combo: {
type: 'hexagon-combo',
style: {
padding: 20,
showCollapseButton: true,
labelText: (d) => d.data?.label,
labelPlacement: 'top',
},
},
behaviors: ['drag-element'],
});
graph.render();
```
### Step 1: Write the Custom Combo Class
```typescript
import { BaseCombo } from '@antv/g6';
import type { BaseComboStyleProps } from '@antv/g6';
// Define button path generation functions
const collapse = (x, y, r) => {
return [
['M', x - r, y],
['a', r, r, 0, 1, 0, r * 2, 0],
['a', r, r, 0, 1, 0, -r * 2, 0],
['M', x - r + 4, y],
['L', x + r - 4, y],
];
};
const expand = (x, y, r) => {
return [
['M', x - r, y],
['a', r, r, 0, 1, 0, r * 2, 0],
['a', r, r, 0, 1, 0, -r * 2, 0],
['M', x - r + 4, y],
['L', x - r + 2 * r - 4, y],
['M', x - r + r, y - r + 4],
['L', x, y + r - 4],
];
};
class HexagonCombo extends BaseCombo {
// Get the path of the hexagon
protected getKeyPath(attributes: Required) {
const [width, height] = this.getKeySize(attributes);
const padding = 10;
const size = Math.min(width, height) + padding;
// Calculate the vertices of the hexagon
const points = [];
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i;
const x = (size / 2) * Math.cos(angle);
const y = (size / 2) * Math.sin(angle);
points.push([x, y]);
}
// Construct the SVG path
const path = [['M', points[0][0], points[0][1]]];
for (let i = 1; i < 6; i++) {
path.push(['L', points[i][0], points[i][1]]);
}
path.push(['Z']);
return path;
}
// Get the style of the main graphic, directly using path data
protected getKeyStyle(attributes: Required) {
const style = super.getKeyStyle(attributes);
return {
...style,
d: this.getKeyPath(attributes),
fill: attributes.collapsed ? '#FF9900' : '#F04864',
fillOpacity: attributes.collapsed ? 0.5 : 0.2,
stroke: '#54BECC',
lineWidth: 2,
};
}
// Draw the main graphic, using path type to directly pass in style objects
protected drawKeyShape(attributes: Required, container: Group) {
return this.upsert('key', 'path', this.getKeyStyle(attributes), container);
}
// Draw the collapse/expand button, using SVG paths for finer control
protected drawCollapseButton(attributes: Required) {
const { collapsed } = attributes;
const [width] = this.getKeySize(attributes);
const btnR = 8;
const x = width / 2 + btnR;
const d = collapsed ? expand(x, 0, btnR) : collapse(x, 0, btnR);
// Create the clickable area and button graphic
const hitArea = this.upsert('hit-area', 'circle', { cx: x, r: 8, fill: '#fff', cursor: 'pointer' }, this);
this.upsert('button', 'path', { stroke: '#54BECC', d, cursor: 'pointer', lineWidth: 1.4 }, hitArea);
}
// Use lifecycle hook methods to bind events
onCreate() {
this.shapeMap['hit-area'].addEventListener('click', () => {
const id = this.id;
const collapsed = !this.attributes.collapsed;
const { graph } = this.context;
if (collapsed) graph.collapseElement(id);
else graph.expandElement(id);
});
}
}
```
### Step 2: Register the Custom Combo
```js
import { ExtensionCategory } from '@antv/g6';
register(ExtensionCategory.COMBO, 'hexagon-combo', HexagonCombo);
```
### Step 3: Apply the Custom Combo
```js
const graph = new Graph({
// ...other configurations
combo: {
type: 'hexagon-combo', // Use the name registered
style: {
padding: 20,
showCollapseButton: true,
labelText: (d) => d.data?.label,
labelPlacement: 'top',
},
},
// Since we implemented the collapse/expand feature ourselves, only drag behavior is needed here
behaviors: ['drag-element'],
});
```
🎉 Congratulations! You have created your first custom combo.
## Going Further: Understanding the Principles of Combo Drawing
### Differences Between Combos and Nodes
Although Combos inherit from `BaseNode`, there are some key differences:
1. **Adaptive Size**: Combos automatically calculate the appropriate size based on internal elements.
2. **Expand/Collapse States**: Combos have two display states and need to handle state transitions.
3. **Hierarchical Structure**: Combos can be nested, forming hierarchical relationships.
4. **Internal Element Management**: Combos need to manage the nodes and sub-combos they contain.
### Atomic Graphics
G6's Combos are drawn using atomic graphic units provided by the [G Graphics System](https://g.antv.antgroup.com/). For an introduction to atomic graphics, please refer to the [Element - Shape (Optional)](/en/manual/element/shape/overview) documentation.
All these graphics can be dynamically created or updated using `upsert()` and automatically manage graphic states and lifecycles.
### Element Base Class
Before customizing Combos, you need to understand some important properties and methods in the G6 element base class:
#### Properties
| Property | Type | Description |
| ---------- | ----------------------------- | --------------------------------------------------------- |
| shapeMap | Record | Mapping table of all graphics under the current element |
| animateMap | Record | Mapping table of all animations under the current element |
#### Methods
#### `upsert(name, Ctor, style, container, hooks)`: Graphic Creation/Update
When creating custom Combos, you will frequently use the `upsert` method. It is short for "update or insert" and is responsible for adding or updating graphics in the element:
```typescript
upsert(key: string, Ctor: { new (...args: any[]): DisplayObject }, style: Record, container: DisplayObject);
```
| Parameter | Type | Description |
| --------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| key | string | Key of the graphic, corresponding to the key in `shapeMap`. Built-in keys include `'key'`, `'label'`, `'halo'`, `'icon'`, `'port'`, `'badge'`
Keys should not use special symbols, and will be converted to camel case to call `getXxxStyle` and `drawXxxShape` methods (see [Element Conventions](#element-conventions)) |
| Ctor | { new (...args: any[]): DisplayObject } | Graphic class |
| style | Record | Graphic style |
| container | DisplayObject | Container to mount the graphic |
For example, insert a fixed-position purple circle:
```js
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 object
container, // Parent container
);
```
Why use `upsert` instead of directly creating graphics with `container.appendChild()`? Because:
1. **Better Performance**: When state changes or data updates, it intelligently reuses existing graphics instead of deleting and recreating them, greatly improving rendering performance.
2. **Simpler Code**: No need to manually check if elements exist.
3. **Easy Management**: All graphics created through `upsert` are recorded in the node's `shapeMap`, and you can easily access them with `this.getShape(key)`.
#### `render(attributes, container)`: Main Entry for Rendering Combos
Every custom combo class must implement the `render(attributes, container)` method, which defines how the combo is "drawn". You can use various atomic graphics here to create the structure you want.
```typescript
render(style: Record, container: Group): void;
```
| Parameter | Type | Description |
| --------- | ------------------- | ------------- |
| style | Record | Element style |
| container | Group | Container |
#### `getShape(name)`: Get Created Graphics
Sometimes, you need to modify the properties of a sub-graphic after creation or have interactions between sub-graphics. In this case, the `getShape` method can help you access any graphics previously created with `upsert`:
**⚠️ Note**: The order of graphics is important. If graphic B depends on the position of graphic A, make sure A is created first.
### Element Conventions
- **Use Convention Properties**
The convention properties in combos include:
- Use `this.getKeySize(attributes)` to get the size of the combo, considering the collapsed state and sub-elements.
- Use `this.getContentBBox(attributes)` to get the bounding box of the content area.
- Use `this.getComboPosition(attributes)` to get the current position of the combo, based on state and sub-elements.
- **Use `getXxxStyle` and `drawXxxShape` Pairing for Graphic Drawing**
`getXxxStyle` is used to get the graphic style, and `drawXxxShape` is used to draw the graphic. Graphics created this way support automatic animation execution.
> `Xxx` is the camel case form of the key passed to the [upsert](#methods) method.
- **Access Graph Context via `this.context`**
### Lifecycle Hooks
The following lifecycle hook functions are provided, and you can override these methods in custom combos to execute specific logic at key moments:
| Hook Function | Trigger Timing | Typical Use Cases |
| ------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| `onCreate` | After the combo is created and the entrance animation is completed | Bind interactive events, initialize combo state, add external listeners |
| `onUpdate` | After the combo is updated and the update animation is completed | Update dependent data, adjust related elements, trigger linkage effects |
| `onDestroy` | After the combo is destroyed and the exit animation is completed | Clean up resources, remove external listeners, execute destruction notifications |
### State Response
One of the most powerful aspects of G6 element design is the ability to separate **"state response"** from **"drawing logic"**.
You can define styles for each state in the combo configuration:
```js
combo: {
type: 'custom-combo',
style: {
fill: '#f0f2f5',
stroke: '#d9d9d9'
},
state: {
selected: {
stroke: '#1890ff',
lineWidth: 2,
shadowColor: 'rgba(24,144,255,0.2)',
shadowBlur: 15,
},
hover: {
fill: '#e6f7ff',
},
},
}
```
Method to switch states:
```js
graph.setElementState(comboId, ['selected']);
```
This state will be passed into the `render()` method's `attributes` and automatically applied to the graphics as a result of the internal system merging.
You can also customize rendering logic based on the state:
```typescript
protected getKeyStyle(attributes: Required) {
const style = super.getKeyStyle(attributes);
// Adjust style based on state
if (attributes.states?.includes('selected')) {
return {
...style,
stroke: '#1890ff',
lineWidth: 2,
shadowColor: 'rgba(24,144,255,0.2)',
shadowBlur: 15,
};
}
return style;
}
```