基类 Cell

13 min read

In the Quick Start case, we added two rectangle nodes and an edge to the canvas via JSON data, in addition to that, we built some basic shapes in X6's Shape namespace, such as Rect, Edge, Circle etc. These shapes end up with a common base class Cell that defines node and edge common properties and methods such as property styles, visibility, business data, etc. and have the same behavior in terms of instantiation, customizing styles, configuring default options, etc. See the inheritance relationship below.

                                     ┌──────────────────┐
                                 ┌──▶│ Shape.Rect       │
                                 │   └──────────────────┘
                                 │   ┌──────────────────┐
                                 ├──▶│ Shape.Circle     │
                     ┌────────┐  │   └──────────────────┘
                  ┌─▶│  Node  │──┤   ┌──────────────────┐
                  │  └────────┘  ├──▶│ Shape.Ellipse    │
                  │              │   └──────────────────┘
                  │              │   ┌──────────────────┐
                  │              └──▶│ Shape.Xxx...     │
      ┌────────┐  │                  └──────────────────┘
      │  Cell  │──┤                                      
      └────────┘  │                  ┌──────────────────┐
                  │              ┌──▶│ Shape.Edge       │
                  │              │   └──────────────────┘
                  │  ┌────────┐  │   ┌──────────────────┐
                  └─▶│  Edge  │──┼──▶│ Shape.DoubleEdge │
                     └────────┘  │   └──────────────────┘
                                 │   ┌──────────────────┐
                                 └──▶│ Shape.ShadowEdge │
                                     └──────────────────┘

We can use the constructors of these graphs to create nodes/edges and then call the graph.addNode or graph.addEdge methods to add them to the canvas.

import { Shape } from '@antv/x6'

const rect = new Shape.Rect({
  id: 'node1',
  x: 40,
  y: 40,
  width: 100,
  height: 40,
  label: 'rect', 
  zIndex: 2,
})

const circle = new Shape.Circle({
  id: 'node2',
  x: 280,
  y: 200,
  width: 60,
  height: 60,
  label: 'circle', 
  zIndex: 2,
})

const edge = new Shape.Edge({
  id: 'edge1',
  source: rect,
  target: circle,
  zIndex: 1,
})

graph.addNode(rect)
graph.addNode(circle)
graph.addEdge(edge)

These constructors all have some base options from Cell, such as id, attrs, zIndex, etc. Let's look at the meaning of each of these base options.

Basic Options

Option NameTypeDefaultDescription
idStringundefinedThe unique identifier of the node/edge, the default is the automatically generated UUID.
markupmarkupundefinedThe SVG/HTML fragment of the node/edge.
attrsObject{ }The node/edge property style.
shapeStringundefinedRender the node/edge's graphics.
viewStringundefinedRenders the view of the node/edge.
zIndexNumberundefinedThe level of the node/edge in the canvas, determined automatically by default based on the node/edge addition order.
visibleBooleantrueIf or not the node/edge is visible.
parentStringundefinedThe parent of the node.
childrenString[]undefinedThe children's nodes/edges.
dataanyundefinedThe business data associated with the node/edge.

id

id is the unique identifier of the node/edge, it is recommended to use an ID with business meaning, the default is the automatically generated UUID.

markup

