基类 Cell

阅读时间约 7 分钟

快速上手案例中,我们通过 JSON 数据添加了两个矩形节点和一条边到画布中,除此之外,我们在 X6 的 Shape 命名空间中内置了一些基础图形,如 RectEdgeCircle 等,这些图形最终都有共同的基类 Cell,定义了节点和边共同属性和方法,如属性样式、可见性、业务数据等,并且在实例化、定制样式、配置默认选项等方面具有相同的行为。看下面的继承关系。

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

我们可以使用这些图形的构造函数来创建节点/边,然后调用 graph.addNodegraph.addEdge 方法将其添加到画布。

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)

这些构造函数都有一些来自 Cell 的基础选项,如 idattrszIndex 等,下面我们就逐个看看这些基础选项的含义。

基础选项

选项名类型默认值描述
idStringundefined节点/边的唯一标识,默认使用自动生成的 UUID。
markupMarkupundefined节点/边的 SVG/HTML 片段。
attrsObject{ }节点/边属性样式。
shapeStringundefined渲染节点/边的图形。
viewStringundefined渲染节点/边的视图。
zIndexNumberundefined节点/边在画布中的层级,默认根据节点/边添加顺序自动确定。
visibleBooleantrue节点/边是否可见。
parentStringundefined父节点。
childrenString[]undefined子节点/边。
dataanyundefined节点/边关联的业务数据。

id

id 是节点/边的唯一标识,推荐使用具备业务意义的 ID,默认使用自动生成的 UUID。

markup

markup 指定了渲染节点/边时使用的 SVG/HTML 片段,使用 JSON 格式描述。例如 Shape.Rect 节点的 markup 定义如下。

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

表示该节点内部包含 <rect><text> 两个 SVG 元素,渲染到页面之后,节点对应的 SVG 元素看起来像下面这样。

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

通过上面的介绍,我们大致了解了 Markup 的结构,下面我们将详细介绍 Markup 定义。

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 元素标签名。

ns

tagName 对应的元素命名空间,默认使用 SVG 元素命名空间 "http://www.w3.org/2000/svg",当 tagName 指定的标签是 HTML 元素时,需要使用 HTML 元素的命名空间 "http://www.w3.org/1999/xhtml"

selector

SVG/HTML 元素的唯一标识,通过该唯一标识为该元素指定属性样式。例如,为 Shape.Rect 节点指定 <rect><text> 元素的属性样式。

const rect = new Shape.Rect({
  x: 40,
  y: 40,
  width: 100,
  height: 40,
  attrs: {
    // 指定 rect 元素的样式
    body: { 
      stroke: '#000', // 边框颜色
      fill: '#fff',   // 填充颜色
    },
    // 指定 text 元素的样式
    label: { 
      text: 'rect', // 文字
      fill: '#333', // 文字颜色
    },
  },
})

groupSelector

群组选择器,通过群组选择器可以为该群组对应的多个元素指定样式。例如,下面定义中两个 <rect> 具备相同的 groupSelector'group1'

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

创建节点时,我们可以像下面这样来指定群组样式

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

attrs

该 SVG/HTML 元素的默认属性键值对,通常用于定义那些不变的通用属性,这些默认样式也可以在实例化节点时被覆盖。需要注意的是,markupattrs 属性只支持原生的 SVG 属性,也就是说 X6 的自定义属性在这里不可用。

例如,我们为 Shape.Rect 节点的 <rect><text> 元素指定了如下默认样式。

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

style

该 SVG/HTML 元素的行内样式键值对。

className

该 SVG/HTML 元素的 CSS 样式名。

textContent

该 SVG/HTML 元素的文本内容。

children

嵌套的子元素。

attrs

快速上手中,我们简单介绍了如何使用 attrs 选项定制节点样式,attrs 选项是一个复杂对象,该对象的 Key 是节点中 SVG 元素的选择器(Selector),对应的值是应用到该 SVG 元素的 SVG 属性值(如 fillstroke),如果你对 SVG 属性还不熟悉,可以参考 MDN 提供的填充和边框入门教程。

选择器(Selector)通过节点的 markup 确定,如 Shape.Rect 节点定义了 'body'(代表 <rect> 元素) 和 'label'(代表 <text> 元素) 两个选择器。

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,
    },
  },
})

节点渲染到画布后的 DOM 结构看起来像下面这样。

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

另外,我们还可以使用 CSS 选择器来指定节点样式,这样我们就不用记住预定的选择器名称,只需要根据渲染后的 DOM 结构来定义样式即可。使用 CSS 选择器时需要注意,指定的 CSS 选择器可能选中多个元素,这时对应的属性样式将同时应用到多个元素上。

const rect = new Shape.Rect({
  x: 40,
  y: 40,
  width: 100,
  height: 40,
  attrs: { 
    rect: { // 使用 rect css 选择器替代预定义的 body 选择器
      fill: '#2ECC71',
      stroke: '#000',
    },
    text: { // 使用 text css 选择器替代预定义的 label 选择器
      text: 'rect',
      fill: '#333',
      fontSize: 13,
    },
  },
})

值得一提的是,支持使用小驼峰(camelCase)格式的属性名,如 'fontSize',这就避免了 'font-size' 这种属性名作为对象 Key 时需要加引号的书写麻烦。

