Custom Node

16 min read

G6 provides abundant Built-in Nodes, including circle, [rect](/en/docs/manual/middle/elements/nodes/built-in/rect, ellipse, diamond, triangle, star, image, modelRect. Besides, the custom machanism allows the users to design their own type of nodes by G6.registerNode(typeName: string, nodeDefinition: object, extendedTypeName?: string). A node with complex graphics shapes, complex interactions, fantastic animations can be implemented easily. For the parameters:

  • typeName: the name of the new node type;
  • extendedTypeName: The name of the existing type that will be extended, which can be a built-in node type, or an existing custom node type. When it is not assigned, the custom node will not extend any existing node type;
  • nodeDefinition: The definition of the new node type. The required options can be found at Custom Mechanism API. When the extendedTypeName is assigned, the functions which are not rewritten will extend from the type with name extendedTypeName.

Noted that if the extendedTypeName is assigned, the required functions such as draw, update, and setState will extend from extendedTypeName unless they are rewritten in nodeDefinition. Due to this mechanism, a question is often fed back:

  • Q: when the custom node/edge is updated, the re-draw logic is not the same as draw or drawShape function defined in nodeDefinition. e.g., some shapes are not updated as expected, and some text shapes show up.
  • A: Since the extendedTypeName is assigned, and the update is not implemented in extendedTypeName, the update of the extended node type will be called when updating the node/edge, whose logic might be different from the draw or drawShape defined by yourself. To avoid this problem, you can override the update by undefined in nodeDefinition. When update is undefined, the draw or drawShape will be called when updating the node/edge.
In this document, we will introduce the custom node mechanism by five examples:
1. Register a brand 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 5. Custom Node with DOM

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 Graphics group, the container of the shapes 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 Graphics group, the container of the shapes 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) {},
    /**
     * Should be rewritten when you want to response the state changes by animation.
     * Responsing the state changes by styles can be configured, which is described in the document Middle-Behavior & Event-State
     * @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) {},
  },
  extendedNodeName,
);
  ⚠️Attention:
  • draw: it is required if the custom node does not extend any parent;
  • Coordinate system: The coordinate system of the shapes inside the custom node is a sub coordinate system relating to itself, which means the (0, 0) is the center of the node. And the coordinates of the node is related to the whole canvas, which is controled by the group contains it and users have no need to use it when customing a node type. When adding a rect shape into a custom node, be caution that its x and y should be minused half of its width and height. See the detail in Register a Bran-new Node;
  • update:

    • When the update function is not undefined: If user has defined the third parameter extendedNodeName of registerNode, which means extending a built-in node type, the update function of the extended node type of the custom node will be executed once the node is updated; If the third parameter of registerNode is not assigned, the draw function of the custom node will be executed instead;
    • When the update function is defined, whether the third parameter of registerNode is defined, the update function will be executed when the node is updated.
  • afterDraw and afterUpdate: they are used for extending the exited nodes in general. e.g. adding extra image on rect node, adding animation on a circle node, ...;
  • setState should be override when you want to response the state changes by animation. Responsing the state changes by simple styles can be achieved by Configure Styles for State;
  • getAnchorPoints: it 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 Brand New Node

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 override it for demonstration.

img
  ⚠️ Attention: From the following code, you will understand that the coordinates of the sub shapes of the custom node is related to itself, which means the (0, 0) is the center of the node. E.g. the x and y of the 'text' shape are both 0, which means the shape is on the center of the node; The path attribute of 'path' is also defined with the origin (0, 0). In the other words, users do not need to control the sub shapes' coordinates according to the nodes' coordinate which is controlled by the matrix of the parent group of the node.
G6.registerNode('diamond', {
  draw(cfg, group) {
    // If there is style object in cfg, it should be mixed here
    const keyShape = 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
      },
      // must be assigned in G6 3.3 and later versions. it can be any value you want
      name: 'path-shape',
      // allow the shape to response the drag events
      draggable: true
    });
    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;
      const label group.addShape('text', {
        attrs: {
          x: 0, // center
          y: 0,
          textAlign: 'center',
          textBaseline: 'middle',
          text: cfg.label,
          fill: '#666',
        },
        // must be assigned in G6 3.3 and later versions. it can be any value you want
        name: 'text-shape',
        // allow the shape to response the drag events
        draggable: true
      });
    }
    return keyShape;
  },
  // 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. Attention: you need to assign name and draggable for the shapes added in the custom node, where the name can be not unique with any value you want. draggable: true means that the shape is allowed to response the drag events. Only when draggable: true, the interact behavior 'drag-node' can be responsed on this shape. In the codes above, if you only assign draggable: true to the keyShape but not the label, the drag events will only be responsed on the keyShape.

The following code uses the diamond node:

const data = {
  nodes: [
    { id: 'node1', x: 50, y: 100, type: 'diamond' }, // The simplest form
    { id: 'node2', x: 150, y: 100, type: 'diamond', size: [50, 100] }, // Add the size
    { id: 'node3', x: 250, y: 100, color: 'red', type: 'diamond' }, // Add the color
    { id: 'node4', x: 350, y: 100, label: '菱形', type: 'diamond' }, // Add the label
  ],
};
const graph = new G6.Graph({
  container: 'mountNode',
  width: 500,
  height: 500,
});
graph.data(data);
graph.render();
img

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, override 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 of draw;
  • 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-node is the base class of all the node types, you can also extend it. (single-edge is the base class of all the edge types.)

For example, we are going to extend the single-node. draw, update, and setState have been implemented in the single-node. Thus, we only override the getShapeStyle, which returns the path and the styles of graphics shapes.

G6.registerNode(
  'diamond',
  {
    draw(cfg, group) {
      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 = G6.Util.mix(
        {},
        {
          path: path,
          stroke: color,
        },
        cfg.style,
      );
      // add a path as keyShape
      const keyShape = group.addShape('path', {
        attrs: {
          ...style,
        },
        draggable: true,
        name: 'diamond-keyShape',
      });
      // return the keyShape
      return keyShape;
    },
  },
  // Extend the 'single-node'
  'single-node',
);

Add Animation

We are going to add animation by afterDraw in this section. The result:
img

  • 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,
        },
        // must be assigned in G6 3.3 and later versions. it can be any value you want
        name: 'image-shape',
      });
      // Execute the animation
      image.animate(
        (ratio) => {
          const matrix = Util.mat3.create();
          const toMatrix = Util.transform(matrix, [['r', ratio * Math.PI * 2]]);
          return {
            matrix: toMatrix,
          };
        },
        {
          repeat: true,
          duration: 3000,
          easing: '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.

img img

(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:

  1. Add a flag on the node data, control the style according to the flag in draw when registering a custom node;
  2. 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:
img

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,
              duration: 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);
});

5. Custom Node with DOM

SVG and DOM shape are not supported in V3.3.x. DOM node is available only when the renderer of the graph instance is 'svg'.

Here, we demonstrate customing a node named 'dom-node' with DOM. We add a 'dom' type shape with group.addShape in draw function, and set the html of it to be the html value we want.

img
G6.registerNode(
  'dom-node',
  {
    draw: (cfg: ModelConfig, group: Group) => {
      return group.addShape('dom', {
        attrs: {
          width: cfg.size[0],
          height: cfg.size[1],
          // DOM's html
          html: `
        <div style="background-color: #fff; border: 2px solid #5B8FF9; border-radius: 5px; width: ${
          cfg.size[0] - 5
        }px; height: ${cfg.size[1] - 5}px; display: flex;">
          <div style="height: 100%; width: 33%; background-color: #CDDDFD">
            <img alt="img" style="line-height: 100%; padding-top: 6px; padding-left: 8px;" src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*Q_FQT6nwEC8AAAAAAAAAAABkARQnAQ" width="20" height="20" />  
          </div>
          <span style="margin:auto; padding:auto; color: #5B8FF9">${cfg.label}</span>
        </div>
          `,
        },
        draggable: true,
      });
    },
  },
  'single-node',
);

Now, we have 'dom-node' type of node with DOM. Be attention that you should assign name and draggable for the shapes you added after V3.3, where name is an ununique string. The shape is allowed to be dragged when draggable is true.

We render the graph with 'dom-node' as following:

img
const data = {
  nodes: [
    { id: 'node1', x: 50, y: 100 },
    { id: 'node2', x: 150, y: 100 },
  ],
  edges: [(source: 'node1'), (target: 'node2')],
};
const graph = new G6.Graph({
  container: 'mountNode',
  width: 500,
  height: 500,
  defaultNode: {
    type: 'dom-node',
    size: [120, 40],
  },
});
graph.data(data);
graph.render();
⚠️ Attention: DOM Shape in G6 does not support the events on Node and Edge. You can bind events for DOM as the way in HTML. e.g.:
G6.registerNode(
  'dom-node',
  {
    draw: (cfg: ModelConfig, group: Group) => {
      return group.addShape('dom', {
        attrs: {
          width: cfg.size[0],
          height: cfg.size[1],
          // DOM's html with onclick event
          html: `
        <div onclick="alert('Hi')" style="background-color: #fff; border: 2px solid #5B8FF9; border-radius: 5px; width: ${
          cfg.size[0] - 5
        }px; height: ${cfg.size[1] - 5}px; display: flex;">
          <div style="height: 100%; width: 33%; background-color: #CDDDFD">
            <img alt="img" style="line-height: 100%; padding-top: 6px; padding-left: 8px;" src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*Q_FQT6nwEC8AAAAAAAAAAABkARQnAQ" width="20" height="20" />  
          </div>
          <span style="margin:auto; padding:auto; color: #5B8FF9">${cfg.label}</span>
        </div>
          `,
        },
        draggable: true,
      });
    },
  },
  'single-node',
);