Custom Node
G6 provides abundant Built-in Nodes, including circle, [rect](/en/docs/manual/middle/elements/nodes/rect, ellipse, diamond, triangle, star, image, modelRect. Besides, the custom machanism allows the users to design their own type of nodes by G6.registerNode('nodeName', options)
. A node with complex graphics shapes, complex interactions, fantastic animations can be implemented easily.
In this document, we will introduce the custom enodeby four examples:
1. Register a bran-new edge;
2. Register an edge by extending a built-in edge;
3. Register an edge with interactions and styles;
4. Register an edge with custom arrow.
1. Register a bran-new node: Draw the graphics; Optimize the performance.
2. Register a node by extending a built-in node: Add extra graphics shape; Add animation.
3. Adjust the anchorPoints(link points);
4. Register a node with state styles: Response the states change by styles and animations
As stated in Shape, there are two points should be satisfied when customize a node:
- Controll the life cycle of the node;
- Analyze the input data and show it by graphics.
The API of cumstom node:
G6.registerNode(
'nodeName',
{
options: {
style: {},
stateStyles: {
hover: {},
selected: {},
},
},
/**
* Draw the node with label
* @param {Object} cfg The configurations of the node
* @param {G.Group} group The container of the node
* @return {G.Shape} The keyShape of the node. It can be obtained by node.get('keyShape')
*/
draw(cfg, group) {},
/**
* The extra operations after drawing the node. There is no operation in this function by default
* @param {Object} cfg The configurations of the node
* @param {G.Group} group The container of the node
*/
afterDraw(cfg, group) {},
/**
* Update the node and its label
* @override
* @param {Object} cfg The configurations of the node
* @param {Node} node The node item
*/
update(cfg, node) {},
/**
* The operations after updating the node. It is combined with afterDraw generally
* @override
* @param {Object} cfg The configurations of the node
* @param {Node} node The node item
*/
afterUpdate(cfg, node) {},
/**
* Response the node states change. Mainly the interaction states. The business states should be handled in the draw function
* The states 'selected' and 'active' will be responsed on keyShape by default. To response more states, implement this function.
* @param {String} name The name of the state
* @param {Object} value The value of the state
* @param {Node} node The node item
*/
setState(name, value, node) {},
/**
* Get the anchorPoints(link points for related edges)
* @param {Object} cfg The configurations of the node
* @return {Array|null} The array of anchorPoints(link points for related edges). Null means there are no anchorPoints
*/
getAnchorPoints(cfg) {},
},
extendNodeName,
);
⚠️Attention:
draw
is required if the custom node does not extend any parent;update
is not required. If it is undefined, thedraw
will be called when updating the node, which means all the graphics will be cleared and repaint;afterDraw
andafterUpdate
are used for extending the exited nodes in general. e.g. adding extra image on rect node, adding animation on a circle node, ...;- In general,
setState
is not required; getAnchorPoints
is only required when you want to contrain the link points for nodes and their related edges. The anchorPoints can be assigned in the node data as well.
1. Register a Bran-new Edge
Render the Node
Now, we are going to register a diamond node:
Although there is a built-in diamond node in G6, we implement it here to rewrite it for demonstration.
G6.registerNode('diamond', {
draw(cfg, group) {
// If there is style object in cfg, it should be mixed here
const shape = group.addShape('path', {
attrs: {
path: this.getPath(cfg), // Get the path by cfg
stroke: cfg.color, // Apply the color to the stroke. For filling, use fill: cfg.color instead
},
});
if (cfg.label) {
// If the label exists
// The complex label configurations can be defined by labeCfg
// const style = (cfg.labelCfg && cfg.labelCfg.style) || {};
// style.text = cfg.label;
group.addShape('text', {
// attrs: style
attrs: {
x: 0, // center
y: 0,
textAlign: 'center',
textBaseline: 'middle',
text: cfg.label,
fill: '#666',
},
});
}
return shape;
},
// Return the path of a diamond
getPath(cfg) {
const size = cfg.size || [40, 40];
const width = size[0];
const height = size[1];
// / 1 \
// 4 2
// \ 3 /
const path = [
['M', 0, 0 - height / 2], // Top
['L', width / 2, 0], // Right
['L', 0, height / 2], // Bottom
['L', -width / 2, 0], // Left
['Z'], // Close the path
];
return path;
},
});
We have registered a dimond node. The following code use the diamond node:
const data = {
nodes: [
{ x: 50, y: 100, shape: 'diamond' }, // The simplest form
{ x: 150, y: 100, shape: 'diamond', size: [50, 100] }, // Add the size
{ x: 250, y: 100, color: 'red', shape: 'diamond' }, // Add the color
{ x: 350, y: 100, label: '菱形', shape: 'diamond' }, // Add the label
],
};
const graph = new G6.Graph({
container: 'mountNode',
width: 500,
height: 500,
});
graph.data(data);
graph.render();
Optimize the Performance
When the nodes or edges are updated by graph.update(item, cfg)
, the draw
will be called for repainting. But in the situation with large amount of data (especially the text), repainting all the graphics shapes by draw
has bad performance.
Therefore, rewrite the update
function when registering a node for partial repainting is necessary. We can repaint some of the graphics shapes instead of all the graphis by update
. The update
is not required if you have no performance problem.
To update a few graphics shapes of a node in update
, you need find the graphics shapes to be updated frist:
- Find the keyShape by
group.get('children')[0]
, which is the return value ofdraw
; - Find the graphics shape of label by
group.get('children')[1]
.
The code shown below update the path and the color of the keyShape of the diamond:
G6.registerNode('diamond', {
draw(cfg, group) {
// ... // Same as the code above
},
getPath(cfg) {
// ... // Same as the code above
},
update(cfg, node) {
const group = node.getContainer(); // Get the container of the node
const shape = group.get('children')[0]; // Find the first graphics shape of the node. It is determined by the order of being added
const style = {
path: this.getPath(cfg),
stroke: cfg.color,
};
shape.attr(style); // Update
},
});
2. Extend a Built-in Node
Extend the Shape
There are several Built-in Nodes in G6. You can extend them to make some modification on them. It is similar to register the diamond node. single-shape is the base class of all the graphics shape, you can also extend it.
For example, we are going to extend the single-shape. draw
, update
, and setState
have been implemented in the single-shape. Thus, we only rewrite the getShapeStyle
, which returns the path and the styles of graphics shapes.
G6.registerNode(
'diamond',
{
shapeType: 'path', // It is required when the shape inherits from 'single-shape', not required otherwise
getShapeStyle(cfg) {
const size = this.getSize(cfg); // translate to [width, height]
const color = cfg.color;
const width = size[0];
const height = size[1];
// / 1 \
// 4 2
// \ 3 /
const path = [
['M', 0, 0 - height / 2], // Top
['L', width / 2, 0], // Right
['L', 0, height / 2], // Bottom
['L', -width / 2, 0], // Left
['Z'], // Close the path
];
const style = Util.mix(
{},
{
path: path,
stroke: color,
},
cfg.style,
);
return style;
},
},
// Extend the 'single-shape'
'single-shape',
);
Add Animation
We are going to add animation by afterDraw
in this section. The result:
- Extend the built-in rect node, and add a graphics shape in the rect;
-
Execute the animation repeatly.
// Register a type of custom node named inner-animate G6.registerNode( 'inner-animate', { afterDraw(cfg, group) { const size = cfg.size; const width = size[0] - 14; const height = size[1] - 14; // Add an image shape const image = group.addShape('image', { attrs: { x: -width / 2, y: -height / 2, width: width, height: height, img: cfg.img, }, }); // Execute the animation image.animate( { onFrame(ratio) { const matrix = Util.mat3.create(); const toMatrix = Util.transform(matrix, [ ['r', ratio * Math.PI * 2], ]); return { matrix: toMatrix, }; }, repeat: true, }, 3000, 'easeCubic', ); }, }, // Extend the rect node 'rect', );
For more information about animation, please refer to Basic Ainmation.
3. Adjust the anchorPoint
The anchorPoint of a node is the intersection of the node and its related edges.
(Left) The diamond node has no anchorPoints. (Right) The diamond node has anchorPoints.
There are two ways to adjust the anchorPoints of the node:
- Configure the
anchorPoints
in the data.
Applicable Scene: Assign different anchorPoints for different nodes.
- Assign
getAnchorPoints
when registering a custom node.
Applicable Scene: Configure the anchorPoints globally for this type of node.
Configure the anchorPoints in Data
const data = {
nodes: [
{
id: 'node1',
x: 100,
y: 100,
anchorPoints: [
[0, 0.5], // The center of the left border
[1, 0.5], // The center of the right border
],
},
//... // Other nodes
],
edges: [
//... // Other edges
],
};
Assign anchorPoints When Registering Node
G6.registerNode(
'diamond',
{
//... // Other functions
getAnchorPoints() {
return [
[0, 0.5], // The center of the left border
[1, 0.5], // The center of the right border
];
},
},
'rect',
);
4. Register Node with State Styles
In general, nodes and edges should response the states change by styles chaging. For example, highlight the node or edge clicked/hovered by user. We can achieve it by two ways:
- Add a flag on the node data, control the style according to the flag in
draw
when registering a custom node; - Separate the interactive states from source data and
draw
, update the node only.
We recommend adjust the state styles by the second way, which can be achieved by:
- Response the states in
setState
function when registering a node/edge; - Set/change the state by
graph.setItemState()
.
Based on rect node, we extend a custom node with white filling. It will be turned to red when the mouse clicks it. Implement it by the code below:
// Extend rect
G6.registerNode(
'custom',
{
// Response the states
setState(name, value, item) {
const group = item.getContainer();
const shape = group.get('children')[0]; // Find the first graphics shape of the node. It is determined by the order of being added
if (name === 'selected') {
if (value) {
shape.attr('fill', 'red');
} else {
shape.attr('fill', 'white');
}
}
},
},
'rect',
);
// Click to select, cancel by clicking again
graph.on('node:click', ev => {
const node = ev.item;
graph.setItemState(node, 'selected', !node.hasState('selected')); // Switch the selected state
});
G6 does not limit the states for nodes/edges, you can assign any states to a node once you response it in the setState
function. e.g. magnify the node by hovering:
G6.registerNode(
'custom',
{
// Response the states change
setState(name, value, item) {
const group = item.getContainer();
const shape = group.get('children')[0]; // Find the first graphics shape of the node. It is determined by the order of being added
if (name === 'running') {
if (value) {
shape.animate(
{
r: 20,
repeat: true,
},
1000,
);
} else {
shape.stopAnimate();
shape.attr('r', 10);
}
}
},
},
'circle',
);
// Activate 'running' by mouse entering. Turn it of by mouse leaving.
graph.on('node:mouseenter', ev => {
const node = ev.item;
graph.setItemState(node, 'running', true);
});
graph.on('node:mouseleave', ev => {
const node = ev.item;
graph.setItemState(node, 'running', false);
});