除了标准的 SVG 属性,我们在 X6 中还定义了一系列特殊属性,详情请参考如何使用特殊属性如何自定义属性。另外,我们还可以使用 CSS 来定制样式,节点和边渲染到画布后分别有 'x6-node''x6-edge' 两个样式名,默认的样式定义参考这里。例如,我们可以像下面这样来指定节点中 <rect> 元素的样式:

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

创建节点/边后,我们可以调用实例上的 attr() 方法来修改节点属性样式。看下面代码,通过 / 分割的路径修改样式,label 选择器对应到 <text> 元素,text 则是该元素的属性名,'hello' 是新的属性值。

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

// 等同于
rect.attr('label', {
  text: 'hello'
})

// 等同于
rect.attr({
  label: {
    text: 'hello'
  }
})

当传入的属性值为 null 时可以移除该属性。

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

shape

节点/边的图形,类似 MVC 模式中的 Model,决定了节点/边的数据逻辑,通常配合 graph.addNodegraph.addEdge 两个方法使用。之前的介绍中都是使用节点/边的构造函数来创建节点/边,其实 graph 上也提供了 graph.addNodegraph.addEdge 两个便捷的方法来创建节点/边并将其添加到画布。

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,
})

这里的关键是使用 shape 来指定了节点/边的图形,graph.addNode 方法中 shape 的默认值为 'rect'graph.addEdge 方法中 shape 的默认值为 'edge',其他选项与使用构造函数创建节点/边一致。在 X6 内部实现中,我们通过 shape 指定的图形找到对应的构造函数来初始化节点/边,并将其添加到画布。

内置节点

内置节点构造函数与 shape 名称对应关系如下表。

构造函数shape 名称描述
Shape.Rectrect矩形。
Shape.Circlecircle圆形。
Shape.Ellipseellipse椭圆。
Shape.Polygonpolygon多边形。
Shape.Polylinepolyline折线。
Shape.Pathpath路径。
Shape.Imageimage图片。
Shape.HTMLhtmlHTML 节点,使用 foreignObject 渲染 HTML 片段。
Shape.TextBlocktext-block文本节点,使用 foreignObject 渲染文本。
Shape.BorderedImageimage-bordered带边框的图片。
Shape.EmbeddedImageimage-embedded内嵌入矩形的图片。
Shape.InscribedImageimage-inscribed内嵌入椭圆的图片。
Shape.Cylindercylinder圆柱。

内置边

内置边构造函数与 shape 名称对应关系如下表。

构造函数shape 名称描述
Shape.Edgeedge边。
Shape.DoubleEdgedouble-edge双线边。
Shape.ShadowEdgeshadow-edge阴影边。

除了使用 X6 的内置节点/边,我们还可以注册自定义节点/边并使用他们,想了解更多请参考自定义节点自定义边教程。

view

指定渲染节点/边所使用的视图,视图的概念与 MVC 模式中的 View 一致,我们将在自定义节点自定义边教程中做详细介绍。

zIndex

节点/边在画布中的层级,默认根据节点/边添加顺序自动确定。节点/边渲染到画布后可以通过 cell.getZIndex()cell.setZIndex(z: number) 来获取或设置 zIndex 的值,也可以调用 cell.toFront()cell.toBack() 来将其移到最顶层或对底层。

visible

节点/边是否可见。

parent

父节点 ID。

children

子节点/边的 ID 数组。

data

与节点/边关联的业务数据。例如,我们在实际使用时通常会将某些业务数据存在节点/边的 data 上。

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

选项默认值

Cell 类提供了一个静态方法 Cell.config(options) 来配置选项的默认值,选项默认值对自定义节点/边非常友好,可以为我们的自定义节点/边指定预设的默认值。例如,我们在定义矩形节点时,为其指定了默认 Markup、默认大小和默认样式。

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',
    }
  },
})

默认选项可以简化我们添加节点的代码,例如,只需要指定矩形节点的位置和文本就可以添加一个矩形到画布。

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

每次调用 config(options) 都是与当前预设值进行深度 merge,例如下面代码分别将矩形的边框默认颜色修改为红色和将默认文本颜色修改为蓝色,最终效果是两者的叠加。

// 只修改边框的默认颜色
Shape.Rect.config({
  attrs: {
    body: {
      stroke: 'red',
    },
  },
})

// 只修改默认文本颜色
Shape.Rect.config({
  attrs: {
    label: {
      fill: 'blue',
      // 覆盖上面定义的 red
      stroke: '#000',
    },
  },
})

自定义选项

也许你已经注意到,在之前创建矩形的代码中,我们使用了 label 选项来设置矩形的标签文本。

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

这并不是什么新魔法,我们只是在定义矩形时通过定义 propHooks 钩子来消费自定义选项,看下面 label 选项钩子的实现细节。

Shape.Rect.config({
  // 通过钩子将 label 应用到 'attrs/text/text' 属性上
  propHooks(metadata) {
    const { label, ...others } = metadata
    if (label) {
      ObjectExt.setByPath(others, 'attrs/text/text', label)
    }
    return others
  },
})

通过 propHooks 钩子,我们很容易就扩展出一些自定义的选项。例如,我们可以将某些样式定义为节点的选项,这样不仅可以减少嵌套,而且使创建节点的代码语义性更强。

看下面的代码,为矩形定义 rxry 自定义选项。

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
    },
  },
})

这样,我们就可以很方便添加圆角矩形。

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