markup specifies the SVG/HTML fragment to be used when rendering the node/edge, described in JSON format. For example, the markup definition for the `Shape.

{
  markup: [
    {
      tagName: 'rect',
      selector: 'body',
    }, 
    {
      tagName: 'text',
      selector: 'label',
    },
  ],
}

means that the node contains <rect> and <text> SVG elements inside the node. After rendering to the page, the node's corresponding SVG element looks like the following.

<g data-cell-id="c2e1dd06-15c6-43a4-987a-712a664b8f85" class="x6-cell x6-node" transform="translate(40,40)">
  <rect fill="#fff" stroke="#000" stroke-width="2" fill-opacity="0.5" width="100" height="40"></rect>
  <text font-size="14" xml:space="preserve" fill="#333" text-anchor="middle" font-family="Arial, helvetica, sans-serif" transform="matrix(1,0,0,1,50,20)">
    <tspan dy="0.3em" class="v-line">rect</tspan>
  </text>
</g>

Through the above introduction, we have a general understanding of the structure of Markup, and in the following we will introduce the Markup definition in detail.

interface Markup {
  tagName: string
  ns?: string
  selector?: string
  groupSelector?: string | string[]
  attrs?: { [key: string]: string | number }
  style?: { [key: string]: string | number }
  className?: string | string[]
  textContent?: string
  children?: Markup[]
}

tagName

SVG/HTML element tag name.

ns

The element namespace corresponding to tagName uses the SVG element namespace "http://www.w3.org/2000/svg" by default, and when the tag specified by tagName is an HTML element, you need to use the HTML element namespace "http://www.w3.org/1999/xhtml".

selector

A unique identifier for an SVG/HTML element by which to specify attribute style for that element. For example, specifying the attribute style for the <rect> and <text> elements for the `Shape.

const rect = new Shape.Rect({
  x: 40,
  y: 40,
  width: 100,
  height: 40,
  attrs: {
    // Specify the style of the rect element
    body: { 
      stroke: '#000', // border color
      fill: '#fff', // fill color
    },
    // Specify the style of the text element
    label: { 
      text: 'rect', // text
      fill: '#333', // text color
    },
  },
})

groupSelector

A group selector allows you to specify styles for multiple elements corresponding to the group. For example, the two <rect>s in the definition below have the same groupSelector value 'group1'.

{
  markup: [
    {
      tagName: 'rect',
      selector: 'body',
      groupSelector: 'group1',
    }, 
    {
      tagName: 'rect',
      selector: 'wrap',
      groupSelector: 'group1',
    }, 
    {
      tagName: 'text',
      selector: 'label',
    },
  ],
}

When creating a node, we can specify the group style like this

new SomeNode({
  attrs: { 
    group1: {
      fill: '#2ECC71',
    },
  },
})

attrs

The default attribute key-value pairs for this SVG/HTML element are typically used to define those invariant generic attributes, and these default styles can also be overridden when the node is instantiated. Note that the attrs attribute of markup only supports native SVG attributes, which means that X6's custom attributes are not available here.

