自定义边

阅读时间约 4 分钟

我们在 X6 中内置了 EdgeDoubleEdgeShadowEdge 三种基础边,虽然 Edge 就能满足常见需求,但也避免不了偶然的定制需求。定制边与定制节点类似,但也有一些不同。

原理

自定义节点实际上是从基础边派生(继承)出我们自己的边,并覆盖基类的某些选项和方法。

三步法

以内置边 Edge 为例,自定义边可以分为以下三步。

第一步:继承

import { Edge as BaseEdge } from '@antv/x6'

class Edge extends BaseEdge { 
  // 省略实现细节
}

第二步:配置

调用继承的静态方法 config(options) 来配置边选项的默认值、自定义选项自定义属性,例如通过 markup 来指定边默认的 SVG/HTML 结构,通过 attrs 来指定边的默认属性样式,通过 defaultlabel 来指定边的默认标签样式。

名称类型是否必选默认值说明
propHooksFunction | Function[] | Objectundefined自定义选项钩子。
attrHooksObjectundefined自定义属性钩子。
...othersObject边选项

看下面 Edge 的默认配置。

Edge.config({
  markup: [
    {
      tagName: 'path',
      selector: 'wrap',
      attrs: {
        fill: 'none',
        cursor: 'pointer',
        stroke: 'transparent',
        strokeLinecap: 'round',
      },
    },
    {
      tagName: 'path',
      selector: 'line',
      attrs: {
        fill: 'none',
        pointerEvents: 'none',
      },
    },
  ],
  attrs: {
    wrap: {
      connection: true,
      strokeWidth: 10,
      strokeLinejoin: 'round',
    },
    line: {
      connection: true,
      stroke: '#333333',
      strokeWidth: 2,
      strokeLinejoin: 'round',
      targetMarker: {
        tagName: 'path',
        d: 'M 10 -5 0 0 10 5 z',
      },
    },
  },
})

上面代码中,我们通过 markup 定义了两个 <path> 元素,分别指定了 wrapline 选择器,并通过 markup 内部的 attrs 指定了基础样式(交互响应):wrap 指代的 <path> 元素默认透明不可见,用于响应用户交互,而 line 指代的 <path> 元素仅仅做视觉渲染,不响应用户交互。

然后在 attrs 上定义了边的默认属性样式:line 指代的 <path> 元素代表了边的主体,我们为其指定了 2px 宽度的黑色边框,并通过 targetMarker 特殊属性为其指定了一个终止箭头;wrap 指代的 <path> 元素用于响应用户交互,为了让鼠标更容易捕获到连线,我们为其指定了 10px 的边框。两者的 connection 特殊属性都为 true,表示两个 <path> 元素的 d 属性值都将根据边的 verticesrouterconnector 选项计算得到。

第三步:注册

调用 Graph 的静态方法 registerEdge 来注册边,注册以后就可以像使用内置边那样来使用边。

Graph.registerEdge(name: string, cls: typeof Edge, overwrite?: boolean)
参数名类型是否必选默认值说明
nameString注册的边名。
clstypeof Edge边类,直接或间接继承 Edge 的类。
overwriteBooleanfalse重名时是否覆盖,默认为 false 不覆盖(重名时报错)。

例如,注册名为 'edge' 的节点。

Graph.registerEdge('edge', Edge)

注册以后,我们可以像下面这样来使用。

graph.addEdge({
  shape: 'edge',
  source,
  target,
})

便捷方法一

有时候我们可能在继承边后并不需要任何扩展任何方法,而只是覆盖某些默认样式。例如,定义一个红色连线的边。

class RedEdge extends Edge { }

RedRect.config({
  attrs: {
    line: {
      stroke: 'red',
    },
  },
})

上面第一行代码就显得有点尴尬:实现了继承但没有扩展任何方法,有点大材小用的感觉。所以我们也提供了一个更加便捷的静态方法 define 来定义这类边。

const RedEdge = Edge.define({
  attrs: {
    line: {
      stroke: 'red',
    },
  },
})

Graph.registerEdge('red-edge', RedRect)

该方法将其调用者(如上面的 Edge)作为基类,继承出一个新的边,然后调用新边的静态方法 config 来配置默认选项。

