Loading...
G6 provides two types of built-in combos: 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.
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.Combos differ from ordinary nodes and have the following characteristics:
There are two ways to create custom combos:
This is the most common way, and you can choose to inherit one of the following types:
BaseCombo
- The most basic combo class, providing core functionality for combos.Circle
- Circular combo.Rect
- Rectangular combo.Why choose this way?
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?
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 for implementation.
Let's start by inheriting BaseCombo
to implement a custom hexagon combo:
(() => {const { Graph, register, BaseCombo, ExtensionCategory } = g6;// Define the path for the collapsed state buttonconst 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 buttonconst 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 hexagongetKeyPath(attributes) {const [width, height] = this.getKeySize(attributes);const padding = 10;const size = Math.min(width, height) + padding;// Calculate the vertices of the hexagonconst 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 pathconst 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 graphicgetKeyStyle(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 graphicdrawKeyShape(attributes, container) {return this.upsert('key', 'path', this.getKeyStyle(attributes), container);}// Draw the expand/collapse button, using paths for finer controldrawCollapseButton(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 graphicconst 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 graphicsrender(attributes, container) {super.render(attributes, container);this.drawCollapseButton(attributes, container);}// Use lifecycle hooks to add event listenersonCreate() {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 comboregister(ExtensionCategory.COMBO, 'hexagon-combo', HexagonCombo);// Create a graph instance and use the custom comboconst container = createContainer({ height: 250 });const graph = new Graph({container,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();return container;})();
import { BaseCombo } from '@antv/g6';import type { BaseComboStyleProps } from '@antv/g6';// Define button path generation functionsconst 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 hexagonprotected getKeyPath(attributes: Required<BaseComboStyleProps>) {const [width, height] = this.getKeySize(attributes);const padding = 10;const size = Math.min(width, height) + padding;// Calculate the vertices of the hexagonconst 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 pathconst 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 dataprotected getKeyStyle(attributes: Required<BaseComboStyleProps>) {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 objectsprotected drawKeyShape(attributes: Required<BaseComboStyleProps>, container: Group) {return this.upsert('key', 'path', this.getKeyStyle(attributes), container);}// Draw the collapse/expand button, using SVG paths for finer controlprotected drawCollapseButton(attributes: Required<BaseComboStyleProps>) {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 graphicconst 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 eventsonCreate() {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);});}}
import { ExtensionCategory } from '@antv/g6';register(ExtensionCategory.COMBO, 'hexagon-combo', HexagonCombo);
const graph = new Graph({// ...other configurationscombo: {type: 'hexagon-combo', // Use the name registeredstyle: {padding: 20,showCollapseButton: true,labelText: (d) => d.data?.label,labelPlacement: 'top',},},// Since we implemented the collapse/expand feature ourselves, only drag behavior is needed herebehaviors: ['drag-element'],});
🎉 Congratulations! You have created your first custom combo.
Although Combos inherit from BaseNode
, there are some key differences:
G6's Combos are drawn using atomic graphic units provided by the G Graphics System. For an introduction to atomic graphics, please refer to the Element - Shape (Optional) documentation.
All these graphics can be dynamically created or updated using upsert()
and automatically manage graphic states and lifecycles.
Before customizing Combos, 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 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:
upsert(key: string, Ctor: { new (...args: any[]): DisplayObject }, style: Record<string, any>, 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) |
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 with container.appendChild()
? Because:
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 CombosEvery 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.
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. 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.
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 method.
this.context
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 |
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:
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:
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:
protected getKeyStyle(attributes: Required<BaseComboStyleProps>) {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;}