For example, we specify the following default style for the <rect> and <text> elements of the `Shape.

{
  markup: [
    {
      tagName: 'rect',
      selector: 'body',
      attrs: {
        fill: '#fff',
        stroke: '#000',
        strokeWidth: 2,
      }
    }, 
    {
      tagName: 'text',
      selector: 'label',
      attrs: {
        fill: '#333',
        textAnchor: 'middle',
        textVerticalAnchor: 'middle',
      }
    },
  ],
}

style

The in-line style key-value pair for this SVG/HTML element.

className

The CSS style name of the SVG/HTML element.

textContent

The text content of the SVG/HTML element.

children

Nested child elements.

attrs

In Quick Start, we briefly described how to customize node styles using the attrs option, which is a complex object whose Key is the Selector of the SVG element in the node, and whose corresponding value is the SVG attribute value applied to that SVG attribute value](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute) applied to that SVG element (e.g. fill SVG/Attribute/fill) and stroke). If you are not familiar with SVG attributes, you can refer to the fill and border for a tutorial to get started.

Selector is determined by the node's markup, e.g. the Shape.Rect node defines the 'body' (representing the <rect> element) and 'label' (representing the <text> element) selectors.

const rect = new Shape.Rect({
  x: 40,
  y: 40,
  width: 100,
  height: 40,
  attrs: { 
    body: {
      fill: '#2ECC71',
      stroke: '#000',
    },
    label: {
      text: 'rect',
      fill: '#333',
      fontSize: 13,
    },
  },
})

The DOM structure of the node after it is rendered to the canvas looks like the following.

<g data-cell-id="3ee1452c-6d75-478d-af22-88e03c6d513b" class="x6-cell x6-node" transform="translate(40,40)">
  <rect fill="#2ECC71" stroke="#000" stroke-width="2" width="100" height="40"></rect>
  <text font-size="13" xml:space="preserve" fill="#333" text-anchor="middle" font-family="Arial, helvetica, sans-serif" transform="matrix(1,0,0,1,50,20)">
    <tspan dy="0.3em" class="v-line">
      rect
    </tspan>
  </text>
</g>

Alternatively, we can use CSS selectors to specify node styles so that we don't have to remember the intended selector name and can simply define the style based on the rendered DOM structure. When using CSS selectors, it is important to note that multiple elements may be selected by the specified CSS selector, and the corresponding property styles will be applied to multiple elements at the same time.

const rect = new Shape.Rect({
  x: 40,
  y: 40,
  width: 100,
  height: 40,
  attrs: { 
    rect: { // Use the rect css selector instead of the predefined body selector
      fill: '#2ECC71',
      stroke: '#000',
    },
    text: { // Use the text css selector instead of the predefined label selector
      text: 'rect',
      fill: '#333',
      fontSize: 13,
    },
  },
})

It is worth mentioning that property names in camelCase format, such as 'fontSize', are supported, which avoids the trouble of writing property names like 'font-size' in quotes when they are used as object keys.

In addition to the standard SVG attributes, we have defined a series of special attributes in X6, please refer to How to use special attributes and How to customize attributes. Also, we can use CSS to customize the styles. Nodes and edges are rendered to the canvas with 'x6-node' and 'x6-edge' style names, respectively, and the default style definitions refer here. For example, we can specify the style of the <rect> element in a node like this.

.x6-node rect {
  fill: #2ECC71;
  stroke: #000;
}

After creating the node/edge, we can call the attr() method on the instance to modify the node attribute style. Look at the following code, the style is modified by the / split path, the label selector corresponds to the <text> element, text is the attribute name of the element, and 'hello' is the new attribute value.

rect.attr('label/text', 'hello')

// Equivalent to
rect.attr('label', {
  text: 'hello'
})

// Equivalent to
rect.attr({
  label: {
    text: 'hello'
  }
})

The property can be removed when the value passed in is null.

rect.attr('label/text', null)

shape

The graph of a node/edge, similar to a Model in the MVC pattern, determines the data logic of the node/edge and is usually used in conjunction with the graph.addNode and graph.addEdge methods. In fact, graph also provides two convenient methods graph.addNode and graph.addEdge to create nodes/edges and add them to the canvas.

const rect = graph.addNode({
  shape: 'rect',
  x: 100,
  y: 200,
  width: 80,
  height: 40,
  label: 'rect', 
})

const circle = graph.addNode({
  shape: 'circle',
  x: 280,
  y: 200,
  width: 60,
  height: 60,
  label: 'circle', 
  zIndex: 2,
})

const edge = graph.addEdge({
  shape: 'edge',
  source: rect,
  target: circle,
})

The key here is the use of shape to specify the graph of the node/edge. The default value of shape in the graph.addNode method is 'rect', the default value of shape in the graph.addEdge method is 'edge', and the other options are the same as using the constructor to create the node/edge. In the X6 internal implementation, we initialize nodes/edges and add them to the canvas by finding the corresponding constructor for the graph specified by shape.

Built-in Nodes

The built-in node constructors correspond to the shape names in the following table.

constructorshape namedescription
Shape.RectrectRectangle.
Shape.CirclecircleCircle.
Shape.EllipseellipseEllipse.
Shape.Polygonpolygonpolygon.
Shape.PolylinepolylinePolyline.
Shape.Pathpathpath.
Shape.ImageimageImage.
Shape.HTMLhtmlHTML node that uses foreignObject to render HTML fragments.
Shape.TextBlocktext-blockText node, use foreignObject to render the text.
Shape.BorderedImageimage-borderedPictures with borders.
Shape.EmbeddedImageimage-embeddedImage with the embedded rectangle.
Shape.InscribedImageimage-inscribedEmbedded ellipse image.
Shape.CylindercylinderCylinders.

Built-in side

The built-in edge constructors correspond to the shape names in the following table.

constructorshape namedescription
Shape.EdgeedgeSide.
Shape.DoubleEdgedouble-edgeDouble line edge.
Shape.ShadowEdgeshadow-edgeShaded edges.

Besides using X6's built-in nodes/edges, we can also register custom nodes/edges and use them. To learn more, please refer to the custom-node and custom-edge tutorials.

view

Specify the view used to render the node/edge. The concept of view is the same as View in MVC pattern, which we will introduce in detail in custom-node and custom-edge tutorials.

zIndex

The node/edge hierarchy in the canvas is automatically determined by default based on the order in which the nodes/edges are added. The value of zIndex can be obtained or set by cell.getZIndex() and cell.setZIndex(z: number) after the node/edge is rendered to the canvas, and it can also be moved to the top or bottom layer by calling cell.toFront() and cell.toBack().

visible

If or not the node/edge is visible.

parent

The parent node ID.

children

ID array of children/edges.

data

The business data associated with a node/edge. For example, we usually store some business data on the data of the node/edge when we use it in practice.

const rect = new Shape.Rect({
  x: 40,
  y: 40,
  width: 100,
  height: 40,
  data: { 
    bizID: 125,
    date: '20200630',
    price: 89.00,
  }
})

Option default value

The Cell class provides a static method Cell.config(options) to configure the option defaults. The option defaults are very friendly to custom nodes/edges and can specify pre-defined defaults for our custom nodes/edges. For example, when we define a rectangular node, we specify the default Markup, default size and default style for it.

Shape.Rect.config({
  width: 80,
  height: 40,
  markup: [
    {
      tagName: 'rect',
      selector: 'body',
    }, 
    {
      tagName: 'text',
      selector: 'label',
    },
  ],
  attrs: {
    body: {
      fill: '#fff',
      stroke: '#000',
      strokeWidth: 2,
    },
    label: {
      fontSize: 14,
      fill: '#333',
      fontFamily: 'Arial, helvetica, sans-serif',
      textAnchor: 'middle',
      textVerticalAnchor: 'middle',
    }
  },
})

The default option simplifies our code for adding nodes, for example, by simply specifying the location and text of the rectangle node to add a rectangle to the canvas.

const rect = graph.addNode({
  x: 100,
  y: 100,
  attrs: {
    label: {
      text: 'rect',
    },
  },
})

Each call to config(options) is a depth merge with the current preset, for example, the following code changes the default color of the border of the rectangle to red and the default text The final effect is a superposition of the two.

// Change only the default color of the border
Shape.Rect.config({
  attrs: {
    body: {
      stroke: 'red',
    },
  },
})

// Change only the default text color
Shape.Rect.config({
  attrs: {
    label: {
      fill: 'blue',
      // Override the red defined above
      stroke: '#000',
    },
  },
})

Customization Options

You may have noticed that in the previous code that created the rectangle, we used the label option to set the label text of the rectangle.

const rect = graph.addNode({
  x: 100,
  y: 100,
  label: 'rect',
})

This is not new magic, we just consume custom options by defining propHooks hooks when defining rectangles, see the implementation details of label option hooks below.

Shape.Rect.config({
  // Apply the 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
  },
})

With the propHooks hook, we can easily extend some custom options. For example, we can define certain styles as options for nodes, which not only reduces nesting, but also makes the code for creating nodes more semantic.

Look at the following code to define rx and ry customization options for rectangles.

Shape.Rect.config({
  propHooks: {
    rx(metadata) { 
      const { rx, ...others } = metadata
      if (rx != null) {
        ObjectExt.setByPath(others, 'attrs/body/rx', rx)
      }
      return others
    },
    ry(metadata) { 
      const { ry, ...others } = metadata
      if (ry != null) {
        ObjectExt.setByPath(others, 'attrs/body/ry', ry)
      }
      return others
    },
  },
})

This way, we can easily add rounded rectangles.

const rect = graph.addNode({
  x: 100,
  y: 100,
  rx: 5,
  ry: 10,
  label: 'rect',
})