自定义节点

7 min read

We have some basic graphics built into X6, such as Rect, Circle and Ellipse, but these are far from enough to meet our practical needs, and we need to be able to define nodes that have business implications, such as the table node in the ER diagram. Customizing nodes is not that hard, it's just a combination of <rect>, <circle>, <ellipse>, <image>, <text>, <path> and other basic elements in SVG, if you are not familiar with these basic elements, you can refer to the tutorial provided by MDN , and use these base elements to define any graphics we want.

Principle

Custom nodes actually derive (inherit) our own nodes from the base node and override certain options and methods of the base class.

Three-step approach

Taking the built-in node Rect as an example, customizing the node can be done in the following three steps.

Step 1: Inheritance

import { Node } from '@antv/x6'

class Rect extends Node { 
  // Omit implementation details
}

Step 2: Configuration

Call the inherited static method config(options) to configure the default value of node options, custom options and custom attributes, the most common option is to specify the default SVG/HTML structure of the node by markup. The most common options are markup to specify the default SVG/HTML structure of the node, and attrs to specify the default attributes of the node. style.

NameTypeMandatoryDefaultDescription
propHooksFunction | Function[] | ObjectNoundefinedCustomization Options Hooks.
attrHooksObjectNoundefinedCustom Properties Hooks.
...othersObjectNode Options

See below for the default configuration of the Rect node.

Rect.config({
  width: 100,
  height: 40,
  markup: [
    {
      tagName: 'rect',
      selector: 'body',
    },
    {
      tagName: 'text',
      selector: 'label',
    },
  ],
  attrs: {
    body: {
      fill: '#ffffff',
      stroke: '#333333',
      strokeWidth: 2,
    },
    label: {
      fontSize: 14,
      fill: '#333333',
      refX: '50%',
      refY: '50%',
      textAnchor: 'middle',
      textVerticalAnchor: 'middle',
    },
  },
  // Apply the custom option label to the 'attrs/text/text' attribute via a hook
  propHooks(metadata) {
    const { label, ...others } = metadata
    if (label) {
      ObjectExt.setByPath(others, 'attrs/text/text', label)
    }
    return others
  },
})

In the above code, we specify the default size of the node by width and height, then we define the node to consist of <rect> and <text> SVG elements by markup, and specify the body and label selectors respectively, and then we can specify the default style of the node by these two selectors in attrs. to specify the default style of the node. Finally, a custom option label is defined by propHooks, so that we can set the label text by label.

Step 3: Register

Call Graph's static method registerNode to register the node and then use it as a built-in node.

Graph.registerNode(name: string, cls: typeof Node, overwrite?: boolean)
Parameter NameTypeMandatoryDefaultDescription
nameStringYesThe name of the registered node.
clstypeof NodeYesNode class, a class that inherits directly or indirectly from Node.
overwriteBooleanNofalseOverride or not when renaming, default is false Do not override (error when renaming).

For example, register the node named 'rect'.

Graph.registerNode('rect', Rect)

Once registered, we can use it like the following.

graph.addNode({
  shape: 'rect',
  x: 30,
  y: 40,
})

Handy method one

Sometimes we may not need to extend any method after inheriting a node, but just override some default style. For example, define a rectangle with a red border.

class RedRect extends Rect { }

// Override the default border color
RedRect.config({
  attrs: {
    body: {
      stroke: 'red',
    },
  },
})

The first line of code above is a bit awkward: it implements inheritance but doesn't extend any methods, which is a bit of an overkill. So we also provide a more convenient static method define to define this kind of node.

const RedRect = Rect.define({
  attrs: {
    body: {
      stroke: 'red',
    },
  },
})

Graph.registerNode('red-rect', RedRect)

This method takes its caller (e.g. Rect above) as the base class, inherits a new node, and then calls the new node's static method config to configure the default options.

Note that the class name of the RedRect class generated by the above code is not 'RedRect', but the system-generated class name, whose big camelCase form will be used as the class name of the new node when the constructorName option is specified.

const RedRect = Rect.define({
  constructorName: 'red-rect',
  attrs: {
    body: {
      stroke: 'red',
    },
  },
})

Graph.registerNode('red-rect', RedRect)

If we provide the shape option, then the system will automatically register the node for you. When the constructorName option is not specified, the large camelCase form of shape will also be the class name of the new node, i.e. the following code defines the class name of the node as 'RedRect'.

Rect.define({
  shape: 'red-rect', // Auto-register node named 'red-rect' and node class named 'RedRect'.
  attrs: {
    body: {
      stroke: 'red',
    },
  },
})

All options are consistent with those of the config method except for two special options, constructorName and shape. The following table shows the options supported by the define method.

NameMandatoryTypeDescription
constructorNameStringClass name.
shapeStringThe auto-registered node name, which will also be used as the class name when constructorName defaults to its big camelCase form.
...othersObjectOptions for config method.

Convenient method two

The Graph.registerNode method mentioned above has another signature, using which it is possible to define and register nodes at the same time.

Graph.registerNode(name: string, options: Object, overwrite?: boolean)
Parameter NameTypeMandatoryDefaultDescription
nameStringYesThe name of the registered node.
optionsObjectYesOptions.
overwriteBooleanNofalseOverride or not when renaming, default is false Do not override (error when renaming).

Specify the inherited base class by options.inherit, the default value is Node class, support string or node class, when options.inherit is string will automatically find the corresponding node from the registered nodes as the base class, other options of options are the same as define method The other options of options are the same as in define method. When options.constructorName is the class name default, the big camelCase form of the first parameter name will also be used as the class name of the custom node.

Graph.registerNode('red-rect', {
  inherit: Rect, // or 'straight' characters
  attrs: {
    body: {
      stroke: 'red',
    },
  },
})

Case

Next we define a rectangle CustomRect based on Shape.Rect, here we do not modify the markup definition of the rectangle, but just do the style override.

Use the convenient method one.

import { Shape } from '@antv/x6'

Shape.Rect.define({
  shape: 'custom-rect',
  width: 300, // Default width
  height: 40, // Default height
  attrs: {
    body: {
      rx: 10, // Rounded rectangle
      ry: 10,
      strokeWidth: 1,
      fill: '#5755a1',
      stroke: '#5755a1',
    },
    label: {
      fill: '#fff',
      fontSize: 18,
      refX: 10, // x-axis offset, similar to margin-left in css
      textAnchor: 'left', // Left Alignment
    }
  },
})

使用便捷方法二。

Graph.registerNode('custom-rect', {
  inherit: 'rect', // Inherited from Shape.
  width: 300, // Default width
  height: 40, // Default height
  attrs: {
    body: {
      rx: 10, // Rounded rectangle
      ry: 10,
      strokeWidth: 1,
      fill: '#5755a1',
      stroke: '#5755a1',
    },
    label: {
      fill: '#fff',
      fontSize: 18,
      refX: 10, // x-axis offset, similar to margin-left in css
      textAnchor: 'left', // Left Alignment
    }
  },
})

Then we can use it like this.

graph.addNode({
  x: 100,
  y: 60,
  shape: 'custom-rect',
  label: 'My Custom Rect', // label Custom options inherited from base class
});