安装

通过 npm 或 yarn 命令安装 X6。

# npm
npm install @antv/x6 --save

# yarn
yarn add @antv/x6

安装完成之后,使用 importrequire 进行引用。

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

缩放与平移

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

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 片段。

定制节点

我们可以通过 markupattrs 来定制节点的形状和样式,markup 可以类比 HTMLattrs 类比 CSS。强烈建议仔细阅读 markupopen in new windowattrsopen 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 默认提供了以下几种路由,点击下面的链接查看每种路由的使用方式。

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

使用箭头

我们定义了 sourceMarkertargetMarker 两个特殊属性来为边定制起始和终止箭头。例如,对 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 // 布局参数

    }

  }

}

然后我们配置 itemsitems 是一个数组 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,点击下面的链接可以了解每种布局算法的使用方法。

连接桩标签位置

groupslabel.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 事件和通常的鼠标移动事件有所区别,它需要在鼠标按下后移动鼠标才能触发。

除了 mouseentermouseleave 外,事件回调函数的参数都包含鼠标相对于画布的位置 xy 和鼠标事件对象 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 元素上添加自定义属性 eventdata-event 来监听该元素的点击事件,例如:

node.attr({

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

  image: {

    event: 'node:delete',

    xlinkHref: 'trash.png',

    width: 20,

    height: 20,

  },

})

可以通过绑定的事件名 node:delete 或通用的 cell:customeventnode:customeventedge: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',

  },

])

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

graph.fromJSON({

  nodes: [],

  edges: [],

})

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

提示

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

内置工具

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

节点:

边: