安装

通过 npm 或 yarn 命令安装 X6。

# npm
npm install @antv/x6 --save

# yarn
yarn add @antv/x6

安装完成之后,使用 import 或 require 进行引用。

import { Graph } from '@antv/x6';

缩放与平移

画布的拖拽、缩放也是常用操作,Graph 中通过 panning 和 mousewheel 配置来实现这两个功能,鼠标按下画布后移动时会拖拽画布,滚动鼠标滚轮会缩放画布。

const graph = new Graph({

  ...,

  panning: true,

  mousewheel: true

})

添加节点

节点和边都有共同的基类 Cellopen in new window,除了从 Cell 继承属性外,还支持以下选项。

属性名类型默认值描述
xnumber0节点位置 x 坐标,单位为 px。
ynumber0节点位置 y 坐标,单位为 px。
widthnumber1节点宽度,单位为 px。
heightnumber1节点高度,单位为 px。
anglenumber0节点旋转角度。
graph.addNode({

  shape: 'rect',

  x: 100,

  y: 40,

  width: 100,

  height: 40,

})

内置节点

上面使用 shape 来指定了节点的图形,shape 的默认值为 rect。X6 内置节点与 shape 名称对应关系如下表。

构造函数shape 名称描述
Shape.Rectrect矩形。
Shape.Circlecircle圆形。
Shape.Ellipseellipse椭圆。
Shape.Polygonpolygon多边形。
Shape.Polylinepolyline折线。
Shape.Pathpath路径。
Shape.Imageimage图片。
Shape.HTMLhtmlHTML 节点,使用 foreignObject 渲染 HTML 片段。

定制节点

我们可以通过 markup 和 attrs 来定制节点的形状和样式,markup 可以类比 HTML,attrs 类比 CSS。强烈建议仔细阅读 markupopen in new window 和 attrsopen in new window 文档。

接下来我们会遇到一个问题,定制的内容要被多个节点使用,是不是需要每个节点都重新定义一次呢?答案是否定的,X6 提供了便捷的方式,可以让不同的节点复用配置。

修改节点(大小颜色)

在渲染完成之后,我们还可以通过 API 修改节点的所有属性。我们会常用到下面两个方法:

添加边

节点和边都有共同的基类 Cellopen in new window,除了从 Cell 继承属性外,还支持以下选项。

属性名类型默认值描述
sourceTerminalData-源节点或起始点。
targetTerminalData-目标节点或目标点。
verticesPoint.PointLike[]-路径点。
routerRouterData-路由。
connectorConnectorData-连接器。
labelsLabel[]-标签。
defaultLabelLabel默认标签open in new window默认标签。
graph.addEdge({

  shape: 'edge',

  source: 'node1',

  target: 'node2',

})

配置边

下面分别看下上面的配置如何使用。

source/target

边的源和目标节点(点)。