需要注意的是,上面代码生成的 RedEdge 类的类名并不是 'RedEdge',而是系统自动生成的类名,当指定 constructorName 选项后,其大驼峰(CamelCase)形式将作为新节点的类名。

const RedEdge = Edge.define({
  constructorName: 'red-edge',
  attrs: {
    line: {
      stroke: 'red',
    },
  },
})

Graph.registerEdge('red-edge', RedEdge)

如果我们提供了 shape 选项,那么系统将自动为你注册节点。当没有指定 constructorName 选项时,shape 的大驼峰形式(CamelCase)也将作新边的类名,也就是说下面代码定义的边的类名为 'RedEdge'

Edge.define({
  shape: 'red-edge', // 自动注册名为 'red-edge' 的节点,并且节点类名为 'RedEdge'。
  attrs: {
    line: {
      stroke: 'red',
    },
  },
})

除了 constructorNameshape 两个特殊选项外,其他选项都与 config 方法的选项保持一致。下表是 define 方法支持的选项。

名称是否必选类型说明
constructorNameString类名。
shapeString自动注册的节点名,当 constructorName 缺省时其大驼峰(CamelCase)形式也将作为类名。
...othersObjectconfig 方法的选项。

便捷方法二

上面提到的 Graph.registerEdge 方法还有另外一种签名,使用该方法可以同时实现定义和注册边。

Graph.registerEdge(name: string, options: Object, overwrite?: boolean)
参数名类型是否必选默认值说明
nameString注册的边名。
optionsObject选项。
overwriteBooleanfalse重名时是否覆盖,默认为 false 不覆盖(重名时报错)。

通过 options.inherit 来指定继承的基类,默认值为 Edge 类,支持字符串或节点类,当 options.inherit 为字符串时将自动从已注册的边中找到对应的边作为基类,options 的其他选项与 define 方法一致。当 options.constructorName 类名缺省时,第一个参数 name 的大驼峰形式(CamelCase)也将作为自定义边的类名。

Graph.registerNode('red-edge', {
  inherit: Edge, // 或 'rect'
  attrs: {
    line: {
      stroke: 'red',
    },
  },
})

案例

接下来我们就基于 Shape.Edge 来自定义一个矩形 CustomEdge,这里不修改边的 markup 定义,而仅仅是做样式覆盖。

使用便捷方法一。

import { Shape } from '@antv/x6'

Shape.Edge.define({
  // 边名称
  shape: 'custom-edge',
  // 属性样式
  attrs: {
    line: {
      stroke: '#5755a1',
    },
  },
  // 默认标签
  defaultLabel: {
    markup: [
      {
        tagName: 'rect',
        selector: 'body',
      },
      {
        tagName: 'text',
        selector: 'label',
      },
    ],
    attrs: {
      label: {
        fill: 'black',
        fontSize: 14,
        textAnchor: 'middle',
        textVerticalAnchor: 'middle',
        pointerEvents: 'none',
      },
      body: {
        ref: 'label',
        fill: 'white',
        stroke: '#5755a1',
        strokeWidth: 2,
        rx: 4,
        ry: 4,
        refWidth: '140%',
        refHeight: '140%',
        refX: '-20%',
        refY: '-20%',
      },
    },
    position: {
      distance: 100, // 绝对定位
      options: {
        absoluteDistance: true,
      },
    },
  },
})

使用便捷方法二。

Graph.registerEdge(
  'custom-edge', // 边名称
  {
    // 基类
    inherit: 'edge',
    // 属性样式
    attrs: {
      line: {
        stroke: '#5755a1',
      },
    },
    // 默认标签
    defaultLabel: {
      markup: [
        {
          tagName: 'rect',
          selector: 'body',
        },
        {
          tagName: 'text',
          selector: 'label',
        },
      ],
      attrs: {
        label: {
          fill: 'black',
          fontSize: 14,
          textAnchor: 'middle',
          textVerticalAnchor: 'middle',
          pointerEvents: 'none',
        },
        body: {
          ref: 'label',
          fill: 'white',
          stroke: '#5755a1',
          strokeWidth: 2,
          rx: 4,
          ry: 4,
          refWidth: '140%',
          refHeight: '140%',
          refX: '-20%',
          refY: '-20%',
        },
      },
      position: {
        distance: 100, // 绝对定位
        options: {
          absoluteDistance: true,
        },
      },
    },
  },
)