graph.addEdge({  source: rect1, // 源节点  target: rect2, // 目标节点})
graph.addEdge({  source: 'rect1', // 源节点 ID  target: 'rect2', // 目标节点 ID})
graph.addEdge({  source: { cell: rect1, port: 'out-port-1' }, // 源节点和连接桩 ID  target: { cell: 'rect2', port: 'in-port-1' }, // 目标节点 ID 和连接桩 ID})
graph.addEdge({  source: 'rect1', // 源节点 ID  target: { x: 100, y: 120 }, // 目标点})

router

路由 router 将对 vertices 进一步处理,并在必要时添加额外的点,然后返回处理后的点。例如,经过 orth 路由open in new window处理后,边的每一条链接线段都是水平或垂直的。

graph.addEdge({

  source: rect1,

  target: rect2,

  vertices: [

    { x: 100, y: 200 },

    { x: 300, y: 120 },

  ],

  // 如果没有 args 参数,可以简写为 router: 'orth'

  router: {

    name: 'orth',

    args: {},

  },

})

X6 默认提供了以下几种路由,点击下面的链接查看每种路由的使用方式。

另外,我们也可以注册自定义路由

使用箭头

我们定义了 sourceMarker 和 targetMarker 两个特殊属性来为边定制起始和终止箭头。例如,对 Shape.Edge 我们可以通过 line 选择器来指定起始和终止箭头。

内置箭头

X6 提供了以下几种内置箭头,使用时只需要指定箭头名和参数(可省略)即可。

graph.addEdge({

  shape: 'edge',

  sourece: [100, 100],

  target: [500, 500],

  attrs: {

    line: {

      sourceMarker: 'block', // 实心箭头

      targetMarker: {

        name: 'ellipse', // 椭圆

        rx: 10, // 椭圆箭头的 x 半径

        ry: 6, // 椭圆箭头的 y 半径

      },

    },

  },

})

连接桩

阅读时间约 6 分钟

在本章节中主要介绍连接桩相关的知识,通过阅读你可以了解到

  • 如何在节点中配置连接桩
  • 连接桩的增、删、改
  • 如何配置连接桩的位置
  • 如何配置连接桩上标签的位置

配置连接桩

首先我们将具有相同行为和外观的连接桩归为同一组,并通过 groups 选项来设置分组,该选项是一个对象 { [groupName: string]: PortGroupMetadata },组名为键,值为每组连接桩的默认选项,支持的选项如下:

interface PortGroupMetadata {

  markup?: Markup // 连接桩 DOM 结构定义。

  attrs?: Attr.CellAttrs // 属性和样式。

  zIndex?: number | 'auto' // 连接桩的 DOM 层级,值越大层级越高。

  // 群组中连接桩的布局。

  position?: [number, number] | string | { name: string; args?: object }

  label?: {

    // 连接桩标签

    markup?: Markup

    position?: {

      // 连接桩标签布局

      name: string // 布局名称

      args?: object // 布局参数

    }

  }

}

然后我们配置 items,items 是一个数组 PortMetadata[],数组的每一项表示一个连接桩,连接桩支持的选项如下:

interface PortMetadata {

  id?: string // 连接桩唯一 ID,默认自动生成。

  group?: string // 分组名称,指定分组后将继承分组中的连接桩选项。

  args?: object // 为群组中指定的连接桩布局算法提供参数, 我们不能为单个连接桩指定布局算法,但可以为群组中指定的布局算法提供不同的参数。

  markup?: Markup // 连接桩的 DOM 结构定义。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。

  attrs?: Attr.CellAttrs // 元素的属性样式。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。

  zIndex?: number | 'auto' // 连接桩的 DOM 层级,值越大层级越高。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。

  label?: {

    // 连接桩的标签。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。

    markup?: Markup // 标签 DOM 结构

    position?: {

      // 标签位置

      name: string // 标签位置计算方法的名称

      args?: object // 标签位置计算方法的参数

    }

  }

}

从下面例子代码中可以清晰看到连接桩的定义方式。

修改连接桩

节点上有丰富的 APIopen in new window 对连接桩进行增、删、改操作。

// 添加连接桩node.addPort({  group: 'top',  attrs: {    text: {      text: 'xx',    },  },})
// 删除连接桩node.removePort(portId)
// 更新连接桩node.portProp(portId, 'attrs/circle/stroke', color)

连接桩位置

连接桩布局算法只能通过 groups 中的 position 选项来指定,因为布局算法在计算连接桩位置时需要考虑到群组中的所有连接桩,我们在单个连接桩中可以通过 args 选项来影响该连接桩的布局结果。

我们默认提供了下面几种连接桩布局算法,同时支持自定义连接桩布局算法并注册使用open in new window,点击下面的链接可以了解每种布局算法的使用方法。

连接桩标签位置

在 groups 的 label.position 选项和节点的 items.label.position 选项中都可以指定标签的位置。

我们默认提供了下面几种标签位置,也支持自定义标签位置并注册使用open in new window,点击下面的链接了解每种标签位置的使用方法。

交互

连线

连线交互规则都是通过 connecting 配置来完成,完整的配置参考 APIopen in new window。下面介绍一些常用的功能。

allowXXX

可以通过 allowXXX 配置来定义连线能否连接到对应的位置。默认支持以下项:

  • allowBlank:是否允许连接到画布空白位置的点,默认为 true。
  • allowLoop:是否允许创建循环连线,即边的起始节点和终止节点为同一节点,默认为 true。
  • allowNode:是否允许边连接到节点(非节点上的连接桩),默认为 true。
  • allowEdge:是否允许边连接到另一个边,默认为 true。
  • allowPort:是否允许边连接到连接桩,默认为 true。
  • allowMulti:是否允许在相同的起始节点和终止之间创建多条边,默认为 true。

它们的值都支持以下两种类型:

new Graph({
  connecting: {
    allowNode: true, // boolean
  },
})

// 函数形式,多用于动态控制连接限制
new Graph({
  connecting: {
    allowNode(args) {
      return true
    },
  },
})

提示

allowMulti 支持设置为字符串 withPort,代表在起始和终止节点的相同连接桩之间只允许创建一条边(即起始和终止节点之间可以创建多条边,但必须要连接在不同的连接桩上)。

视图交互事件

通过鼠标、键盘或者各种可交互的组件与应用产生交互时触发的事件。

鼠标事件

事件cell 节点/边node 节点port 连接桩edge 边blank 画布空白区域
单击cell:clicknode:clicknode:port:clickedge:clickblank:click
双击cell:dblclicknode:dblclicknode:port:dblclickedge:dblclickblank:dblclick
右键cell:contextmenunode:contextmenunode:port:contextmenuedge:contextmenublank:contextmenu
鼠标按下cell:mousedownnode:mousedownnode:port:mousedownedge:mousedownblank:mousedown
移动鼠标cell:mousemovenode:mousemovenode:port:mousemoveedge:mousemoveblank:mousemove
鼠标抬起cell:mouseupnode:mouseupnode:port:mouseupedge:mouseupblank:mouseup
鼠标滚轮cell:mousewheelnode:mousewheel-edge:mousewheelblank:mousewheel
鼠标进入cell:mouseenternode:mouseenternode:port:mouseenteredge:mouseentergraph:mouseenter
鼠标离开cell:mouseleavenode:mouseleavenode:port:mouseleaveedge:mouseleavegraph:mouseleave

注意

需要注意的是,这里的 mousemove 事件和通常的鼠标移动事件有所区别,它需要在鼠标按下后移动鼠标才能触发。

除了 mouseenter 和 mouseleave 外,事件回调函数的参数都包含鼠标相对于画布的位置 x、y 和鼠标事件对象 e 等参数。

graph.on('cell:click', ({ e, x, y, cell, view }) => {})
graph.on('node:click', ({ e, x, y, node, view }) => {})
graph.on('edge:click', ({ e, x, y, edge, view }) => {})
graph.on('blank:click', ({ e, x, y }) => {})

graph.on('cell:mouseenter', ({ e, cell, view }) => {})
graph.on('node:mouseenter', ({ e, node, view }) => {})
graph.on('edge:mouseenter', ({ e, edge, view }) => {})
graph.on('graph:mouseenter', ({ e }) => {})

自定义点击事件

我们可以在节点/边的 DOM 元素上添加自定义属性 event 或 data-event 来监听该元素的点击事件,例如:

node.attr({

  // 表示一个删除按钮,点击时删除该节点

  image: {

    event: 'node:delete',

    xlinkHref: 'trash.png',

    width: 20,

    height: 20,

  },

})

可以通过绑定的事件名 node:delete 或通用的 cell:customevent、node:customevent、edge:customevent 事件名来监听。

graph.on('node:delete', ({ view, e }) => {
  e.stopPropagation()
  view.cell.remove()
})

graph.on('node:customevent', ({ name, view, e }) => {
  if (name === 'node:delete') {
    e.stopPropagation()
    view.cell.remove()
  }
})
import React from 'react'
import { Graph } from '@antv/x6'
import './index.less'

Graph.registerNode(
  'custom-click-node',
  {
    markup: [
      {
        tagName: 'rect',
        selector: 'body',
      },
      {
        tagName: 'text',
        selector: 'label',
      },
      {
        tagName: 'g',
        children: [
          {
            tagName: 'text',
            selector: 'btnText',
          },
          {
            tagName: 'rect',
            selector: 'btn',
          },
        ],
      },
    ],
    attrs: {
      btn: {
        refX: '100%',
        refX2: -28,
        y: 4,
        width: 24,
        height: 18,
        rx: 10,
        ry: 10,
        fill: 'rgba(255,255,0,0.01)',
        stroke: 'red',
        cursor: 'pointer',
        event: 'node:delete',
      },
      btnText: {
        fontSize: 14,
        fill: 'red',
        text: 'x',
        refX: '100%',
        refX2: -19,
        y: 17,
        cursor: 'pointer',
        pointerEvent: 'none',
      },
      body: {
        stroke: '#8f8f8f',
        strokeWidth: 1,
        fill: '#fff',
        rx: 6,
        ry: 6,
        refWidth: '100%',
        refHeight: '100%',
      },
      label: {
        fontSize: 14,
        fill: '#333333',
        refX: '50%',
        refY: '50%',
        textAnchor: 'middle',
        textVerticalAnchor: 'middle',
      },
    },
  },
  true,
)

export default class Example extends React.Component {
  private container: HTMLDivElement

  componentDidMount() {
    const graph = new Graph({
      container: this.container,
      background: {
        color: '#F2F7FA',
      },
    })

    const source = graph.addNode({
      shape: 'custom-click-node',
      x: 40,
      y: 80,
      width: 120,
      height: 40,
      attrs: {
        label: {
          text: 'Source',
        },
      },
    })

    const target = graph.addNode({
      shape: 'custom-click-node',
      x: 360,
      y: 80,
      width: 120,
      height: 40,
      attrs: {
        label: {
          text: 'Target',
        },
      },
    })

    graph.addEdge({
      source,
      target,
      attrs: {
        line: {
          stroke: '#8f8f8f',
          strokeWidth: 1,
        },
      },
    })

    graph.on('node:delete', ({ view, e }: any) => {
      e.stopPropagation()
      view.cell.remove()
    })
  }

  refContainer = (container: HTMLDivElement) => {
    this.container = container
  }

  render() {
    return (
      <div className="custom-click-app">
        <div className="app-content" ref={this.refContainer} />
      </div>
    )
  }
}

节点/边

添加/删除/修改

当节点/边被添加到画布时,触发以下事件:

  • added
  • cell:added
  • node:added(仅当 cell 是节点时才触发)
  • edge:added(仅当 cell 是边时才触发)

当节点/边被移除时,触发以下事件:

  • removed
  • cell:removed
  • node:removed(仅当 cell 是节点时才触发)
  • edge:removed(仅当 cell 是边时才触发)

当节点/边发生任何改变时,触发以下事件:

  • changed
  • cell:changed
  • node:changed(仅当 cell 是节点时才触发)
  • edge:changed(仅当 cell 是边时才触发)

可以在节点/边上监听:

cell.on('added', ({ cell, index, options }) => {})

cell.on('removed', ({ cell, index, options }) => {})

cell.on('changed', ({ cell, options }) => {})

或者在 Graph 上监听:

graph.on('cell:added', ({ cell, index, options }) => {})
graph.on('cell:removed', ({ cell, index, options }) => {})
graph.on('cell:changed', ({ cell, options }) => {})

graph.on('node:added', ({ node, index, options }) => {})
graph.on('node:removed', ({ node, index, options }) => {})
graph.on('node:changed', ({ node, options }) => {})

graph.on('edge:added', ({ edge, index, options }) => {})
graph.on('edge:removed', ({ edge, index, options }) => {})
graph.on('edge:changed', ({ edge, options }) => {})

change:xxx

当调用 setXxx(val, options) 和 removeXxx(options) 方法去改变节点/边的数据时,并且 options.silent 不为 true 时,都将触发对应的 change 事件,并触发节点/边重绘。例如:

cell.setZIndex(2)

cell.setZIndex(2, { silent: false })

cell.setZIndex(2, { anyKey: 'anyValue' })

将触发 Cell 上的以下事件:

  • change:*
  • change:zIndex

和 Graph 上的以下事件:

  • cell:change:*
  • node:change:*(仅当 cell 是节点时才触发)
  • edge:change:*(仅当 cell 是边时才触发)
  • cell:change:zIndex
  • node:change:zIndex(仅当 cell 是节点时才触发)
  • edge:change:zIndex(仅当 cell 是边时才触发)

可以在节点/边上监听:

// 当 cell 发生任何改变时都将被触发,可以通过 key 来确定改变项
cell.on(
  'change:*',
  (args: {
    cell: Cell
    key: string // 通过 key 来确定改变项
    current: any // 当前值
    previous: any // 改变之前的值
    options: any // 透传的 options
  }) => {
    if (key === 'zIndex') {
      //
    }
  },
)

cell.on(
  'change:zIndex',
  (args: {
    cell: Cell
    current?: number // 当前值
    previous?: number // 改变之前的值
    options: any // 透传的 options
  }) => {},
)

或者在 Graph 上监听:

graph.on(
  'cell:change:zIndex',
  (args: {
    cell: Cell
    current?: number // 当前值
    previous?: number // 改变之前的值
    options: any // 透传的 options
  }) => {},
)

// 当 cell 为节点时触发
graph.on(
  'node:change:zIndex',
  (args: {
    cell: Cell
    node: Node
    current?: number // 当前值
    previous?: number // 改变之前的值
    options: any // 透传的 options
  }) => {},
)

// 当 cell 为边时触发
graph.on(
  'edge:change:zIndex',
  (args: {
    cell: Cell
    edge: Edge
    current?: number // 当前值
    previous?: number // 改变之前的值
    options: any // 透传的 options
  }) => {},
)

其他 change 事件如下列表,回调函数的参数与上面提到的 change:zIndex 的参数结构一致。

  • Cell
    • change:*
    • change:attrs
    • change:zIndex
    • change:markup
    • change:visible
    • change:parent
    • change:children
    • change:tools
    • change:view
    • change:data
  • Node
    • change:size
    • change:angle
    • change:position
    • change:ports
    • change:portMarkup
    • change:portLabelMarkup
    • change:portContainerMarkup
    • ports:added
    • ports:removed
  • Edge
    • change:source
    • change:target
    • change:terminal
    • change:router
    • change:connector
    • change:vertices
    • change:labels
    • change:defaultLabel
    • vertexs:added
    • vertexs:removed
    • labels:added
    • labels:removed

除了上述这些内置的 Key,我们也支持监听自定义的 Key,例如

cell.on('change:custom', ({ cell, current, previous, options }) => {

  console.log(current)

})

当通过 cell.prop('custom', 'any data') 方法修改 custom 属性的值时将触发 change:custom 事件。

视图

由于 X6 实现了异步的渲染调度算法,所以节点的添加不一定意味着挂载到画布上。节点在被挂载到画布时以及从画布上卸载时会分别触发单独的事件。

事件名回调参数说明
view:mounted{ view: CellView }节点被挂载到画布上时触发。
view:unmounted{ view: CellView }节点从画布上卸载时触发。
graph.on('view:mounted', ({ view }) => {})

graph.on('view:unmounted', ({ view }) => {})

大家还有经常需要在调用 fromJSON 或者 resetCells 后监听画布完成渲染事件,这时候可以使用 render:done 事件来监听 (2.15.1 版本新增)。

graph.on('render:done', () => {  // pass})
graph.fromJSON([...])

数据

导出

我们可以调用 graph.toJSON() 方法来导出图中的节点和边,返回一个具有 { cells: [] } 结构的对象,其中 cells 数组按渲染顺序保存节点和边。

其中,导出的节点结构如下:

{

  id: string,

  shape: string,

  position: {

    x: number

    y: number

  },

  size: {

    width: number

    height: number

  },

  attrs: object,

  zIndex: number,

}

边的结构如下:

{

  id: string,

  shape: string,

  source: object,

  target: object,

  attrs: object,

  zIndex: number,

}
{
  "cells": [
    {
      "position": {
        "x": 0,
        "y": 0
      },
      "size": {
        "width": 100,
        "height": 40
      },
      "attrs": {
        "text": {
          "text": "Hello"
        },
        "body": {
          "stroke": "#8f8f8f",
          "strokeWidth": 1,
          "fill": "#fff",
          "rx": 6,
          "ry": 6
        }
      },
      "visible": true,
      "shape": "rect",
      "id": "f56d0228-1b2d-4d1f-a549-624a112f1013",
      "zIndex": 1
    },
    {
      "position": {
        "x": 0,
        "y": 240
      },
      "size": {
        "width": 100,
        "height": 40
      },
      "attrs": {
        "text": {
          "text": "World"
        },
        "body": {
          "stroke": "#8f8f8f",
          "strokeWidth": 1,
          "fill": "#fff",
          "rx": 6,
          "ry": 6
        }
      },
      "visible": true,
      "shape": "ellipse",
      "id": "4aa790be-3370-4735-bc1d-15e6f9aeb9f7",
      "zIndex": 2
    },
    {
      "shape": "edge",
      "attrs": {
        "line": {
          "stroke": "#8f8f8f",
          "strokeWidth": 1
        }
      },
      "id": "a363d4ea-b0bf-468d-9dfc-0afd87d8691f",
      "source": {
        "cell": "f56d0228-1b2d-4d1f-a549-624a112f1013"
      },
      "target": {
        "cell": "4aa790be-3370-4735-bc1d-15e6f9aeb9f7"
      },
      "labels": [
        {
          "attrs": {
            "label": {
              "text": "X6"
            }
          }
        }
      ],
      "zIndex": 3
    }
  ]
}

导入

支持节点/边元数据数组 graph.fromJSON(cells: (Node.Metadata | Edge.Metadata)[])。

graph.fromJSON([

  {

    id: 'node1',

    x: 40,

    y: 40,

    width: 100,

    height: 40,

    label: 'Hello',

    shape: 'rect',

  },

  {

    id: 'node2',

    x: 40,

    y: 40,

    width: 100,

    height: 40,

    label: 'Hello',

    shape: 'ellipse',

  },

  {

    id: 'edge1',

    source: 'node1',

    target: 'node2',

    shape: 'edge',

  },

])

或者提供一个包含 cells、nodes、edges 的对象,按照 [...cells, ...nodes, ...edges] 顺序渲染。

graph.fromJSON({

  nodes: [],

  edges: [],

})

通常,我们通过 graph.fromJSON(...) 来渲染 graph.toJSON() 导出的数据。

提示

当数据中没有提供 zIndex 时,则按照节点/边在数组中的顺序渲染,也就是说越靠前的节点/边,其 zIndex 越小,在画布中的层级就越低

内置工具

工具是渲染在节点/边上的小部件,用于增强节点/边的交互能力,我们分别为节点和边提供了以下内置工具:

节点:

边: