# X6 — Full Documentation > JavaScript 图编辑引擎 - [Resource index](/llms.txt) - Canonical site: https://x6.antv.antgroup.com/zh/ - Source repository: https://github.com/antvis/x6 - Languages: zh, en - Current version: 3.x ## 中文文档 ### api #### 画布 - Canonical page: https://x6.antv.antgroup.com/zh/api/graph/graph/ - Markdown source: https://x6.antv.antgroup.com/zh/api/graph/graph.md - Description: 画布 --- title: "画布" description: "画布" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/graph/graph/" version: "3.x" --- ## 配置 ```ts new Graph(options: Options) ``` | 选项 | 类型 | 必选 | 描述 | 默认值 | | --- | --- | :-: | --- | --- | | container | `HTMLElement \| string` | ✓ | 画布的容器。字符串仅作为元素 ID 解析,无需包含 `#`。 | | | width | `number` | | 画布宽度,默认使用容器宽度。 | - | | height | `number` | | 画布高度,默认使用容器高度。 | - | | scaling | `{ min?: number, max?: number }` | | 画布的最小最大缩放级别。 | `{ min: 0.01, max: 16 }` | | [autoResize](/zh/tutorial/basic/graph#画布大小) | `boolean \| Element \| Document` | | 是否监听容器大小改变,并自动更新画布大小。 | `false` | | [panning](/zh/api/graph/panning) | `boolean \| PanningManager.Options` | | 画布是否可以拖拽平移,默认启用。 | `true` | | [mousewheel](/zh/api/graph/mousewheel) | `boolean \| MouseWheel.Options` | | 鼠标滚轮缩放,默认禁用。 | `false` | | [grid](/zh/api/graph/grid) | `boolean \| number \| GridManager.Options` | | 网格,默认使用 `10px` 的网格,但不绘制网格背景。 | `false` | | [background](/zh/api/graph/background) | `false \| BackgroundManager.Options` | | 背景,默认不绘制背景。 | `false` | | [translating](/zh/api/model/interaction#移动范围) | `Translating.Options` | | 限制节点移动、移动节点重叠时自动偏移。 | `{ restrict: false, autoOffset: true }` | | [embedding](/zh/api/model/interaction#组合) | `boolean \| Embedding.Options` | | 嵌套节点,默认禁用。 | `false` | | [connecting](/zh/api/model/interaction#connecting) | `Connecting.Options` | | 连线选项。 | `{ snap: false, ... }` | | [highlighting](/zh/api/model/interaction#高亮) | `Highlighting.Options` | | 高亮选项。 | `{...}` | | [interacting](/zh/api/model/interaction#限制) | `Interacting.Options` | | 定制节点和边的交互行为。 | `{ edgeLabelMovable: false }` | | [magnetThreshold](/zh/api/mvc/view#magnetthreshold) | `number \| onleave` | | 鼠标移动多少次后才触发连线,或者设置为 `onleave` 时表示鼠标移出元素时才触发连线。 | `0` | | [moveThreshold](/zh/api/mvc/view#movethreshold) | `number` | | 触发 `mousemove` 事件之前,允许鼠标移动的次数。 | `0` | | [clickThreshold](/zh/api/mvc/view#clickthreshold) | `number` | | 当鼠标移动次数超过指定的数字时,将不触发鼠标点击事件。 | `0` | | [preventDefaultContextMenu](/zh/api/mvc/view#preventdefaultcontextmenu) | `boolean` | | 是否禁用浏览器默认右键菜单。 | `true` | | [preventDefaultBlankAction](/zh/api/mvc/view#preventdefaultblankaction) | `boolean` | | 在画布空白位置响应鼠标事件时,是否禁用鼠标默认行为。 | `true` | | [async](/zh/api/mvc/view#async) | `boolean` | | 是否异步渲染 | `true` | | [virtual](/zh/api/mvc/view#virtual) | `boolean \| VirtualOptions` | | 是否只渲染可视区域内容;当传入对象时可配置缓冲边距 `margin` 与开关 `enabled` | `false` | | [onPortRendered](/zh/api/mvc/view#onportrendered) | `(args: OnPortRenderedArgs) => void` | | 当某个连接桩渲染完成时触发的回调。 | - | | [onEdgeLabelRendered](/zh/api/mvc/view#onedgelabelrendered) | `(args: OnEdgeLabelRenderedArgs) => void \| ((args: OnEdgeLabelRenderedArgs) => void)` | | 当边的文本标签渲染完成时触发的回调,并且它可以返回一个清理函数,该函数会在标签被销毁时执行。 | - | | [createCellView](/zh/api/mvc/view#createcellview) | `(cell: Cell) => CellView \| null \| undefined` | | 是自定义元素的视图。 | - | #### 元素 - Canonical page: https://x6.antv.antgroup.com/zh/api/model/cell/ - Markdown source: https://x6.antv.antgroup.com/zh/api/model/cell.md - Description: 元素 --- title: "元素" description: "元素" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/model/cell/" version: "3.x" --- Cell 是 [Node](/zh/api/model/node) 和 [Edge](/zh/api/model/edge) 的基类,包含节点和边的通用属性和方法定义,如属性样式、可见性、业务数据等,并且在实例化、样式定制、默认选项、自定义选项等方面具有相同的行为。 ## 属性 | 选项 | 类型 | 默认值 | 必选 | 描述 | |----------|---------------------------------|--------|:----:|--------------------------------------------------------------------| | id | string | | | 节点/边的唯一标识,推荐使用具备业务意义的 ID,默认使用自动生成的 UUID。 | | markup | Markup | | | 节点/边的 SVG/HTML 片段。 | | attrs | Attr.CellAttrs | | | 节点/边属性样式。 | | shape | string | | | 渲染节点/边的图形。节点对应的默认值为 `rect`,边对应的默认值为 `edge`。 | | view | string | | | 渲染节点/边的视图。 | | zIndex | number | | | 节点/边在画布中的层级,默认根据节点/边添加顺序自动确定。 | | visible | boolean | `true` | | 节点/边是否可见。 | | parent | string | | | 父节点。 | | children | string[] | | | 子节点/边。 | | tools | ToolItem \| ToolItem[] \| Tools | | | 工具选项。 | | data | any | | | 节点/边关联的业务数据。 | ### id `id` 是节点/边的唯一标识,推荐使用具备业务意义的 ID,默认使用自动生成的 UUID。 ### markup `markup` 指定了渲染节点/边时使用的 SVG/HTML 片段,使用 JSON 格式描述。例如内置节点 `Shape.Rect` 的 `markup` 定义如下: ```ts { markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'text', selector: 'label', }, ], } ``` 表示该节点内部包含 `` 和 `` 两个 SVG 元素,渲染到页面之后,节点对应的 SVG 元素看起来像下面这样。 ```html rect ``` 通过上面的介绍,我们大致了解了 `Markup` 的结构,下面我们将详细介绍 `Markup` 定义。 ```ts 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 | string | | ✓ | SVG/HTML 元素标签名。 | | ns | string | `"http://www.w3.org/2000/svg"` | | SVG/HTML 元素命名空间。 | | selector | string | - | | 该元素的选择器(唯一),通过选择器来定位该元素或为该元素指定属性样式。 | | groupSelector | string | - | | 该元素的群组选择器,可以同时为该群组对应的多个元素指定样式。 | | attrs | Attr.SimpleAttrs | - | | 该元素的默认属性键值对。 | | style | KeyValue | - | | 该元素的行内样式键值对。 | | className | string | - | | 该元素的 CSS 样式名。 | | textContent | string | - | | 该元素的文本内容。 | | children | Markup[] | - | | 嵌套的子元素。 | #### tagName 通过 `tagName` 指定需要创建哪种 SVG/HTML 元素。 #### ns 该元素的命名空间。需要与 `tagName` 指定的的元素类型对应,默认使用 SVG 元素命名空间 `"http://www.w3.org/2000/svg"`。 - SVG 元素命名空间为 `"http://www.w3.org/2000/svg"` - HTML 元素的命名空间为 `"http://www.w3.org/1999/xhtml"` #### selector 该元素的唯一选择器,通过选择器为该元素指定[属性样式](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Fills_and_Strokes)。例如,为内置节点 `Shape.Rect` 指定 `` 和 `` 元素的属性样式: ```ts 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 该元素的群组选择器,通过群组选择器可以为该群组关联的多个元素指定样式。例如,下面 Markup 中两个 `` 具备相同的 `groupSelector` 值 `group1`: ```ts { markup: [ { tagName: 'rect', selector: 'body', groupSelector: 'group1', }, { tagName: 'rect', selector: 'wrap', groupSelector: 'group1', }, { tagName: 'text', selector: 'label', }, ], } ``` 创建节点时,我们可以像下面这样来指定群组样式: ```ts new SomeNode({ attrs: { group1: { fill: '#2ECC71', }, }, }) ``` #### attrs 该元素的默认属性键值对,通常用于定义那些不变的通用属性,这些默认属性也可以在实例化节点时被覆盖。需要注意的是 `markup` 中的 `attrs` 属性只支持原生的 SVG 属性,也就是说 X6 的[自定义属性]()在这里不可用。 例如,我们为内置节点 `Shape.Rect` 的 `` 和 `` 元素指定了如下默认属性: ```js { markup: [ { tagName: 'rect', selector: 'body', attrs: { fill: '#fff', stroke: '#000', strokeWidth: 2, } }, { tagName: 'text', selector: 'label', attrs: { fill: '#333', textAnchor: 'middle', textVerticalAnchor: 'middle', } }, ], } ``` #### style 该元素的行内样式键值对。 #### className 该元素的 CSS 样式名。 #### textContent 该元素的文本内容。 #### children 嵌套的子元素。 ### attrs 属性选项 `attrs` 是一个复杂对象,该对象的 Key 是节点 Markup 定义中元素的选择器([selector](#selector)),对应的值是应用到该 SVG 元素的 [SVG 属性值](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute)(如 [fill](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute/fill) 和 [stroke](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute/stroke)),如果你对 SVG 属性还不熟悉,可以参考 MDN 提供的[填充和边框](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Fills_and_Strokes)入门教程。 例如,内置节点 `Shape.Rect` 的 Markup 定义了 `body`(代表 `` 元素) 和 `label`(代表 `` 元素) 两个选择器,我们可以像下面这样为该节点中的元素指定属性样式: ```ts 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 结构看起来像下面这样: ```html rect ``` 另外,我们还可以使用 CSS 选择器来指定节点样式,这样我们就不用记住预定义的选择器名称,只需要根据渲染后的 DOM 结构来定义样式即可。使用 CSS 选择器时需要注意,指定的 CSS 选择器可能选中多个元素,这时对应的属性样式将同时应用到多个元素上。 ```ts 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)格式](https://zh.wikipedia.org/zh-cn/%E9%A7%9D%E5%B3%B0%E5%BC%8F%E5%A4%A7%E5%B0%8F%E5%AF%AB)的属性名,如 `fontSize`,这就避免了 `font-size` 这种属性名作为对象 Key 时需要加引号的麻烦。 除了标准的 [SVG 属性](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute),我们在 X6 中还定义了一系列特殊属性,详情请参考[特殊属性](/zh/api/model/attrs)和[自定义属性](/zh/api/model/cell/#自定义属性)。另外,我们还可以使用 [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS) 来定制样式,节点和边渲染到画布后分别有 `x6-node` 和 `x6-edge` 两个样式名,默认的样式定义[参考这里](https://github.com/antvis/X6/blob/master/src/style/index.less)。例如,我们可以像下面这样来指定节点中 `` 元素的样式: ```css .x6-node rect { fill: #2ecc71; stroke: #000; } ``` 创建节点/边后,我们可以调用实例上的 `attr()` 方法来修改节点属性样式。看下面代码,通过 `/` 分割的路径修改样式,`label` 选择器对应到 `` 元素,`text` 则是该元素的属性名,`hello` 是新的属性值。 ```ts rect.attr('label/text', 'hello') // 等同于 rect.attr('label', { text: 'hello', }) // 等同于 rect.attr({ label: { text: 'hello', }, }) ``` 当传入的属性值为 `null` 时可以移除该属性。 ```ts rect.attr('label/text', null) ``` ### shape 节点/边的图形,图形类似 MVC 模式中的 Model,决定了节点/边的结构化数据。该选项通常在使用 `graph.addNode` 和 `graph.addEdge` 两个方法添加节点和边时使用。 ```ts 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', }) const edge = graph.addEdge({ shape: 'edge', source: rect, target: circle, }) ``` 在 X6 内部实现中,我们通过 `shape` 指定的图形找到对应的构造函数来初始化节点/边,并将其添加到画布。 该选项的默认值为: - `graph.addNode` 方法中 `shape` 的默认值为 `rect` - `graph.addEdge` 方法中 `shape` 的默认值为 `edge` 同时,我们在 X6 中内置了一系列节点和边。 | 构造函数 | shape 名称 | 描述 | |----------------|------------|----------------------------------------------| | Shape.Rect | rect | 矩形。 | | Shape.Circle | circle | 圆形。 | | Shape.Ellipse | ellipse | 椭圆。 | | Shape.Polygon | polygon | 多边形。 | | Shape.Polyline | polyline | 多段线。 | | Shape.Path | path | 路径。 | | Shape.Image | image | 图片。 | | Shape.HTML | html | HTML 节点,使用 `foreignObject` 渲染 HTML 片段。 | ### view 指定渲染节点/边所使用的视图,视图的概念与 MVC 模式中的 View 一致,一般情况下不需要设置 view 字段,默认使用 X6 内置视图。 ### zIndex 节点/边在画布中的层级,默认根据节点/边添加顺序自动确定。节点/边渲染到画布后可以通过 `cell.getZIndex()` 和 `cell.setZIndex(z: number)` 来获取或设置 `zIndex` 的值,也可以调用 `cell.toFront()` 和 `cell.toBack()` 来将其移到最顶层或最底层。 ### visible 节点/边是否可见,默认可见。 ### parent 父节点 ID。 ### children 子节点/边的 ID 数组。 ### tools 节点/边的小工具。小工具可以增强节点/边的交互能力,我们分别为节点和边提供了以下内置工具: 节点 - [button](/zh/api/registry/node-tool#button) 在指定位置处渲染一个按钮,支持自定义按钮的点击交互。 - [button-remove](/zh/api/registry/node-tool#button-remove) 在指定的位置处,渲染一个删除按钮,点击时删除对应的节点。 - [boundary](/zh/api/registry/node-tool#boundary) 根据节点的包围盒渲染一个包围节点的矩形。注意,该工具仅仅渲染一个矩形,不带任何交互。 - [node-editor](/zh/api/registry/node-tool#node-editor) 提供节点上文本编辑功能。 边 - [vertices](/zh/api/registry/edge-tool#vertices) 路径点工具,在路径点位置渲染一个小圆点,拖动小圆点修改路径点位置,双击小圆点删除路径点,在边上单击添加路径点。 - [segments](/zh/api/registry/edge-tool#segments) 线段工具。在边的每条线段的中心渲染一个工具条,可以拖动工具条调整线段两端的路径点的位置。 - [boundary](/zh/api/registry/edge-tool#boundary) 根据边的包围盒渲染一个包围边的矩形。注意,该工具仅仅渲染一个矩形,不带任何交互。 - [button](/zh/api/registry/edge-tool#button) 在指定位置处渲染一个按钮,支持自定义按钮的点击交互。 - [button-remove](/zh/api/registry/edge-tool#button-remove) 在指定的位置处,渲染一个删除按钮,点击时删除对应的边。 - [source-arrowhead-和-target-arrowhead](/zh/api/registry/edge-tool#source-arrowhead-和-target-arrowhead) 在边的起点或终点渲染一个图形(默认是箭头),拖动该图形来修改边的起点或终点。 - [edge-editor](/zh/api/registry/edge-tool#edge-editor) 提供边上文本编辑功能 可以指定单个工具: ```ts graph.addNode({ x: 40, y: 40, width: 100, height: 40, tools: 'button-remove', // or { name: 'button-remove' } }) ``` 同时,可以像下面这样指定工具的参数选项: ```ts graph.addNode({ x: 40, y: 40, width: 100, height: 40, tools: { name: 'button-remove', args: { x: 10, // 按钮的 x 坐标,相对于节点的左上角 y: 10, // 按钮的 y 坐标,相对于节点的左上角 }, }, }) ``` 也可以同时指定多个小工具: ```ts graph.addNode({ x: 40, y: 40, width: 100, height: 40, tools: [ 'button-remove', { name: 'boundary', args: { padding: 5, }, }, ], }) ``` ### data 与节点/边关联的业务数据。例如,我们在实际使用时通常会将某些业务数据存在节点/边的 `data` 上。 ```ts const rect = new Shape.Rect({ x: 40, y: 40, width: 100, height: 40, data: { bizID: 125, date: '20200630', price: 89.0, }, }) ``` ## 方法 ### 通用 #### get shape 获取节点/边的图形,返回注册到 X6 的图形的名称。 ```ts if (node.shape === 'rect') { // do something if the node is a 'rect' node. } ``` #### get view 获取节点/边的视图,返回注册到 X6 的视图的名称。 ```ts if (node.view === 'rect') { // do something if the node is a 'rect' view. } ``` #### isNode() ```ts isNode(): boolean ``` 检测实例是不是 [Node](/zh/api/model/node) 实例,如果是 [Node](/zh/api/model/node) 实例则返回 `true`,否则返回 `false`。所有继承自 [Node](/zh/api/model/node) 的节点都返回 `true`。 ```ts if (cell.isNode()) { // do something if the cell is a node. } ``` #### isEdge() ```ts isEdge(): boolean ``` 检测实例是不是 [Edge](/zh/api/model/edge) 实例,如果是 [Edge](/zh/api/model/edge) 实例则返回 `true`,否则返回 `false`。所有继承自 [Edge](/zh/api/model/edge) 的边都返回 `true`。 ```ts if (cell.isEdge()) { // do something if the cell is an edge. } ``` #### toJSON(...) ```ts toJSON(options?: Cell.ToJSONOptions): Object ``` 将节点/边的结构化数据转换为 JSON 数据,以便做持久化存储(通常我们通过调用 `graph.toJSON` 来导出整个画布的数据)。 | 选项 | 类型 | 默认值 | 必选 | 描述 | |--------------|---------|---------|:----:|---------------------------------------| | options.diff | boolean | `false` | | 是否返回与默认值相比具有差异的那些数据(还是会导出整个画布的数据)。 | - 当 `options.diff` 为 `false` 时,返回完整数据。 - 当 `options.diff` 为 `true` 时,返回差异数据(剔除属性的默认值)。 #### clone(...) ```ts clone(options?: Cell.CloneOptions): Cell | Node | Edge | { [id:string]: Node | Edge } ``` 克隆节点/边。 | 选项 | 类型 | 默认值 | 必选 | 描述 | |--------------|---------|---------|:----:|---------------------------------------------------| | options.deep | boolean | `false` | | 是否克隆子孙节点和边,默认为 `false` 表示只克隆自身。 | - 当 `options.deep` 为 `false` 时,返回通克隆创建的新节点/边。 - 当 `options.deep` 为 `true` 时,返回一个对象,对象的 Key 是被克隆节点/边的 ID,对象的 Value 是克隆出来的节点/边。 #### on(...) ```ts on(name: string, handler: Events.Handler, context?: any): this ``` 监听事件。 | 选项 | 类型 | 默认值 | 必选 | 描述 | |---------|----------------|--------|:----:|---------------------| | name | string | | ✓ | 事件名称。 | | handler | Events.Handler | | ✓ | 回调函数。 | | context | any | | | 回调函数的调用上下文。 | #### once(...) ```ts once(name: string, handler: Events.Handler, context?: any): this ``` 监听一次事件,当事件被触发后自动删除该监听。 | 选项 | 类型 | 默认值 | 必选 | 描述 | |---------|----------------|--------|:----:|---------------------| | name | string | | ✓ | 事件名称。 | | handler | Events.Handler | | ✓ | 回调函数。 | | context | any | | | 回调函数的调用上下文。 | #### off(...) ```ts /** * 删除所有事件监听。 */ off(): this /** * 删除指定 name 的所有事件监听。 */ off(name: string): this /** * 删除指定 handler 对应的事件监听。 */ off(name: null, handler: Events.Handler): this /** * 删除指定 name 和 handler 的事件监听。 */ off(name: string, handler: Events.Handler, context?: any): this ``` 删除事件监听。 #### trigger(...) ```ts trigger(name: string, ...args?: any[]): boolean | Promise ``` 触发事件。 | 选项 | 类型 | 默认值 | 必选 | 描述 | |---------|--------|--------|:----:|-------------------| | name | string | | ✓ | 事件名称。 | | ...args | any[] | | | 传给回调函数的参数。 | - 当回调函数都是同步函数时,只要某个回调函数返回 `false` 时就返回 `false`,否则返回 `true`。 - 当回调函数中存在异步函数时,按照同步回调的判断逻辑,返回 `Promise`。 #### dispose() ```ts dispose(): void ``` 销毁并从父节点中移除节点/边。 ### 元素结构 markup 指定了渲染节点/边时使用的 SVG/HTML 结构,使用 [JSON 格式描述](#markup),通常在定义节点/边时通过 [`config`]() 方法将其设置为所有实例共享。当修改 `markup` 时,将触发 `change:markup` 事件和画布重绘。 #### get markup 获取 `markup`。 ```ts const markup = cell.markup ``` #### set markup 设置 `markup`,并触发 `change:markup` 事件和画布重绘。 ```ts cell.markup = markup ``` #### getMarkup() ```ts getMarkup(): Markup ``` 获取 `markup`。 ```ts const markup = cell.getMarkup() ``` #### setMarkup(...) ```ts setMarkup(markup: Markup, options?: Cell.SetOptions): this ``` 设置 `markup`。默认情况触发 `change:markup` 事件和画布重绘,当 `options.silent` 为 `true` 时不触发 `change:markup` 事件和画布重绘。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|-------------------|:----:|---------|----------------------------------------------------| | markup | [Markup](#markup) | ✓ | | | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:markup` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### removeMarkup(...) ```ts removeMarkup(options?: Cell.SetOptions): this ``` 删除 `markup`。默认情况触发 `change:markup` 事件和画布重绘,当 `options.silent` 为 `true` 时不触发 `change:markup` 事件和画布重绘。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|------|---------|--------------------------------------------------| | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:markup` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### 元素属性 attrs 属性 `attrs` 是一个[复杂对象](#attrs-1),当修改 `attrs` 时,将触发 `change:attrs` 事件和画布重绘。 #### get attrs 获取属性。 ```ts const atts = cell.attrs ``` #### set attrs 设置属性,并触发 `change:attrs` 事件和画布重绘。 ```ts cell.atts = attrs ``` #### getAttrs() ```ts getAttrs(): Attr.CellAttrs ``` 获取属性。 ```ts const atts = cell.getAttrs() ``` #### setAttrs(...) ```ts setAttrs(attrs: Attr.CellAttrs, options?: Cell.SetAttrOptions): this ``` 设置属性,默认情况触发 `change:attrs` 事件和画布重绘。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |-------------------|-------------------------------------|:----:|---------|----------------------------------------------------------------------------------------| | attrs | Attr.CellAttrs \| null \| undefined | ✓ | | | | options.overwrite | boolean | | `false` | 为 `true` 时替换现有属性,否则根据 `options.deep` 选项进行深度或浅度 merge。 | | options.deep | boolean | | `true` | 当 `options.overwrite` 为 `false` 时有效, 为 `true` 时进行深度 merge,否则进行浅 merge。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:attrs` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | 默认情况,指定的属性将与旧属性进行[深度 merge](https://lodash.com/docs/4.17.15#merge): ```ts console.log(cell.getAttrs()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } cell.setAttrs({ body: { fill: '#f5f5f5' }, label: { text: 'My Label' }, }) console.log(cell.getAttrs()) // { // body: { fill: '#f5f5f5' }, // label: { fill: '#333333', text: 'My Label' }, // } ``` 当 `options.deep` 为 `false` 时,进行浅 merge: ```ts console.log(cell.getAttrs()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } cell.setAttrs({ label: { text: 'My Label' } }, { deep: false }) console.log(cell.getAttrs()) // { // body: { fill: '#ffffff' }, // label: { text: 'My Label' }, // } ``` 当 `options.overwrite` 为 `true` 时,直接替换旧属性: ```ts console.log(cell.getAttrs()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } cell.setAttrs({ label: { text: 'My Label' } }, { overwrite: true }) console.log(cell.getAttrs()) // { // label: { text: 'My Label' }, // } ``` #### replaceAttrs(...) ```ts replaceAttrs(attrs: Attr.CellAttrs, options: Cell.SetOptions = {}): this ``` 用给定的属性替换原有属性,相当于调用 `setAttrs(attrs, { ...options, overwrite: true })`。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|-------------------------------------|:----:|---------|---------------------------------------------------| | attrs | Attr.CellAttrs \| null \| undefined | ✓ | | | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:attrs` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### updateAttrs(...) ```ts updateAttrs(attrs: Attr.CellAttrs, options: Cell.SetOptions = {}): this ``` 使用浅 merge 更新属性,相当于调用 `setAttrs(attrs, { ...options, deep: false })`。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|-------------------------------------|:----:|---------|---------------------------------------------------| | attrs | Attr.CellAttrs \| null \| undefined | ✓ | | | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:attrs` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### removeAttrs(...) ```ts removeAttrs(options?: Cell.SetOptions): this ``` 删除属性。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|:----:|---------|-------------------------------------------------| | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:attrs` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### getAttrByPath(...) ```ts getAttrByPath(path?: string | string[]): T ``` 根据属性路径获取属性值。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------|--------------------|:----:|--------|--------------------------------------------------------------------------------------------------------------------------------------| | path | string \| string[] | | | 属性路径。当 `path` 为 `string` 类型时,路径是以 `\` 分割的字符串。 当 `path` 为 `string[]` 类型时,路径是属性对象路径上的 Key 构成的数组。 | 某节点的属性值如下: ```ts console.log(cell.getAttrs()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } ``` 当路径为空时返回全部属性: ```ts console.log(cell.getAttrByPath()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } ``` 通过字符串路径获取属性值: ```ts console.log(cell.getAttrByPath('body')) // { fill: '#ffffff' } console.log(cell.getAttrByPath('body/fill')) // '#ffffff' console.log(cell.getAttrByPath('unkonwn')) // undefined console.log(cell.getAttrByPath('body/unkonwn')) // undefined ``` 通过属性对象的 Key 数组构成的路径获取属性值: ```ts console.log(cell.getAttrByPath(['body'])) // { fill: '#ffffff' } console.log(cell.getAttrByPath(['body', 'fill'])) // '#ffffff' console.log(cell.getAttrByPath(['unkonwn'])) // undefined console.log(cell.getAttrByPath(['body', 'unkonwn'])) // undefined ``` #### setAttrByPath(...) ```ts setAttrByPath(path: string | string[], value: Attr.ComplexAttrValue, options?: Cell.SetOptions): this ``` 根据属性路径设置属性值。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|-----------------------|:----:|---------|--------------------------------------------------------------------------------------------------------------------------------------| | path | string \| string[] | ✓ | | 属性路径。 当 `path` 为 `string` 类型时,路径是以 `\` 分割的字符串。当 `path` 为 `string[]` 类型时,路径是属性对象路径上的 Key 构成的数组。 | | value | Attr.ComplexAttrValue | ✓ | | 新属性值。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:attrs` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | 某节点的初始属性值如下: ```ts console.log(cell.getAttrs()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } ``` 通过字符串路径设置属性值: ```ts cell.setAttrByPath('body', { stroke: '#000000' }) // 替换 body 属性值 console.log(cell.getAttrs()) // { // body: { stroke: '#000000' }, // label: { fill: '#333333' }, // } cell.setAttrByPath('body/fill', '#f5f5f5') // 设置 body.fill 属性值 console.log(cell.getAttrs()) // { // body: { stroke: '#000000', fill: '#f5f5f5' }, // label: { fill: '#333333' }, // } ``` 或者通过属性对象的 Key 数组构成的路径设置属性值: ```ts cell.setAttrByPath(['body'], { stroke: '#000000' }) // 替换 body 属性值 console.log(cell.getAttrs()) // { // body: { stroke: '#000000' }, // label: { fill: '#333333' }, // } cell.setAttrByPath(['body', 'fill'], '#f5f5f5') // 设置 body.fill 属性值 console.log(cell.getAttrs()) // { // body: { stroke: '#000000', fill: '#f5f5f5' }, // label: { fill: '#333333' }, // } ``` #### removeAttrByPath(...) ```ts removeAttrByPath(path: string | string[], options?: Cell.SetOption ): this ``` 删除指定路径的属性值。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|--------------------|:----:|---------|---------------------------------------------------------------------------------------------------------------------------------------| | path | string \| string[] | ✓ | | 属性路径。 当 `path` 为 `string` 类型时,路径是以 `\` 分割的字符串。 当 `path` 为 `string[]` 类型时,路径是属性对象路径上的 Key 构成的数组。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:attrs` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | 某节点的初始属性值如下: ```ts console.log(cell.getAttrs()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } ``` 通过字符串路径删除属性值: ```ts cell.removeAttrByPath('body/fill') console.log(cell.getAttrs()) // { // body: { }, // label: { fill: '#333333' }, // } cell.removeAttrByPath('body') console.log(cell.getAttrs()) // { // label: { fill: '#333333' }, // } ``` 或者通过属性对象的 Key 数组构成的路径删除属性值: ```ts cell.removeAttrByPath(['body', 'fill']) console.log(cell.getAttrs()) // { // body: { }, // label: { fill: '#333333' }, // } cell.removeAttrByPath(['body']) console.log(cell.getAttrs()) // { // label: { fill: '#333333' }, // } ``` #### attr(...) ```ts /** * 获取属性。 */ attr(): Cell.CellAttrs /** * 获取指定路径上的属性值。 */ attr(path: string | string[]): T /** * 设置指定路径上的属性值。 */ attr(path: string | string[], value: Attr.ComplexAttrValue | null, options?: Cell.SetOptions): this /** * 设置属性值,传入的属性与旧属性进行深度 merge。 */ attr(attrs: Attr.CellAttrs, options?: Cell.SetOptions): this ``` 该方法是 [`getAttrByPath`](#getattrbypath)、[`setAttrByPath`](#setattrbypath) 和 [`setAttrs`](#setattrs) 三个方法的整合,提供了上面四种函数签名,是一个非常实用的方法。 获取全部属性值: ```ts console.log(cell.attr()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } ``` 获取指定路径上的属性值: ```ts console.log(cell.attr('body/fill')) // '#ffffff' ``` 设置指定路径上的属性值: ```ts cell.attr('body/fill', '#f5f5f5') console.log(cell.attr()) // { // body: { fill: '#f5f5f5' }, // label: { fill: '#333333' }, // } ``` 通过属性对象设置属性值,与就属性对象进行[深度 merge](https://lodash.com/docs/4.17.15#merge)。 ```ts cell.attr({ body: { stroke: '#000000' }, label: { fill: 'blue', text: 'my lable' }, }) console.log(cell.attr()) // { // body: { fill: '#f5f5f5', stroke: '#000000' }, // label: { fill: 'blue', text: 'my lable' }, // } ``` ### 层级 zIndex `zIndex` 是节点/边在画布中的层级,默认根据节点/边添加顺序自动确定。当修改 `zIndex` 时,将触发 `change:zIndex` 事件和画布重绘。 #### get zIndex 获取 `zIndex`。 ```ts const z = cell.zIndex ``` #### set zIndex 设置 `zIndex`,触发 `change:zIndex` 事件和画布重绘。 ```ts cell.zIndex = 2 ``` #### getZIndex() ```ts getZIndex(): number ``` 获取 `zIndex`。 ```ts const z = cell.getZIndex() ``` #### setZIndex(...) ```ts setZIndex(zIndex: number, options?: Cell.SetOptions): this ``` 设置 `zIndex`。默认情况触发 `change:zIndex` 事件和画布重绘,当 `options.silent` 为 `true` 时不触发 `change:zIndex` 事件和画布重绘。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|:----:|---------|----------------------------------------------------| | zIndex | number | ✓ | | | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:zIndex` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### removeZIndex(...) ```ts removeZIndex(options?: Cell.SetOptions): this ``` 删除 `zIndex`。默认情况触发 `change:zIndex` 事件和画布重绘,当 `options.silent` 为 `true` 时不触发 `change:zIndex` 事件和画布重绘。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|------|---------|--------------------------------------------------| | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:zIndex` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### toFront(...) ```ts toFront(options?: Cell.ToFrontOptions): this ``` 将节点/边移到最顶层。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|------|---------|--------------------------------------------------| | options.deep | boolean | | `false` | 为 `true` 时同时更新所有子节点/边的层级。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:zIndex` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### toBack(...) ```ts toBack(options?: Cell.ToBackOptions): this ``` 将节点/边移到最底层。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|------|---------|--------------------------------------------------| | options.deep | boolean | | `false` | 为 `true` 时同时更新所有子节点/边的层级。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:zIndex` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | 默认情况,更新 `zIndex` 时,触发 `change:zIndex` 事件和画布重绘: ```ts cell.toBack() ``` 当 `options.deep` 为 `true` 时,同时更新所有子节点/边的层级。: ```ts cell.toBack({ deep: true }) ``` ### 可见性 Visible #### get visible 返回节点/边是否可见。 ```ts if (cell.visible) { // do something } ``` #### set visible 设置节点/边是否可见,并触发 `change:visible` 事件和画布重绘。 #### show(...) ```ts show(options?: Cell.SetOptions): this ``` 显示节点/边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|:----:|---------|---------------------------------------------------| | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:visible` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### hide(...) ```ts hide(options?: Cell.SetOptions): this ``` 隐藏节点/边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|:----:|---------|---------------------------------------------------| | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:visible` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | > **提示** X6 2.x 是通过在节点标签上增加 `display: none` 来实现元素隐藏。 #### isVisible() ```ts isVisible(): boolean ``` 返回节点/边是否可见。 #### setVisible(...) ```ts setVisible(visible: boolean, options?: Cell.SetOptions): this ``` 设置节点/边的可见性。默认情况触发 `change:visible` 事件和画布重绘,当 `options.silent` 为 `true` 时不触发 `change:visible` 事件和画布重绘。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|:----:|---------|-----------------------------------------------------| | visible | boolean | ✓ | | | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:visible` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### toggleVisible(...) ```ts toggleVisible(options?: Cell.SetOptions): this ``` 切换节点/边的可见性。默认情况触发 `change:visible` 事件和画布重绘,当 `options.silent` 为 `true` 时不触发 `change:visible` 事件和画布重绘。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|:----:|---------|---------------------------------------------------| | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:visible` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### 业务数据 Data 与节点/边关联的业务数据。例如,我们在实际使用时通常会将某些业务数据存在节点/边的 data 上。 ```ts const rect = new Shape.Rect({ x: 40, y: 40, width: 100, height: 40, data: { bizID: 125, date: '20200630', price: 89.0, }, }) ``` #### get data 获取关联的数据。 #### set data 设置关联的数据,并触发 `change:data` 事件和画布重绘。 #### getData() ```ts getData(): any ``` 获取关联的数据。 #### setData(...) ```ts setData(data: any, options?: Cell.SetDataOptions): this ``` 设置关联的业务数据。默认情况触发 `change:data` 事件和画布重绘,当 `options.silent` 为 `true` 时不触发 `change:data` 事件和画布重绘。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |-------------------|---------|:----:|---------|----------------------------------------------------------------------------------------| | data | any | ✓ | | | | options.overwrite | boolean | | `false` | 为 `true` 时替换现有值,否则根据 `options.deep` 选项进行深度或浅度 merge。 | | options.deep | boolean | | `true` | 当 `options.overwrite` 为 `false` 时有效, 为 `true` 时进行深度 merge,否则进行浅 merge。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:data` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | 默认与原数据进行[深度 merge](https://lodash.com/docs/4.17.15#merge),并触发 `change:data` 事件和画布重绘: ```ts cell.setData(data) ``` 当 `options.overwrite` 为 `true` 时,替换旧数据: ```ts cell.setData(data, { overwrite: true }) ``` 当 `options.deep` 为 `false` 时,与原数据进行浅 merge: ```ts cell.setData(data, { deep: false }) ``` > **提示** `setData` 方法是通过浅比较来判断数据是否有更新,从而决定是否触发节点重绘。 ```ts const obj = { name: 'x6', star: true } node.setData(obj) // 此时会触发节点重绘 obj.star = false node.setData(obj) // 注意,此时不会进行深比较,判定对象未发生修改,不会触发节点重绘 node.setData({ ...obj, star: false, }) // 此时会触发节点重绘 ``` #### replaceData(...) ```ts replaceData(data: any, options: Cell.SetOptions = {}): this ``` 用指定的数据替换原数据,相当于调用 `setData(data, { ...options, overwrite: true })`。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|:----:|---------|--------------------------------------------------| | data | any | ✓ | | | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:data` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### updateData(...) ```ts updateData(data: any, options: Cell.SetOptions = {}): this ``` 通过浅 merge 来更新数据,相当于调用 `setData(data, { ...options, deep: false })`。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|:----:|---------|--------------------------------------------------| | data | any | ✓ | | | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:data` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### removeData(...) ```ts removeData(options: Cell.SetOptions): this ``` 删除数据。默认情况触发 `change:data` 事件和画布重绘,当 `options.silent` 为 `true` 时不触发 `change:data` 事件和画布重绘。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|------|---------|------------------------------------------------| | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:data` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### 父子关系 Parent/Children #### get parent 获取父节点。 #### getParent() ```ts getParent(): Cell | null ``` 获取父节点。当父节点存在是返回父节点,否则返回 `null`。 #### setParent(...) ```ts setParent(parent: Cell | null, options?: Cell.SetOptions): this ``` 设置父节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|--------------|:----:|---------|---------------------------------------------------| | parent | Cell \| null | ✓ | | 父节点或 `null`,当 `parent` 为 `null` 时删除父节点。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:parent` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### getParentId() ```ts getParentId(): string | undefined ``` 获取父节点的 ID。父节点存在是返回父节点的 ID,否则返回 `undefined`。 #### hasParent() ```ts hasParent(): boolean ``` 检查节点/边是否有父节点。 #### get children 获取所有子节点/边。 #### getChildren() ```ts getChildren(): Cell[] | null ``` 获取所有子节点/边。 #### setChildren() ```ts setChildren(children: Cell[] | null, options?: Cell.SetOptions) ``` 设置子节点/边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|----------------|:----:|---------|---------------------------------------------------------------| | children | Cell[] \| null | ✓ | | 子节点/边数组或 `null`,当 `children` 为 `null` 时清空子节点/边。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:children` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### isParentOf(...) ```ts isParentOf(child: Cell | null): boolean ``` 返回当前节点是否是指定 Cell 的父节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |-------|--------------|:----:|--------|------| | child | Cell \| null | ✓ | | | #### isChildOf(...) ```ts isChildOf(parent: Cell | null): boolean ``` 返回当前节点/边是否是指定节点的的子节点/边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |--------|--------------|:----:|--------|------| | parent | Cell \| null | ✓ | | | #### eachChild(...) ```ts eachChild(iterator: (child: Cell, index: number, children: Cell[]) => void, context?: any): this ``` 遍历子节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |----------|--------------------------------------------------------|:----:|--------|-----------------------| | iterator | (child: Cell, index: number, children: Cell[]) => void | ✓ | | 迭代器函数。 | | context | any | | | 迭代器函数的执行上下文。 | #### filterChild(...) ```ts filterChild(iterator: (child: Cell, index: number, children: Cell[]) => boolean, context?: any): Cell[] ``` 过滤子节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |----------|-----------------------------------------------------------|:----:|--------|-----------------------| | iterator | (child: Cell, index: number, children: Cell[]) => boolean | ✓ | | 过滤器函数。 | | context | any | | | 过滤器函数的执行上下文。 | #### getChildCount() ```ts getChildCount(): number ``` 获取子节点/边的数量。 #### getChildIndex(...) ```ts getChildIndex(child: Cell): number ``` 获取子节点/边的索引。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |-------|------|:----:|--------|------| | child | Cell | ✓ | | | #### getChildAt(...) ```ts getChildAt(index: number): Cell | null ``` 获取指定索引位置的子节点/边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |-------|--------|:----:|--------|---------| | index | number | ✓ | | 索引位置。 | #### getAncestors(...) ```ts getAncestors(options?: { deep?: boolean }): Cell[] ``` 获取所有祖先节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |--------------|---------|:----:|--------|--------------------------------------------------------------------| | options.deep | boolean | | `true` | 默认递归获取所有祖先节点,设置为 `false` 时只返回当前节点/边的父节点。 | #### getDescendants(...) ```ts getDescendants(options?: Cell.GetDescendantsOptions): Cell[] ``` 获取所有子孙节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |----------------------|---------|:----:|---------|--------------------------------------------------------------------| | options.deep | boolean | | `true` | 默认递归获取所有子孙节点,设置为 `false` 时只返回当前节点孩子节点/边。 | | options.breadthFirst | boolean | | `false` | 默认使用深度优先算法,设置为 `true` 时使用广度优先搜索算法。 | 返回子孙节点/边的数组。 #### isDescendantOf(...) ```ts isDescendantOf(ancestor: Cell | null, options?: { deep?: boolean }): boolean ``` 返回当前节点/边是否是指定节点的子孙节点/边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |--------------|--------------|:----:|--------|-------------------------------------------------------------------------| | ancestor | Cell \| null | ✓ | | 指定节点。 | | options.deep | boolean | | `true` | 默认递归判断指定节点的所有子孙节点/边,设置为 `false` 时只判断孩子节点/边。 | #### isAncestorOf(...) ```ts isAncestorOf(descendant: Cell | null, options?: { deep?: boolean }): boolean ``` 返回当前节点是否是指定节点/边的祖先节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |--------------|--------------|:----:|--------|-------------------------------------------------------------------------| | descendant | Cell \| null | ✓ | | 指定节点/边。 | | options.deep | boolean | | `true` | 默认递归判断指定节点的所有子孙节点/边,设置为 `false` 时只判断孩子节点/边。 | #### getCommonAncestor(...) ```ts getCommonAncestor(...cells: (Cell | null | undefined)[]): Cell | null ``` 获取给定节点/边的共同祖先节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |----------|-------------------------------|:----:|--------|------------| | ...cells | (Cell \| null \| undefined)[] | ✓ | | 指定节点/边。 | #### addChild(...) ```ts addChild(child: Cell, options?: Cell.SetOptions): this ``` 将指定的节点/边添加到当前节点的子节点的末尾。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|:----:|---------|----------------------------------------------------| | child | Cell | ✓ | | 指定的节点/边。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:children` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### removeChild(...) ```ts removeChild(child: Cell, options?: Cell.RemoveOptions): Cell | null ``` 移除指定的子节点/边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|:----:|---------|-------------------------------------------------------------| | child | Cell | ✓ | | 指定的节点/边。 | | options.deep | boolean | | `true` | 默认递归移除所有子节点/边,设置为 `false` 时只移除当前节点/边。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:children` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### remove(...) ```ts remove(options?: Cell.RemoveOptions): this ``` 先将当前节点/边从父节点,然后将其从画布中移除。 ### 节点和边的属性 Properties 上面介绍的 `markup`、`attrs`、`zIndex`、`data` 等基础选项,以及节点的 `size`、 `position`、`angle`、`ports` 等选项,还有边的 `source`、`target`、`labels` 等选项,还有创建节点/边时提供的那些额外的键值对,我们都称为属性(Property)。 ```ts const rect = new Shape.Rect({ x: 30, y: 30, width: 100, height: 40, attrs: {...}, data: {...}, zIndex: 10, sale: {...}, product: { id: '1234', name: 'apple', price: 3.99, }, }) ``` 例如,上面代码中的 `attrs`、`data`、`zIndex` 都是标准的属性,其中 `x` 和 `y` 是一对[自定义选项](#自定义选项),节点初始化时被转换为了 `position` 属性,同样 `width` 和 `height` 也是一对[自定义选项](#自定义选项),节点初始化时被转换为了 `size` 属性,最后剩余的 `sale` 和 `product` 两个对象是非标准的属性。 上面介绍了一些标准属性以及操作(`get`/`set`)这些标准属性的方法,下面再介绍几个比较通用的方法,这些方法对标准和非标准属性都适用。 #### getProp(...) 获取指定的属性值。 ```ts getProp(key: string, defaultValue?: T): T ``` | 名称 | 类型 | 必选 | 默认值 | 描述 | |--------------|--------|:----:|--------|----------------------------------------| | key | string | ✓ | | 属性名称。 | | defaultValue | T | | - | 默认值,当指定的属性不存在时返回该默认值。 | ```ts // 获取标准属性 const zIndex = rect.getProp('zIndex') const position = rect.getProp<{ x: number; y: number }>('position') // 获取非标准属性 const product = rect.getProp('product') ``` #### setProp(...) 设置指定的属性,默认情况触发对应的 `change:xxx` 事件和画布重绘,当 `options.silent` 为 `true` 时不触发。 ```ts // 设置指定的属性 setProp(key: string, value: any, options?: Cell.SetOptions): this // 批量设置属性,提供的属性与原属性进行深度 merge。注意使用这种方式会触发 propHooks 的调用 setProp(props: Partial, options?: Cell.SetOptions): this ``` | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|-----------------------|:----:|---------|---------------------------------------------------------------------------------------------------| | key | string | ✓ | | 属性名称。 | | value | any | ✓ | | 属性值。 | | props | `Partial` | ✓ | | 属性键值对,将与现有属性进行[深度 merge](https://lodash.com/docs/4.17.15#merge)。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:markup` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ```ts // 设置单个属性: rect.setProp('size', { width: 100, height: 30 }) rect.setProp('zIndex', 10) // 同时设置多个属性 rect.setProp({ size: { width: 100, height: 30, }, zIndex: 10, }) ``` #### removeProp(...) 删除指定路径的属性。默认情况触发对应的 `change:xxx` 事件和画布重绘,当 `options.silent` 为 `true` 时不触发。 ```ts removeProp(path: string | string[], options?: Cell.SetOptions): this ``` | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|--------------------|:----:|---------|--------------------------------------------------| | path | string \| string[] | ✓ | | 属性路径。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:markup` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ```ts rect.removeProp('zIndex') rect.removeProp('product/id') ``` #### prop(...) 该方法是上面几个方法的整合,提供了上面四种函数签名,是一个非常实用的方法。 ```ts prop(): Properties // 获取所有属性。 prop(path: string | string[]): T // 获取指定路径的属性值。 prop(path: string | string[], value: any, options?: Cell.SetOptions): this // 设置指定路径的属性值,与路径上的现有属性进行深度 merge。 prop(props: Partial, options?: Cell.SetOptions): this // 设置属性,与现有属性进行深度 merge。 ``` ```ts // 获取属性: rect.prop() rect.prop('zIndex') rect.prop('product/price') // 设置属性: rect.prop('zIndex', 10) rect.prop('product/price', 5.99) rect.prop({ product: { id: '234', name: 'banana', price: 3.99, }, }) ``` #### hasChanged(...) ```ts hasChanged(key: string | undefined | null): boolean ``` 返回指定的属性或所有属性是否已经改变。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------|-----------------------------|:----:|--------|------------------------------| | key | string \| undefined \| null | | | 属性名。缺省时表示检查所有属性。 | #### previous(...) ```ts previous(name: string): T | undefined ``` 当指定的属性发生改变后,获取改变前的属性值。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------|--------|:----:|--------|-------| | key | string | ✓ | | 属性名。 | ### 工具集 Tools #### addTools(...) ```ts addTools( items: Cell.ToolItem | Cell.ToolItem[], options?: Cell.AddToolOptions, ): this addTools( items: Cell.ToolItem | Cell.ToolItem[], name: string, options?: Cell.AddToolOptions, ): this ``` 添加小工具。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|----------------------------------|:----:|---------|-------------------------------------------------------------------------------------------------------------------------------------| | items | Cell.ToolItem \| Cell.ToolItem[] | | | [NodeTool](/zh/api/registry/node-tool#presets) 或 [EdgeTool](/zh/api/registry/edge-tool#presets) 中定义的小工具。 | | name | string | | `null` | 定义该组工具的别称,可以作为`hasTools(name)`的参数 | | options.reset | boolean | | `false` | 是否清空工具集,默认向工具集追加小工具。 | | options.local | boolean | | `false` | 工具是否渲染到节点/边的容器中,默认为 `false`,所有工具会渲染在 `x6-graph-svg-decorator` 下面,只有在 `options.reset` 为 `true` 为时生效 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:tools` 事件和小工具重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### getTools() ```ts getTools(): Cell.Tools | null ``` 获取工具集。 #### removeTools(...) ```ts removeTools(options?: Cell.SetOptions): this ``` 删除所有工具。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|:----:|---------|---------------------------------------------------| | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:tools` 事件和小工具重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### hasTool(...) ```ts hasTool(name: string): boolean ``` 是否包含指定名称的工具。 #### removeTool(...) ```ts removeTool(name: string, options?: Cell.SetOptions): this removeTool(index: number, options?: Cell.SetOptions): this ``` 删除指定名称的工具。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|------------------|:----:|---------|---------------------------------------------------| | nameOrIndex | string \| number | ✓ | | 工具名称或索引。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `change:tools` 事件和小工具重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ## config(...) ```ts config(presets: C): void ``` 设置节点/边的选项默认值。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |-------------------|------------------------|:----:|--------|----------------------------------------------------------------------------------------------| | options.propHooks | Cell.PropHooks\ | | | 自定义选项。 | | options.attrHooks | Attr.Definitions | | | 自定义属性键值对。Key 是自定义属性的属性名,Value 是自定义属性对象(包含属性检查、应用属性等方法)。 | | options...others | object | | | 其他选项,节点/边的属性(Properties)。 | ### 选项默认值 该方法对自定义节点/边非常友好,方便我们为我们的节点/边设置一些预设选项。例如,我们在定义矩形节点时,为其指定了默认 Markup、默认大小和默认样式。 ```ts Shape.Rect.config({ width: 80, height: 40, markup: ..., attrs: ..., }) ``` 我们创建矩形的代码就可以非常简单: ```ts const rect = graph.addNode({ x: 100, y: 100, attrs: { label: { text: 'rect', }, }, }) ``` 每次调用 `config(presets)` 都是与当前预设值进行[深度 merge](https://lodash.com/docs/4.17.15#merge),例如下面代码分别将矩形的边框默认颜色修改为红色和将默认文本颜色修改为蓝色,最终效果是两者的叠加: ```ts // 只修改边框的默认颜色 Shape.Rect.config({ attrs: { body: { stroke: 'red', }, }, }) // 只修改默认文本颜色 Shape.Rect.config({ attrs: { label: { fill: 'blue', // 覆盖上面定义的 red stroke: '#000', }, }, }) ``` ### 自定义选项 创建矩形时我们可以使用 `label` 来设置矩形的标签文本: ```ts const rect = graph.addNode({ x: 100, y: 100, label: 'rect', }) ``` 而我们并没有为矩形定义 `label` 这个选项,那这个 `label` 是怎么应用到 `attrs/label/text` 上的呢?这就用到了 `propHooks` 钩子,我们可以定义 `propHooks` 钩子来消费这些非标准的选项。 看下面 `label` 选项钩子的实现细节: ```ts 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` 钩子,我们很容易就扩展出一些自定义的选项。例如,我们可以将某些样式定义为节点的选项,这样不仅可以减少嵌套,而且使创建节点的代码语义性更强。 看下面的代码,为矩形定义 `rx` 和 `ry` 自定义选项: ```ts 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 }, }, }) ``` 这样,我们就可以很方便添加圆角矩形: ```ts const rect = graph.addNode({ x: 100, y: 100, rx: 5, ry: 10, label: 'rect', }) ``` ### 自定义属性 自定义属性是指那些非标准的 SVG/HTML 属性,如系统内置的 `refWidth`、`refHeight`、`sourceMarker`、`targetMarker` 等属性。这些属性都是全局共享的,我们可以通过 `attrHooks` 钩子来为节点/边定义**独享**的自定义属性。 例如: ```ts import { Shape, Color } from '@antv/x6' Shape.Rect.config({ attrHooks: { fill: { set(val) { return Color.invert(val) // 自动反转填充色 }, }, theme: { set(val) { // 同时设置填充色和边框色 return { fill: val, stroke: Color.invert(val), } }, }, }, }) ``` 我们可以这样来使用上面定义的 `fill` 和 `theme` 属性: ```ts const rect = graph.addNode({ x: 100, y: 100, rx: 5, ry: 10, label: 'rect', attrs: { body: { theme: '#f5f5f5', }, label: { fill: '#fff', }, }, }) ``` ## 静态方法 ### generateId 我们在 Cell 基类上提供了一个静态方法 `generateId` 来快速实现自定义节点/边 ID。 ```ts generateId(metadata: Cell.Metadata): string ``` 例如: ```ts import { Cell } from '@antv/x6' let sid = 0 Cell.generateId = ({ shape }) => { return `${shape}_${sid++}` } ``` #### 节点 - Canonical page: https://x6.antv.antgroup.com/zh/api/model/node/ - Markdown source: https://x6.antv.antgroup.com/zh/api/model/node.md - Description: 节点 --- title: "节点" description: "节点" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/model/node/" version: "3.x" --- Node 是所有节点的基类,继承自 [Cell](/zh/api/model/cell),并定义了节点的通用属性和方法。 ## 属性 除了从 Cell 继承[属性](/zh/api/model/cell#属性)外,还支持以下属性。 | 选项 | 类型 | 默认值 | 必选 | 描述 | | --- | --- | --- | :-: | --- | | size | `{ width: number; height: number }` | `{ width: 1, height: 1 }` | | 节点大小。 | | position | `{ x: number; y: number } ` | - | | 节点位置。 | | angle | number | - | | 节点的旋转角度。 | | ports | object | - | | 连接桩。 | | portMarkup | Markup | object | | 连接桩的 DOM 结构。 | | portLabelMarkup | Markup | object | | 连接桩标签的 DOM 结构。 | ### size 节点大小,是一个包含 `width` 和 `height` 属性的对象,可以通过 [`size(...)`](#size-1) 方法来获取和设置节点大小。 ### position 节点位置,是一个包含 `x` 和 `y` 属性的对象,可以通过 [`position(...)`](#position-1) 方法来获取和设置节点位置。 ### angle 节点的旋转角度,旋转中心为节点的中心,可以通过 [`rotate(...)`](#rotate) 方法来获取和设置节点的旋转角度。 ### ports 连接桩是节点上的固定连接点,很多图应用都有连接桩,并且有些应用还将连接桩分为输入连接桩和输出连接桩。 连接桩选项 `ports` 是一个复杂对象,可以像下面这样使用。 ```ts const node = new Node({ ports: { group: { ... }, // 连接桩组定义 items: [ ... ], // 连接桩 } }) ``` 或者 ```ts const node = new Node({ ports: [ ... ], // 连接桩 }) ``` 通常我们将具有相同行为和外观的连接桩归为同一组,并通过 `group` 选项来设置分组,该选项是一个对象 `{ [groupName: string]: PortGroupMetadata }`,组名为键,值为每组连接桩的默认选项,支持的选项如下: ```ts interface PortGroupMetadata { /** * 连接桩 DOM 结构定义。 */ markup?: Markup /** * 属性和样式。 */ attrs?: Attr.CellAttrs /** * 连接桩的 DOM 层级,值越大层级越高。 */ zIndex?: number | 'auto' /** * 群组中连接桩的布局。 */ position?: | [number, number] // 绝对定位 | string // 连接桩布局方法的名称 | { // 连接桩布局方法的名称和参数 name: string args?: object } /** * 连接桩标签。 */ label?: { markup?: Markup position?: { // 连接桩标签布局 name: string // 布局名称 args?: object // 布局参数 } } } ``` 例如: ```ts const node = new Node({ ports: { group: { group1: { markup: { tagName: 'circle' }, attrs: { }, zIndex: 1, position: { name: 'top', args: {}, }, }, group2: { ... }, group3: { ... }, }, items: [ ... ], } }) ``` 另一个选项 `items` 是一个数组 `PortMetadata[]`,数组的每一项表示一个连接桩,连接桩支持的选项如下: ```ts interface PortMetadata { /** * 连接桩唯一 ID,默认自动生成。 */ id?: string /** * 分组名称,指定分组后将继承分组中的连接桩选项。 */ group?: string /** * 为群组中指定的连接桩布局算法提供参数。 * 我们不能为单个连接桩指定布局算法,但可以为群组中指定的布局算法提供不同的参数。 */ args?: object /** * 连接桩的 DOM 元素和结构定义。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。 */ markup?: Markup /** * 元素的属性样式。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。 */ attrs?: Attr.CellAttrs /** * 连接桩的 DOM 层级,值越大层级越高。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。 */ zIndex?: number | 'auto' /** * 连接桩标签。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。 */ label?: { markup?: Markup position?: { // 连接桩标签布局 name: string // 布局名称 args?: object // 布局参数 } } } ``` 例如: ```ts const node = new Node({ ports: { group: { ... }, items: [ { id: 'port1', group: 'group1', ... }, { id: 'port2', group: 'group1', ... }, { id: 'port3', group: 'group2', ... }, ], } }) ``` 更多详情请参考[配置连接桩](#连接桩-ports)文档。 ### portMarkup 连接桩的 DOM 结构。当 `ports.groups` 和 `ports.items` 都没有为对应的连接桩指定 `markup` 时,则使用这个默认选项来渲染连接桩,其默认值为: ```ts { tagName: 'circle', selector: 'circle', attrs: { r: 10, fill: '#fff', stroke: '#000', }, } ``` 表示用半径为 `10px` 的圆来渲染连接桩。 ### portLabelMarkup 连接桩标签的 DOM 结构。当 `ports.groups` 和 `ports.items` 都没有为对应的连接桩标签指定 `markup` 时,则使用这个默认选项来渲染连接桩标签,其默认值为: ```ts { tagName: 'text', selector: 'text', attrs: { fill: '#000000', }, } ``` ## 方法 ### 通用 #### isNode() ```ts isNode(): true ``` 判断是不是节点,该方法始终返回 `true`。 #### getBBox(...) ```ts getBBox(options: { deep?: boolean }): Rectangle ``` 获取节点的包围盒。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | options.deep | boolean | | `false` | 为 `true` 时表示包含所有子节点和边的包围盒,默认为 `false `。 | ```ts const rect1 = node.getBBox() const rect2 = node.getBBox({ deep: true }) ``` ### 大小 Size #### size(...) ```ts /** * 获取节点大小。 */ size(): Size /** * 设置节点大小。 */ size(size: Size, options?: Node.ResizeOptions): this /** * 设置节点大小。 */ size(width: number, height: number, options?: Node.ResizeOptions): this ``` 获取节点大小。 ```ts const size = node.size() console.log(size.width, size.height) ``` 设置节点大小的参数和使用方法与 [`resize`](#resize) 方法一样。 #### resize(...) 改变节点大小。根据旋转角度和 `options.direction` 的不同,节点位置和大小都可能发生改变。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | width | number | | | 节点宽度。 | | height | number | | | 节点高度。 | | options.direction | Direction | | `'bottom-right'` | 向哪个方向改变大小,默认左上角固定,往右下角改变节点大小。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:size'` 和 `'change:position'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | 支持向 8 个方向改变节点大小,默认为 `'bottom-right'` 表示左上角固定,往右下角改变节点大小。 - top - right - bottom - left - top-left - top-right - bottom-left - bottom-right ```ts node.resize(100, 40) // 右下角固定,向左上角改变大小 node.resize(100, 40, { direction: 'top-left' }) // 不触发事件和重绘 node.resize(100, 40, { silent: true }) ``` #### scale(...) ```ts scale( sx: number, sy: number, origin?: Point.PointLike, options?: Node.SetOptions, ): this ``` 缩放节点。根据缩放中心和缩放比例不同,节点的大小和位置都可能发生改变。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | sx | number | ✓ | | X 轴方向的缩放比例。 | | sy | number | ✓ | | Y 轴方向的缩放比例。 | | origin | Point.PointLike \| null | | - | 缩放中心,默认为节点中心。 | | options.silent | boolean | | `false` | 为 `true` 时不触不触发 `'change:size'` 和 `'change:position'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ```ts node.scale(1.5, 1.5) // 自定义缩放中心 node.scale(1.5, 1.5, { x: 100, y: 30 }) // 不触发事件和重绘 node.scale(1.5, 1.5, null, { silent: true }) ``` #### fit(...) ```ts fit(options?: Node.FitEmbedsOptions): this ``` 根据子节点和边的大小位置,自动调整节点的大小和位置,使所有子节点和边都位于节点的包围盒内。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | options.padding | number \| `{ top: number; right: number; bottom: number; left: number }` | | `0` | 边距。 | | options.deep | boolean | | `false` | 是否包含所有后代节点和边,默认只包含直接子节点和边。 | | options.silent | boolean | | `false` | 为 `true` 时不触不触发事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ```ts node.fit() node.fit({ padding: 10 }) node.fit({ padding: { top: 20, bottom: 20, left: 10, right: 10 } }) ``` ### 位置 Position #### position(...) ```ts /** * 获取节点位置。 */ position(options?: Node.GetPositionOptions): Point.PointLike /** * 设置节点位置。 */ position(x: number, y: number, options?: Node.SetPositionOptions): this ``` 获取节点位置。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | options.relative | boolean | | `false` | 是否返回相对于父节点的相对位置,默认为 `false` 表示返回节点相对于画布的绝对位置。 | ```ts const pos = rect.position() console.log(pos.x, pos.y) const relativePos = child.position({ relative: true }) console.log(relativePos.x, relativePos.y) ``` 设置节点位置。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | x | number | ✓ | | 节点绝对或相对 X 轴坐标。 | | y | number | ✓ | | 节点绝对或相对 Y 轴坐标。 | | options.relative | boolean | | `false` | 提供的坐标是否为相对坐标。为 `true` 时表示提供的坐标为相对于父节点位置的坐标,默认为 `false` 表示提供的坐标为相对于画布的绝对坐标。 | | options.deep | boolean | | `false` | 是否同时改变子节点/边的位置。 | | options.silent | boolean | | `false` | 为 `true` 时不触不触发 `'change:position'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | 默认使用绝对坐标,当 `options.relative` 为 `true` 时表示使用相对坐标。 ```ts // 将节点移动到画布 [30, 30] 位置处 node.position(30, 30) // 将子节点移动到相对父节点左上角 [30, 30] 位置处 child.position(30, 30, { relative: true }) ``` 当 `options.deep` 为 `true` 时将同时移动子节点和边。 ```ts parent.position(30, 30, { deep: true }) ``` 当 `options.silent` 为 `true` 时不触不触发 `'change:position'` 事件和画布重绘。 ```ts node.position(30, 30, { silent: true }) ``` 在选项中支持其他自定义键值对,可以在事件回调用使用。 ```ts node.position(30, 30, { otherKey: 'otherValue', ... }) ``` #### translate(...) ```ts translate(tx?: number, ty?: number, options?: Node.TranslateOptions): this ``` 平移节点以及节点包含的子节点和边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | tx | number | | `0` | 节点在 X 轴的偏移量。 | | ty | number | | `0` | 节点在 Y 轴的偏移量。 | | options.restrict | Rectangle.RectangleLike | | `undefined` | 将节点的可移动范围限制在指定的矩形区域内。 | | options.transition | boolean \| KeyframeEffectOptions | | `false` | 是否使用动画或指定一个动画选项。 | | options.silent | boolean | | `false` | 为 `true` 时不触不触发 `'change:position'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | 当指定的 `tx` 和 `ty` 为 `undefined` 时,表示对应方向的平移量为 `0`。 ```ts node.translate(30, 30) node.translate(30) // 只在 X 轴移动 node.translate(undefined, 30) // 只在 Y 轴移动 ``` 我们可以通过 `options.restrict` 选项来将节点的移动限制在指定的矩形 `{x: number; y: number; width: number; height: number}` 范围内。 例如,我们可以将子节点的移动限制在父节点的包围盒中: ```ts child.translate(30, 30, { restrict: child.getParent().getBBox(), }) ``` 当 `options.transition` 为 `true` 或指定了一个动画选项时,表示使用动画来平移节点。 ```ts // 使用默认动画在平移节点 node.translate(30, 30, { transition: true, }) // 自定动画选项 node.translate(30, 30, { transition: { duration: 2000, }, }) ``` 当 `options.silent` 为 true 时不触不触发 `'change:position'` 事件和画布重绘。 ```ts node.translate(30, 30, { silent: true }) ``` 在选项中支持其他自定义键值对,可以在事件回调用使用。 ```ts node.translate(30, 30, { otherKey: 'otherValue', ... }) ``` ### 旋转角度 Angle #### getAngle() ```ts getAngle(): number ``` 获取节点的旋转角度。 ```ts if (node.getAngle() !== 0) { // do something } ``` #### rotate(...) ```ts rotate( deg: number, absolute?: boolean, origin?: Point.PointLike, options?: Node.RotateOptions, ): this ``` 旋转节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | deg | number | ✓ | | 旋转度数。 | | options.absolute | boolean | | `false` | 为 `true` 时表示给定的度数为节点旋转后的绝对度数,默认为 `false`,表示节点在当前旋转角度的基础上再旋转给定的度数。 | | options.center | Point.PointLike | | `undefined` | 默认沿节点中心旋转,当给定 `options.center` 后表示沿给定的中心旋转。 | | options.silent | boolean | | `false` | 为 `true` 时不触不触发 `'change:angle'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | 默认为相对旋转,即在当前旋转角度的基础上在旋转给定的度数,当 `options.absolute` 为 `true` 时表示绝对旋转,即给定的度数为节点旋转后的度数。 ```ts // 相对旋转,节点在当前旋转角度的基础上在旋转 30 度 node.rotate(30) // 绝对旋转,节点旋转后的角度为 30 度 node.rotate(30, { absolute: true }) ``` 默认沿节点中心旋转,可以通过 `options.center` 选项来指定一个旋转中心。 ```ts node.rotate(30, { center: { x: 10, y: 10 } }) ``` 默认触发 `'change:angle'` 事件和画布重绘,当 `options.silent` 为 `true` 时,不触发 `'change:angle'` 事件和画布重绘。 ```ts node.rotate(30, { silent: true }) ``` 在选项中支持其他自定义键值对,可以在事件回调用使用。 ```ts node.rotate(30, { otherKey: 'otherValue', ... }) ``` ### 连接桩 Ports 连接桩是节点上的固定连接点,很多图应用都有连接桩,并且有些应用还将连接桩分为输入连接桩和输出连接桩。 在上面我们介绍了连接桩的数据结构,这里我们将继续介绍节点上操作连接桩的一些方法。 #### addPort(...) ```ts addPort(port: PortMetadata, options?: Node.SetOptions): this ``` 添加单个连接桩。连接桩被添加到连接桩数组末尾。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | port | [PortMetadata](#ports) | ✓ | | 连接桩。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:ports'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### addPorts(...) ```ts addPorts(ports: PortMetadata[], options?: Node.SetOptions) ``` 添加多个连接桩。连接桩被添加到连接桩数组末尾。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | port | [PortMetadata](#ports)[] | ✓ | | 连接桩数组。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:ports'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### insertPort(...) ```ts insertPort(index: number, port: PortMetadata, options?: Node.SetOptions): this ``` 在指定位置添加连接桩。注意 `port` 参数需要带上 `id` 属性。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | index | number | ✓ | | 连接桩位置。 | | port | [PortMetadata](#ports) | ✓ | | 连接桩。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:ports'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### hasPort(...) ```ts hasPort(portId: string): boolean ``` 检查指定的连接桩是否存在。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | ------ | ------ | :--: | ------ | ----------- | | portId | string | ✓ | | 连接桩 ID。 | ```ts if (node.hasPort('port1')) { // do something } ``` #### hasPorts() ```ts hasPorts(): boolean ``` 检查节点是否包含连接桩。 ```ts if (node.hasPorts()) { // do something } ``` #### getPort(...) ```ts getPort(portId: string): PortMetadata ``` 根据连接桩 ID 获取连接桩。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | ------ | ------ | :--: | ------ | ----------- | | portId | string | ✓ | | 连接桩 ID。 | #### getPortAt(...) ```ts getPortAt(index: number): PortMetadata | null ``` 获取指定索引位置的连接桩。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | ----- | ------ | :--: | ------ | ------------ | | index | number | ✓ | | 连接桩索引。 | #### getPorts() ```ts getPorts(): PortMetadata[] ``` 获取所有连接桩。 #### getPortsByGroup(...) ```ts getPortsByGroup(groupName: string): PortMetadata[] ``` 获取群组下的所有连接桩。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --------- | ------ | :--: | ------ | ---------- | | groupName | string | ✓ | | 群组名称。 | #### removePort(...) ```ts /** * 删除指定的连接桩。 */ removePort(port: PortMetadata, options?: Node.SetOptions): this /** * 删除指定 ID 的连接桩。 */ removePort(portId: string, options?: Node.SetOptions): this ``` 删除指定的连接桩。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | port | PortMetadata | ✓ | | 连接桩。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:ports'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | 删除指定 ID 的连接桩。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | portId | string | ✓ | | 连接桩 ID。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:ports'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### removePortAt(...) ```ts removePortAt(index: number, options?: Node.SetOptions): this ``` 删除指定索引位置的连接桩。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | index | number | ✓ | | 连接桩索引。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:ports'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### removePorts(...) ```ts /** * 删除所有连接桩。 */ removePorts(options?: Node.SetOptions): this /** * 删除指定的连接桩。 */ removePorts(ports: (PortMetadata | string)[], options?: Node.SetOptions): this ``` 删除指定的多个连接桩。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | ports | (PortMetadata \| string)[] | | | 要删除的连接桩数组。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:ports'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### getPortIndex(...) ```ts getPortIndex(port: PortMetadata | string): number ``` 获取连接桩的索引位置。 #### getPortProp(...) ```ts getPortProp(portId: string, path?: string | string[]): any ``` 获取连接桩指定路径上的属性值。当路径为空时,返回完整的连接桩。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | portId | string | ✓ | | 连接桩 ID。 | | path | string \| string[] | | | 属性路径。当 `path` 为 `string` 类型时,路径是以 `'/'` 分割的字符串。 当 `path` 为 `string[]` 类型时,路径是连接桩对象路径上的 Key 构成的数组。 | ```ts node.getPortProp('port1') node.getPortProp('port1', 'attrs/circle') node.getPortProp('port1', ['attrs', 'circle']) ``` #### setPortProp(...) ```ts setPortProp( portId: string, path: string | string[], value: any, options?: Node.SetOptions, ): this ``` 根据路径设置连接桩的属性。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | portId | string | ✓ | | 连接桩 ID。 | | path | string \| string[] | ✓ | | 属性路径。当 `path` 为 `string` 类型时,路径是以 `'/'` 分割的字符串。当 `path` 为 `string[]` 类型时,路径是连接桩对象路径上的 Key 构成的数组。 | | value | any | ✓ | | 属性值。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:ports'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ```ts node.setPortProp('port1', 'attrs/circle', { fill: '#ffffff', stroke: '#000000', }) node.setPortProp('port1', ['attrs', 'circle'], { fill: '#ffffff', stroke: '#000000', }) ``` ```ts setPortProp( portId: string, value: DeepPartial, options?: Node.SetOptions, ): this ``` 设置连接桩的属性,提供的属性选项与当前值进行[深度 merge](https://lodash.com/docs/4.17.15#merge)。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | portId | string | ✓ | | 连接桩 ID。 | | value | DeepPartial\ | ✓ | | 连接桩选项。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:ports'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ```ts node.setPortProp('port1', { attrs: { circle: { fill: '#ffffff', stroke: '#000000', }, }, }) ``` #### removePortProp(...) ```ts removePortProp(portId: string, options?: Node.SetOptions): this ``` 删除指定连接桩的选项。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | portId | string | ✓ | | 连接桩 ID。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:ports'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ```ts removePortProp(portId: string, path: string | string[], options?: Node.SetOptions): this ``` 删除指定连接桩和指定路径的选项。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | portId | string | ✓ | | 连接桩 ID。 | | path | string \| string[] | ✓ | | 属性路径。当 `path` 为 `string` 类型时,路径是以 `'/'` 分割的字符串。当 `path` 为 `string[]` 类型时,路径是连接桩对象路径上的 Key 构成的数组。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:ports'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### portProp(...) ```ts portProp(portId: string): PortMetadata portProp(portId: string, path: string | string[]): T portProp( portId: string, path: string | string[], value: any, options?: Node.SetOptions, ): this portProp( portId: string, value: DeepPartial, options?: Node.SetOptions, ): this ``` 该方法是 [`getPortProp`](#getportprop) 和 [`setPortProp`](#setportprop) 两个方法的集合,参数选项和使用方法与这两个方法一致。 #### 模型 - Canonical page: https://x6.antv.antgroup.com/zh/api/mvc/model/ - Markdown source: https://x6.antv.antgroup.com/zh/api/mvc/model.md - Description: 模型 --- title: "模型" description: "模型" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/mvc/model/" version: "3.x" --- ## 配置 ### model 画布对应的模型,默认创建一个新模型。 ## 方法 ### isNode(...) ```ts isNode(cell: Cell): cell is Node ``` 返回指定的 Cell 是否是节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------|------|:----:|--------|------------| | cell | Cell | ✓ | | 指定的 Cell。 | ### isEdge(...) ```ts isEdge(cell: Cell): cell is Edge ``` 返回指定的 Cell 是否是边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------|------|:----:|--------|------------| | cell | Cell | ✓ | | 指定的 Cell。 | ### createNode(...) ```ts createNode(metadata: Node.Metadata): Node ``` 创建节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |----------|---------------|:----:|--------|-----------| | metadata | Node.Metadata | ✓ | | 节点元数据。 | ### addNode(...) ```ts addNode(metadata: Node.Metadata, options?: AddOptions): Node addNode(node: Node, options?: AddOptions): Node ``` 添加节点到画布,返回添加的节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|-----------------------|:----:|---------|-------------------------------------------------------------------| | node | Node.Metadata \| Node | ✓ | | 节点元数据或节点实例。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'node:added'` 和 `'cell:added'` 事件和画布重绘。 | | options.sort | boolean | | `true` | 是否按照 `zIndex` 排序。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### addNodes(...) ```ts addNodes(nodes: (Node.Metadata | Node)[], options?: AddOptions): Graph ``` 添加多个节点到画布,返回该画布。批量添加节点的时候,推荐使用这个方法,相比多次 addNode,它具备更好的性能。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------------------------|:----:|---------|-------------------------------------------------------------------| | nodes | (Node.Metadata \| Node)[] | ✓ | | 节点元数据或节点实例数组。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'node:added'` 和 `'cell:added'` 事件和画布重绘。 | | options.sort | boolean | | `true` | 是否按照 `zIndex` 排序。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### removeNode(...) ```ts removeNode(nodeId: string, options?: RemoveOptions): Node | null removeNode(node: Node, options?: RemoveOptions): Node | null ``` 删除节点,返回删除的节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|----------------|:----:|---------|-----------------------------------------------------------------------| | node | string \| Node | ✓ | | 节点 ID 或节点实例。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'node:removed'` 和 `'cell:removed'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### createEdge(...) ```ts createEdge(metadata: Edge.Metadata): Edge ``` 创建边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |----------|---------------|:----:|--------|-----------| | metadata | Edge.Metadata | ✓ | | 节点元数据。 | ### addEdge(...) ```ts addEdge(metadata: Edge.Metadata, options?: AddOptions): Edge addEdge(edge:Edge, options?: AddOptions): Edge ``` 添加边到画布,返回添加的边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|-----------------------|:----:|---------|-------------------------------------------------------------------| | edge | Edge.Metadata \| Edge | ✓ | | 边元数据或边实例。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'edge:added'` 和 `'cell:added'` 事件和画布重绘。 | | options.sort | boolean | | `true` | 是否按照 `zIndex` 排序。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### addEdges(...) ```ts addEdges(edges: (Edge.Metadata | Edge)[], options?: AddOptions): Graph ``` 添加多条边到画布,返回该画布。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------------------------|:----:|---------|-------------------------------------------------------------------| | edges | (Edge.Metadata \| Edge)[] | ✓ | | 边元数据或边实例数组。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'edge:added'` 和 `'cell:added'` 事件和画布重绘。 | | options.sort | boolean | | `true` | 是否按照 `zIndex` 排序。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### removeEdge(...) ```ts removeEdge(edgeId: string, options?: RemoveOptions): Edge | null removeEdge(edge: Edge, options?: RemoveOptions): Edge | null ``` 删除边,返回删除的边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|----------------|:----:|---------|-----------------------------------------------------------------------| | edge | string \| Edge | ✓ | | 边 ID 或边实例。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'edge:removed'` 和 `'cell:removed'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### addCell(...) ```ts addCell(cell: Cell | Cell[], options?: AddOptions): this ``` 添加节点或边到画布。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|----------------|:----:|---------|----------------------------------------------------------------------------------| | cell | Cell \| Cell[] | ✓ | | 节点实例或边实例,支持传入数组同时添加多个节点或边。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'cell:added'`、`'node:added'` 和 `'edge:added'` 事件和画布重绘。 | | options.sort | boolean | | `true` | 是否按照 `zIndex` 排序。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### removeCell(...) ```ts removeCell(cellId: string, options?: RemoveOptions): Cell | null removeCell(cell: Cell, options?: RemoveOptions): Cell | null ``` 删除节点或边,返回删除的节点或边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|----------------|:----:|---------|----------------------------------------------------------------------------------------| | cell | string \| Cell | ✓ | | 节点/边 ID 或节点/边的实例。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'cell:removed'`、`'node:removed'` 和 `'edge:removed'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### removeCells(...) ```ts removeCells(cells: (Cell | string)[], options?: RemoveOptions): Cell[] ``` 删除多个节点/边,返回删除的节点或边的数组。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|--------------------|:----:|---------|----------------------------------------------------------------------------------------| | cell | (string \| Cell)[] | ✓ | | 节点/边 ID 或节点/边数组。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'cell:removed'`、`'node:removed'` 和 `'edge:removed'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### removeConnectedEdges(...) ```ts removeConnectedEdges(cell: Cell | string, options?: RemoveOptions): Edge[] ``` 删除连接到节点/边的边,返回被删除边的数组。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|----------------|:----:|---------|-----------------------------------------------------------------------| | cell | string \| Cell | ✓ | | 节点/边 ID 或节点/边。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'cell:removed'` 和 `'edge:removed'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### disconnectConnectedEdges(...) ```ts disconnectConnectedEdges(cell: Cell | string, options?: Edge.SetOptions): this ``` 将链接到节点/边的边的起点和终点设置为原点 `{x: 0, y: 0}`,即断开连接。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|----------------|:----:|---------|-----------------------------------------------------------------------------------| | cell | string \| Cell | ✓ | | 节点/边 ID 或节点/边。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'edge:change:source'` 和 `'edge:change:target'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### clearCells(...) ```ts clearCells(options?: SetOptions): this ``` 清空画布。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|:----:|---------|----------------------------------------------------------------------------------------| | options.silent | boolean | | `false` | 为 `true` 时不触发 `'cell:removed'`、`'node:removed'` 和 `'edge:removed'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### resetCells(...) ```ts resetCells(cells: Cell[], options?: SetOptions): this ``` 清空画布并添加用指定的节点/边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|:----:|---------|-------------------------------------------------------------------------------------------------------------------------------------| | cell | Cell[] | ✓ | | 节点/边数组。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'cell:added'`、`'node:added'`、`'edge:added'`、`'cell:removed'`、`'node:removed'` 和 `'edge:removed'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### hasCell(...) ```ts hasCell(cellId: string): boolean hasCell(cell: Cell): boolean ``` 返回画布中是否包含指定的节点/边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------|----------------|:----:|--------|---------------------| | cell | string \| Cell | ✓ | | 节点/边 ID 或节点/边。 | ### getCellById(...) ```ts getCellById(id: string) ``` 根据节点/边的 ID 获取节点/边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------|--------|:----:|--------|-------------| | id | string | ✓ | | 节点/边的 ID。 | ### updateCellId(...) ```ts updateCellId(cell: Cell, newId: string) ``` 更新节点或者边的 ID,会返回新创建的节点/边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |-------|--------|:----:|--------|--------| | cell | Cell | ✓ | | 节点/边。 | | newId | string | ✓ | | 新的 ID。 | ### getCells() ```ts getCells(): Cell[] ``` 返回画布中所有节点和边。 ### getCellCount() ```ts getCellCount(): number ``` 返回画布中所有节点和边的数量。 ### getNodes() ```ts getNodes(): Node[] ``` 返回画布中所有节点。 ### getEdges() ```ts getEdges(): Edge[] ``` 返回画布中所有边。 ### getOutgoingEdges(...) ```ts getOutgoingEdges(cell: Cell | string): Edge[] | null ``` 获取连接到节点/边的输出边,即边的起点为指定节点/边的边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------|----------------|:----:|--------|---------------------| | cell | string \| Cell | ✓ | | 节点/边 ID 或节点/边。 | ### getIncomingEdges(...) ```ts getIncomingEdges(cell: Cell | string): Edge[] | null ``` 获取连接到节点/边的输入边,即边的终点为指定节点/边的边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------|----------------|:----:|--------|---------------------| | cell | string \| Cell | ✓ | | 节点/边 ID 或节点/边。 | ### getConnectedEdges(...) ```ts getConnectedEdges(cell: Cell | string, options?: GetConnectedEdgesOptions): Edge[] ``` 获取与节点/边相连接的边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|----------------|:----:|---------|-------------------------------------------------------------------------------------------------------------------| | cell | string \| Cell | ✓ | | 节点/边 ID 或节点/边。 | | options.incoming | boolean | | - | 是否包含输入边,默认返回所有输入和输出边,`incoming` 和 `outgoing` 两个选项中,当 `incoming` 为 `true` 时只返回输入边。 | | options.outgoing | boolean | | - | 是否包含输出边,默认返回所有输入和输出边,`incoming` 和 `outgoing` 两个选项中,当 `outgoing` 为 `true` 时只返回输出边。 | | options.deep | boolean | | `false` | 是否递归获取所有子节点/边,为 `true` 时将同时返回链接到所有子孙节点/边的边。 | | options.enclosed | boolean | | `false` | 是否包含子孙节点之间相连接的边。 | | options.indirect | boolean | | `false` | 是否包含哪些间接连接的边,即连接到输入或输出边上的边。 | ```ts const edges = graph.getConnectedEdges(node) // 返回输入和输出边 const edges = graph.getConnectedEdges(node, { incoming: true, outgoing: true }) // 返回输入和输出边 const edges = graph.getConnectedEdges(node, { incoming: true }) // 返回输入边 const edges = graph.getConnectedEdges(node, { incoming: true, outgoing: false, }) // 返回输入边 const edges = graph.getConnectedEdges(node, { outgoing: true }) // 返回输出边 const edges = graph.getConnectedEdges(node, { incoming: false, outgoing: true, }) // 返回输出边 const edges = graph.getConnectedEdges(node, { deep: true }) // 返回输入和输出边,包含链接到所有子孙节点/边的输入和输出边 const edges = graph.getConnectedEdges(node, { deep: true, incoming: true }) // 返回输入边,包含链接到所有子孙节点/边的输入边 const edges = graph.getConnectedEdges(node, { deep: true, enclosed: true }) // 返回输入和输出边,同时包含子孙节点/边之间相连的边 const edges = graph.getConnectedEdges(node, { indirect: true }) // 返回输入和输出边,包含间接连接的边 ``` ### getRootNodes() ```ts getRootNodes(): Node[] ``` 获取所有根节点,即没有输入边的节点。 ### isRootNode(...) ```ts isRootNode(cell: Cell | string): boolean ``` 返回指定的节点是否是根节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------|----------------|:----:|--------|---------------------| | cell | string \| Cell | ✓ | | 节点/边 ID 或节点/边。 | ### getLeafNodes() ```ts getLeafNodes(): Node[] ``` 返回所有叶子节点,即没有输出边的节点。 ### isLeafNode(...) ```ts isLeafNode(cell: Cell | string): boolean ``` 返回指定的节点是否是叶子节点 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------|----------------|:----:|--------|---------------------| | cell | string \| Cell | ✓ | | 节点/边 ID 或节点/边。 | ### getNeighbors(...) ```ts getNeighbors(cell: Cell, options?: GetNeighborsOptions): Cell[] ``` 获取邻居节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|---------|:----:|---------|-------------------------------------------------------------------------------------------------------------------------------------| | cell | Cell | ✓ | | 节点/边。 | | options.incoming | boolean | | - | 是否包含输入侧的邻居节点,默认包含输入和输出侧的节点,`incoming` 和 `outgoing` 两个选项中,当 `incoming` 为 `true` 时只返回输入侧的节点。 | | options.outgoing | boolean | | - | 是否包含输出侧的邻居节点,默认包含输入和输出侧的节点,`incoming` 和 `outgoing` 两个选项中,当 `outgoing` 为 `true` 时只返回输出侧的节点。 | | options.deep | boolean | | `false` | 是否递归获取所有子节点/边,为 `true` 时将同时返回所有子孙节点/边的邻居节点。 | | options.indirect | boolean | | `false` | 是否包含哪些间接连接的邻居节点,即中间包含多条边(边与边连接)的邻居。 | ### isNeighbor(...) ```ts isNeighbor(cell1: Cell, cell2: Cell, options?: GetNeighborsOptions): boolean ``` 返回 `cell2` 是否是 `cell1` 的邻居, 其中 `options` 选项与 [`getNeighbors(...)`](#getneighbors) 方法的选项一致。 ### getPredecessors(...) ```ts getPredecessors(cell: Cell, options?: GetPredecessorsOptions): Cell[] ``` 返回节点的前序节点,即从根节点开始连接到指定节点的节点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |----------------------|-------------------------------------------------------|:----:|---------|--------------------------------------------------------------------------| | cell | Cell | ✓ | | 节点/边。 | | options.breadthFirst | boolean | | `false` | 是否使用广度优先搜索算法,默认使用深度优先搜索算法。 | | options.deep | boolean | | `false` | 是否递归获取所有子节点/边,为 `true` 时将同时返回所有子孙节点/边的前序节点。 | | options.distance | number \| number[] \| ((distance: number) => boolean) | | - | 距获取指定距离的前序节点,节点和节点之间相隔的边的数量为 `1` 个距离单位。 | ### isPredecessor(...) ```ts isPredecessor(cell1: Cell, cell2: Cell, options?: GetPredecessorsOptions): boolean ``` 返回 `cell2` 是否是 `cell1` 的前序节点,其中 `options` 选项与 [`getPredecessors(...)`](#getpredecessors) 方法的选项一致。 ### getSuccessors(...) ```ts getSuccessors(cell: Cell, options?: GetPredecessorsOptions): Cell[] ``` 获取所有后续节点,即从指定节点开始连接到叶子节点的节点。其中 `options` 选项与 [`getPredecessors(...)`](#getpredecessors) 方法的选项一致。 ### isSuccessor(...) ```ts isSuccessor(cell1: Cell, cell2: Cell, options?: GetPredecessorsOptions): boolean ``` 返回 `cell2` 是否是 `cell1` 的后续节点,其中 `options` 选项与 [`getPredecessors(...)`](#getpredecessors) 方法的选项一致。 ### getCommonAncestor(...) ```ts getCommonAncestor(...cells: {Cell | Cell[])[]): Cell | null ``` 获取指定节点的共同祖先节点。 ### getSubGraph(...) ```ts getSubGraph(cells: Cell[], options?: GetSubgraphOptions): Cell[] ``` 返回指定节点和边构成的子图。通过遍历指定的 `cells` 数组,当遇到边时,同时包含边的起始和终止节点;当遇到节点时,如果与节点相连的边的两端的节点都在 `cells` 数组中,则同时包含该条边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |--------------|---------|:----:|---------|--------------------------| | cells | Cell[] | ✓ | | 节点/边数组。 | | options.deep | boolean | | `false` | 是否递归获取所有子节点/边。 | ### cloneCells(...) ```ts cloneCells(cells: Cell[]): { [oldCellId: string]: Cell } ``` 克隆,返回旧节点/边 ID 和克隆后节点/边的键值对。 ### cloneSubGraph(...) ```ts cloneSubGraph(cells: Cell[], options?: GetSubgraphOptions): { [oldCellId: string]: Cell } ``` 获取子图并克隆。其中 `options` 选项与 [`getSubGraph(...)`](#getsubgraph) 方法的选项一致。 ### getNodesFromPoint(...) ```ts getNodesFromPoint(x: number, y: number): Node[] getNodesFromPoint(p: Point.PointLike): Node[] ``` 返回指定位置的节点,即返回节点的矩形区域包含了指定点的节点。 ### getNodesInArea(...) ```ts getNodesInArea( x: number, y: number, w: number, h: number, options?: Model.GetCellsInAreaOptions, ): Node[] getNodesInArea( rect: Rectangle.RectangleLike, options?: Model.GetCellsInAreaOptions, ): Node[] ``` 返回指定矩形区域的节点,当 `options.strict` 为 `true` 时,要求节点的矩形区域完全包含指定的矩形,否则只需要相交即可。 ### getNodesUnderNode(...) ```ts getNodesUnderNode( node: Node, options?: { by?: 'bbox' | Rectangle.KeyPoint }, ): Node[] ``` 返回与指定节点位置的节点,通过 `options.by` 选项来指定获取方式,包含: - `null` 或 `bbox`:返回与指定的节点矩形区域相交的节点 - `Rectangle.KeyPoint`:返回包含矩形的某个关键点的节点,其中 `Rectangle.KeyPoint` 的取值为: - `"center"` - `"origin"` - `"corner"` - `"topLeft"` - `"topCenter"` - `"topRight"` - `"bottomLeft"` - `"bottomCenter"` - `"bottomRight"` - `"leftMiddle"` - `"rightMiddle"` ### searchCell(...) ```ts searchCell(cell: Cell, iterator: SearchIterator, options?: SearchOptions): this ``` 从指定的节点/边开始进行遍历。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |----------------------|---------------------------------------|:----:|---------|-------------------------------------------------------------------------------------------------------------------------------------| | cell | Cell | ✓ | | 节点/边。 | | iterator | (cell: Cell, distance: number) => any | ✓ | | 遍历方法。 | | options.breadthFirst | boolean | | `false` | 是否使用广度优先搜索算法,默认使用深度优先搜索算法。 | | options.incoming | boolean | | - | 是否遍历输入侧的邻居节点,默认遍历输入和输出侧的节点,`incoming` 和 `outgoing` 两个选项中,当 `incoming` 为 `true` 时只遍历输入侧的节点。 | | options.outgoing | boolean | | - | 是否遍历输出侧的邻居节点,默认遍历输入和输出侧的节点,`incoming` 和 `outgoing` 两个选项中,当 `outgoing` 为 `true` 时只遍历输出侧的节点。 | | options.deep | boolean | | `false` | 是否递归遍历所有子节点/边,为 `true` 时将同时遍历所有子孙节点/边的邻居节点。 | | options.indirect | boolean | | `false` | 是否遍历哪些间接连接的邻居节点,即中间包含多条边(边与边连接)的邻居。 | ### getShortestPath(...) ```ts getShortestPath( source: Cell | string, target: Cell | string, options?: GetShortestPathOptions, ): string[] ``` 获取节点之间的最短路径,返回最短路径上的节点 ID。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|----------------------------------|:----:|---------------|--------------------------------------------------------------| | source | Cell \| string | ✓ | | 起始节点/边。 | | source | Cell \| string | ✓ | | 终止节点/边。 | | options.directed | boolean | | `false` | 是否考虑方向性,为 `true` 时需要路径沿开始节点到终止节点的方向。 | | options.weight | (u: string, v: string) => number | | `(u, v) => 1` | 距离权重算法,`u` 和 `v` 为相邻的两个节点,默认距离为 `1`。 | ### getAllCellsBBox(...) ```ts getAllCellsBBox(): Rectangle | null ``` 返回画布上所有节点和边的矩形区域。 ### getCellsBBox(...) ```ts getCellsBBox(cells: Cell[], options?: Cell.GetCellsBBoxOptions): Rectangle | null ``` 返回指定节点和边构成的矩形区域。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |--------------|---------|:----:|---------|-------------------------| | cells | Cell[] | ✓ | | 节点和边数组。 | | options.deep | boolean | | `false` | 是否包含所有子孙节点和边。 | ### toJSON(...) ```ts toJSON(options?: ToJSONOptions): object ``` 导出图中的节点和边,返回一个具有 `{ cells: [] }` 结构的对象,其中 `cells` 数组按渲染顺序保存节点和边。 | 名称 | 类型 | 必选 | 默认值 | 描述 | |--------------|------|:----:|---------|-------------------------------------------------------------------------------------------------| | options.deep | diff | | `false` | 是否导出节点和边的差异数据(与节点和边的[默认配置](/zh/api/model/cell#选项默认值)不同的部分)。 | ### parseJSON(...) 将指定的数据转换为节点和边。 支持节点/边元数据数组,按照数组顺序返回创建的节点和边。 ```ts parseJSON(cells: (Node.Metadata | Edge.Metadata)[]): (Node | Edge)[] ``` 或者提供一个包含 `cells`、`nodes`、`edges` 的对象,按照 `[...cells, ...nodes, ...edges]` 顺序创建节点和边并返回。 ```ts parseJSON({ cells?: (Node.Metadata | Edge.Metadata)[], nodes?: Node.Metadata[], edges?: Edge.Metadata[], }): (Node | Edge)[] ``` ### fromJSON(...) 按照指定的 JSON 数据渲染节点和边。 支持节点/边元数据数组,按照数组顺序渲染节点和边。 ```ts fromJSON(data: (Node.Metadata | Edge.Metadata)[], options?: FromJSONOptions): this ``` 或者提供一个包含 `cells`、`nodes`、`edges` 的对象,按照 `[...cells, ...nodes, ...edges]` 顺序渲染。 ```ts fromJSON( data: { cells?: (Node.Metadata | Edge.Metadata)[], nodes?: Node.Metadata[], edges?: Edge.Metadata[], }, options?: FromJSONOptions, ): this ``` 当 `options.silent` 为 `true` 时,不触发 `cell:added`、`node:added` 和 `edge:added` 事件和画布重绘,当 `options.diff` 为 `true` 时,会通过 `id` 进行增量更新。 #### 网格 - Canonical page: https://x6.antv.antgroup.com/zh/api/graph/grid/ - Markdown source: https://x6.antv.antgroup.com/zh/api/graph/grid.md - Description: 网格 --- title: "网格" description: "网格" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/graph/grid/" version: "3.x" --- 网格是渲染/移动节点的最小单位。网格默认大小为 `10px`,渲染节点时表示以 `10` 为最小单位对齐到网格,如位置为 `{ x: 24, y: 38 }`的节点渲染到画布后的实际位置为 `{ x: 20, y: 40 }`, 移动节点时表示每次移动最小距离为 `10px`。 ## 演示 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Settings } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.graph = new Graph({ container: this.container, grid: { visible: true, }, background: { color: '#F2F7FA', }, mousewheel: { enabled: true, minScale: 0.2, maxScale: 4, }, }) const source = this.graph.addNode({ x: 40, y: 40, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ x: 200, y: 180, width: 100, height: 40, label: 'Grid', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph.centerContent() } onGridChanged = (options: any) => { this.graph.drawGrid(options) } onGridSizeChanged = (size: number) => { this.graph.setGridSize(size) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 配置 ### size 创建画布时,通过下面配置来设置网格大小。 ```ts const graph = new Graph({ grid: 10, }) // 等同于 const graph = new Graph({ grid: { size: 10, }, }) ``` ### type 网格默认不可见,创建画布时,通过下面配置来启用网格绘制。 ```ts const graph = new Graph({ grid: true, // 网格大小 10px,并绘制网格 }) // 等同于 const graph = new Graph({ grid: { size: 10, // 网格大小 10px visible: true, // 绘制网格,默认绘制 dot 类型网格 }, }) ``` 同时,我们内置了以下四种网格类型,通过 `type` 选项来指定网格类型,默认值为 `dot`,并支持通过 `args` 选项来配置网格样式。 #### dot (默认值) 点状网格。 ```ts new Graph({ container: this.container, grid: { visible: true, type: 'dot', args: { color: '#a0a0a0', // 网点颜色 thickness: 1, // 网点大小 }, }, }) ``` #### fixedDot 固定网点大小的点状网格。当画布的缩放比例小于 `1` 时,网点大小随着画布缩放比例缩放,当画布缩放比例大于 `1` 时,网点大小为给定的 `thickness` 的值。 ```ts new Graph({ container: this.container, grid: { visible: true, size: 10, type: 'fixedDot', args: { color: '#a0a0a0', // 网点颜色 thickness: 2, // 网点大小 }, }, }) graph.scale(10, 10) ``` #### mesh 网状网格。 ```ts new Graph({ container: this.container, grid: { visible: true, type: 'mesh', args: { color: '#ddd', // 网格线颜色 thickness: 1, // 网格线宽度 }, }, }) ``` #### doubleMesh 双线网状网格。 ```ts const graph = new Graph({ grid: { size: 10, visible: true, type: 'doubleMesh', args: [ { color: '#eee', // 主网格线颜色 thickness: 1, // 主网格线宽度 }, { color: '#ddd', // 次网格线颜色 thickness: 1, // 次网格线宽度 factor: 4, // 主次网格线间隔 }, ], }, }) ``` ## 方法 ### getGridSize() ```ts getGridSize(): number ``` 获取网格大小。 ### setGridSize() ```ts setGridSize(gridSize: number): this ``` 设置网格大小。 ### showGrid() ```ts showGrid(): this ``` 显示网格。 ### hideGrid() ```ts hideGrid(): this ``` 隐藏网格。 ### clearGrid() ```ts clearGrid(): this ``` 清除网格。 ### drawGrid(...) ```ts drawGrid(options?: DrawGridOptions): this ``` 重绘网格。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | type | string | | `dot` | 网格类型。详情请[参考这里](/zh/api/graph/grid)。 | | args | object | | - | 与网格类型对应的网格参数。 | ## 自定义网格 这里以注册一个红色点状网格为例: ```ts Graph.registerGrid('red-dot', { color: 'red', thickness: 1, markup: 'rect', update(elem, options) { const width = options.thickness * options.sx const height = options.thickness * options.sy Dom.attr(elem, { width, height, rx: width, ry: height, fill: options.color, }) }, }) const graph = new Graph({ grid: { type: 'red-dot', }, }) ``` ### docs/demos/api/grid/playground/index.less ```less .grid-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/grid/playground/settings.tsx ```tsx import React from 'react' import { Input, Select, Slider, Card, Row, Col } from 'antd' export interface Props { onGridSizeChange: (size: number) => void onChange: (res: any) => void } export interface State { type: string size: number color: string thickness: number colorSecond: string thicknessSecond: number factor: number } export class Settings extends React.Component { state: State = { type: 'dot', size: 10, color: '#aaaaaa', thickness: 1, colorSecond: '#888888', thicknessSecond: 3, factor: 4, } notifyChange() { if (this.state.type === 'doubleMesh') { this.props.onChange({ type: this.state.type, args: [ { color: this.state.color, thickness: this.state.thickness, }, { color: this.state.colorSecond, thickness: this.state.thicknessSecond, factor: this.state.factor, }, ], }) } else { this.props.onChange({ type: this.state.type, args: [ { color: this.state.color, thickness: this.state.thickness, }, ], }) } } onTypeChanged = (type: string) => { this.setState({ type }, () => { this.notifyChange() }) } onSizeChanged = (size: number) => { this.setState({ size }, () => { this.props.onGridSizeChange(this.state.size) }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onSecondaryColorChanged = (e: any) => { this.setState({ colorSecond: e.target.value }, () => { this.notifyChange() }) } onThicknessChanged = (thickness: number) => { this.setState({ thickness }, () => { this.notifyChange() }) } onSecondaryThicknessChanged = (thicknessSecond: number) => { this.setState({ thicknessSecond }, () => { this.notifyChange() }) } onFactorChanged = (factor: number) => { this.setState({ factor }, () => { this.notifyChange() }) } render() { return ( Grid Type Grid Size
{this.state.size}
{this.state.type === 'doubleMesh' ? ( Primary Color Primary Thickness
{this.state.thickness.toFixed(1)}
Secondary Color Secondary Thickness
{this.state.thicknessSecond.toFixed(1)}
Scale Factor
{this.state.factor}
) : ( Grid Color Thickness
{this.state.thickness.toFixed(1)}
)}
) } } ``` #### 边 - Canonical page: https://x6.antv.antgroup.com/zh/api/model/edge/ - Markdown source: https://x6.antv.antgroup.com/zh/api/model/edge.md - Description: 边 --- title: "边" description: "边" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/model/edge/" version: "3.x" --- Edge 是边的基类,继承自 [Cell](/zh/api/model/cell),并定义了边的通用属性和方法。 ## 属性 除了从 Cell 继承[属性](/zh/api/model/cell#属性)外,还支持以下属性。 | 选项 | 类型 | 默认值 | 必选 | 描述 | | --- | --- | --- | :-: | --- | | source | TerminalData | | | 起点或起始节点、连接桩信息。 | | target | TerminalData | | | 终点或终止节点、连接桩信息。 | | vertices | Point.PointLike[] | | | 路径点。 | | router | RouterData | | | 路由。 | | connector | ConnectorData | | | 连接器。 | | labels | Label[] \| string[] | | | 标签。 | | defaultLabel | Label | | | 默认标签。 | ### source 和 target 设置边的起点/终点或起始节点/终止节点,可以分为一下几种情况: - **连接到画布上的点** ```ts const edge = graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 180, y: 80 }, }) ``` - **连接到节点/边** ```ts const edge = graph.addEdge({ source: { cell: 'source-cell-id' }, target: { cell: 'target-cell-id' }, }) ``` - **连接到节点上的连接桩** ```ts const edge = graph.addEdge({ source: { cell: 'source-cell-id', port: 'port-id' }, target: { cell: 'target-cell-id', port: 'port-id' }, }) ``` - **连接到节点上的某个元素** ```ts const edge = graph.addEdge({ source: { cell: 'source-cell-id', selector: 'some-selector' }, target: { cell: 'target-cell-id', selector: 'some-selector' }, }) ``` 另外,边的[锚点 anchor](/zh/api/registry/node-anchor) 和[连接点 ConnectionPoint](/zh/api/registry/connection-point) 选项共同确定了边的起点和终点。 - 起点:从第一个路径点或目标节点的中心(没有路径点时)画一条参考线到源节点的锚点,然后根据 connectionPoint 指定的交点计算方法,计算参考线与图形的交点,该交点就是边的起点。 - 终点:从最后一个路径点或源节点的中心(没有路径点时)画一条参考线到目标节点的锚点,然后根据 connectionPoint 指定的交点计算方法,计算参考线与图形的交点,该交点就是边的终点。 创建边时可以分别为 `source` 和 `target` 指定锚点和连接点。 - **指定锚点** ```ts const edge = graph.addEdge({ source: { cell: 'source-id', anchor: { name: 'midSide', args: { dx: 10, }, }, }, target: { cell: 'target-id', anchor: 'orth', // 没有参数时可以简化写法 }, }) ``` - **指定连接点** ```ts const edge = graph.addEdge({ source: { cell: 'source-id', connectionPoint: { name: 'boundary', args: { sticky: true, }, }, }, target: { cell: 'target-id', connectionPoint: 'boundary', // 没有参数时可以简化写法 }, }) ``` ### vertices 路径点 `vertices` 是一个点的数组。边从起点开始,按顺序经过路径点,最后到达终点。 ```ts const edge = graph.addEdge({ source, target, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], }) ``` ### router 路由 `router` 将对 `vertices` 进一步处理,并在必要时添加额外的点,然后返回处理后的点(不包含边的起点和终点)。例如,经过 [`orth`](/zh/api/registry/router#orth) 路由处理后,边的每一条链接线段都是水平或垂直的。 我们默认提供了以下几种路由。 | 路由名称 | 说明 | | --- | --- | | normal | [默认路由](/zh/api/registry/router#normal),原样返回路径点。 | | orth | [正交路由](/zh/api/registry/router#orth),由水平或垂直的正交线段组成。 | | oneSide | [受限正交路由](/zh/api/registry/router#oneside),由受限的三段水平或垂直的正交线段组成。 | | manhattan | [智能正交路由](/zh/api/registry/router#manhattan),由水平或垂直的正交线段组成,并自动避开路径上的其他节点(障碍)。 | | metro | [智能地铁线路由](/zh/api/registry/router#metro),由水平或垂直的正交线段和斜角线段组成,类似地铁轨道图,并自动避开路径上的其他节点(障碍)。 | | er | [实体关系路由](/zh/api/registry/router#er),由 `Z` 字形的斜角线段组成。 | 可以这样指定路由名称 `name` 和路由参数 `args`: ```ts const edge = graph.addEdge({ source, target, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], router: { name: 'orth', args: { padding: 10, }, }, }) ``` 当没有路由参数 `args` 时,也可以简化为: ```ts const edge = graph.addEdge({ source, target, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], router: 'orth', }) ``` 除了上面几种内置路由,我们还可以创建自定义路由,并注册使用,更多细节请参考[自定义路由](/zh/api/registry/router#registry)教程。 ### connector 连接器将起点、路由返回的点、终点加工为 `` 元素的 [`d`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) 属性,决定了边渲染到画布后的样式。 我们默认提供了以下几种连接器。 | 连接器名称 | 说明 | | --- | --- | | normal | [简单连接器](/zh/api/registry/connector#normal),用直线连接起点、路由点和终点。 | | smooth | [平滑连接器](/zh/api/registry/connector#smooth),用三次贝塞尔曲线线连接起点、路由点和终点。 | | rounded | [圆角连接器](/zh/api/registry/connector#rounded),用直线连接起点、路由点和终点,并在线段连接处用圆弧链接(倒圆角)。 | | jumpover | [跳线连接器](/zh/api/registry/connector#jumpover),用直线连接起点、路由点和终点,并在边与边的交叉处用跳线符号链接。 | 可以这样指定连接器名称 `name` 和路由参数 `args`: ```ts const edge = graph.addEdge({ source, target, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], connector: { name: 'rounded', args: { radius: 20, }, }, }) ``` 当没有连接器参数 `args` 时,也可以简化为: ```ts const edge = graph.addEdge({ source, target, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], connector: 'rounded', }) ``` 除了上面几种内置连接器,我们还可以创建自定义连接器,并注册使用,更多细节请参考[自定义连接器](/zh/api/registry/connector#registry)教程。 ### labels 和 defaultLabel 由于标签配置非常灵活,所以我们提供了单独的教程来介绍如何使用标签,详情请参考[使用标签](/zh/api/model/labels)教程。 ## 方法 ### 通用 #### isEdge() ```ts isEdge(): true ``` 判断是不是边,该方法始终返回 `true`。 #### getBBox() ```ts getBBox(): Rectangle ``` 返回边的包围盒。 #### getPolyline() ```ts getPolyline(): Polyline ``` 返回由端点和路径点组成的线段。 #### hasLoop(...) ```ts hasLoop(options: { deep?: boolean }): boolean ``` 是否包含循环链接。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | ------------ | ------- | :--: | ------- | ------------------ | | options.deep | boolean | | `false` | 是否进行嵌套检查。 | - 当 `options.deep` 为 `false` 时,表示仅当起始节点和终止节点为同一节点时才是循环连接。 - 当 `options.deep` 为 `true` 时,表示当起始节点和终止节点为同一节点或起始节点与终止节点有父子嵌套关系时都是循环连接。 ### 链接 Terminal #### getSource() ```ts getSource(): Edge.TerminalData ``` 获取边的起始节点/起始点信息。 #### getSourceCell() ```ts getSourceCell(): Cell | null ``` 获取边的起始节点/边,没有连接到节点/边时返回 `null`。 #### getSourceNode() ```ts getSourceNode(): Node | null ``` 获取边的起始节点,没有连接到节点时返回 `null`。 #### getSourceCellId() ```ts getSourceCellId(): string | null ``` 获取边的起始节点/边的 ID,没有连接到节点/边时返回 `null`。 #### getSourcePortId() ```ts getSourcePortId(): string | null ``` 获取边的起始连接桩 ID,没有连接到连接桩时返回 `null`。 #### getSourcePoint() ```ts getSourcePoint(): Point.PointLike | null ``` 获取边链接到画布的起始点,当边连接到节点/边时返回 `null`。 #### setSource(...) ```ts /** * 链接到节点。 */ setSource( node: Node, args?: Edge.SetCellTerminalArgs, options?: Edge.SetOptions, ): this /** * 链接到边。 */ setSource( edge: Edge, args?: Edge.SetEdgeTerminalArgs, options?: Edge.SetOptions, ): this /** * 链接到画布上的点。 */ setSource( point: Point | Point.PointLike, args?: Edge.SetTerminalCommonArgs, options?: Edge.SetOptions, ): this /** * 设置边的起点或起始节点/边。 */ setSource(args: Edge.TerminalData, options?: Edge.SetOptions): this ``` #### getTarget() ```ts getTarget(): Edge.TerminalData ``` 获取边的终止节点/终止点信息。 #### getTargetCell() ```ts getTargetCell(): Cell | null ``` 获取边的终止节点/边,没有连接到节点/边时返回 `null`。 #### getTargetNode() ```ts getTargetNode(): Node | null ``` 获取边的终止节点,没有连接到节点时返回 `null`。 #### getTargetCellId() ```ts getTargetCellId(): string | null ``` 获取边的终止节点/边的 ID,没有连接到节点/边时返回 `null`。 #### getTargetPortId() ```ts getTargetPortId(): string | null ``` 获取边的终止连接桩 ID,没有连接到连接桩时返回 `null`。 #### getTargetPoint() ```ts getTargetPoint(): Point.PointLike | null ``` 获取边链接到画布的终止点,当边连接到节点/边时返回 `null`。 #### setTarget() ```ts /** * 链接到节点。 */ setTarget( edge: Node, args?: Edge.SetCellTerminalArgs, options?: Edge.SetOptions, ): this /** * 链接到边。 */ setTarget( edge: Edge, args?: Edge.SetEdgeTerminalArgs, options?: Edge.SetOptions, ): this /** * 链接到画布上的点。 */ setTarget( point: Point | Point.PointLike, args?: Edge.SetTerminalCommonArgs, options?: Edge.SetOptions, ): this /** * 设置边的终点或终止节点/边。 */ setTarget(args: Edge.TerminalData, options?: Edge.SetOptions): this ``` #### disconnect(...) ```ts disconnect(options?: Edge.SetOptions) ``` 删除边的链接信息,即将边的起点和终点都设置为画布的原点 `{ x:0, y:0 }`。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:source'` 和 `'change:target'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### 路径点 Vertice #### getVertices() ```ts getVertices(): Point.PointLike[] ``` 获取路径点,当没有路径点时返回空数组。 #### setVertices(...) ```ts setVertices( vertices: Point.PointLike | Point.PointLike[], options?: Edge.SetOptions, ): this ``` 设置路径点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | vertices | Point.PointLike \| Point.PointLike[] | ✓ | | 路径点。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:vertices'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### insertVertex(...) ```ts insertVertex( vertice: Point.PointLike, index?: number, options?: Edge.SetOptions, ): this ``` 在指定位置插入一个路径点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | vertice | Point.PointLike | ✓ | | 路径点。 | | index | number | | | 插入位置,默认插入到路径点数组的末尾。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:vertices'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### appendVertex(...) ```ts appendVertex(vertex: Point.PointLike, options?: Edge.SetOptions): this ``` 在路径点数组的末尾插入一个路径点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | vertex | Point.PointLike | ✓ | | 路径点。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:vertices'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### getVertexAt(...) ```ts getVertexAt(index: number): Point.PointLike | null ``` 获取指定索引位置的路径点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | ----- | ------ | :--: | ------ | ---------- | | index | number | ✓ | | 索引位置。 | #### setVertexAt(...) ```ts setVertexAt( index: number, vertice: Point.PointLike, options?: Edge.SetOptions, ): this ``` 设置指定索引位置的路径点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | index | number | ✓ | | 索引位置。 | | vertice | Point.PointLike | ✓ | | 路径点。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:vertices'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### removeVertexAt(...) ```ts removeVertexAt(index: number, options?: Edge.SetOptions): this ``` 删除指定索引位置的路径点。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | index | number | ✓ | | 索引位置。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:vertices'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### 路由 Router #### getRouter() ```ts getRouter(): Edge.RouterData ``` 获取路由。 #### setRouter(...) ```ts setRouter(name: string, args?: KeyValue, options?: Edge.SetOptions): this setRouter(router: Edge.RouterData, options?: Edge.SetOptions): this ``` 设置路由。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | name | string | ✓ | | 路由名称。 | | args | KeyValue | | | 路由参数。 | | router | Edge.RouterData | ✓ | | 路由。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:router'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### removeRouter(...) ```ts removeRouter(options?: Edge.SetOptions): this ``` 删除路由。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:router'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### 连接器 Connector #### getConnector() ```ts getConnector(): Edge.ConnectorData ``` 获取连接器。 #### setConnector(...) ```ts setConnector(name: string, args?: KeyValue, options?: Edge.SetOptions): this setConnector(connector: Edge.ConnectorData, options?: Edge.SetOptions): this ``` 设置连接器。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | name | string | ✓ | | 连接器名称。 | | args | KeyValue | | | 连接器参数。 | | connector | Edge.ConnectorData | ✓ | | 连接器。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:connector'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### removeConnector(...) ```ts removeConnector(options?: Edge.SetOptions): this ``` 删除连接器。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:connector'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | ### 标签 Label #### getDefaultLabel() ```ts getDefaultLabel(): Edge.Label ``` 获取默认标签。 #### getLabels() ```ts getLabels(): Edge.Label[] ``` 获取所有标签。 #### setLabels(...) ```ts setLabels( labels: Edge.Label | Edge.Label[] | string | string[], options?: Edge.SetOptions, ): this ``` 设置标签。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | labels | Edge.Label \| Edge.Label[] \| string \| string[] | ✓ | | 标签或标签数组。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:labels'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### insertLabel(...) ```ts insertLabel( label: Edge.Label | string, index?: number, options?: Edge.SetOptions, ): this ``` 在指定位置插入标签。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | label | Edge.Label \| string | ✓ | | 标签。 | | index | number | | | 插入的位置,缺省时插入到标签数组的末尾。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:labels'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### appendLabel(...) ```ts appendLabel(label: Edge.Label | string, options?: Edge.SetOptions): this ``` 在标签数组末尾插入标签。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | label | Edge.Label \| string | ✓ | | 标签。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:labels'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### getLabelAt(...) ```ts getLabelAt(index: number): Edge.Label | null ``` 获取指定位置的标签。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | ----- | ------ | :--: | ------ | ---------- | | index | number | ✓ | | 索引位置。 | #### setLabelAt(...) ```ts setLabelAt( index: number, label: Edge.Label | string, options?: Edge.SetOptions, ): this ``` 设置指定位置的标签。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | index | number | ✓ | | 索引位置。 | | label | Edge.Label \| string | ✓ | | 标签。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:labels'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### removeLabelAt(...) ```ts removeLabelAt(index: number, options?: Edge.SetOptions): this ``` 删除指定位置的标签。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | index | number | ✓ | | 索引位置。 | | options.silent | boolean | | `false` | 为 `true` 时不触发 `'change:labels'` 事件和画布重绘。 | | options...others | object | | | 其他自定义键值对,可以在事件回调中使用。 | #### 背景 - Canonical page: https://x6.antv.antgroup.com/zh/api/graph/background/ - Markdown source: https://x6.antv.antgroup.com/zh/api/graph/background.md - Description: 背景 --- title: "背景" description: "背景" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/graph/background/" version: "3.x" --- 背景用于为画布指定背景颜色或背景图片,支持[水印背景](#repeat),背景层在 DOM 层级上位于画布的最底层。 ## 演示 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Settings } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA' }, }) const source = this.graph.addNode({ x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ x: 160, y: 180, width: 100, height: 40, label: 'Grid', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph.centerContent() } onBackgroundChanged = (options: Graph.BackgroundManager.Options) => { this.graph.drawBackground(options) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 配置 ### color 背景颜色,支持所有 [CSS background-color](https://developer.mozilla.org/en-US/docs/Web/CSS/background-color) 属性的取值,如: - `red` - `#f5f5f5` - `rgba(255, 255, 128, 0.5)` - `hsla(50, 33%, 25%, 0.75)` - `radial-gradient(ellipse at center, red, green)` ### image 背景图片的 URL 地址。默认值为 `undefined`,表示没有背景图片。 ### position 背景图片位置,支持所有 [CSS background-position](https://developer.mozilla.org/en-US/docs/Web/CSS/background-position) 属性的取值,默认为 `'center'`。 ### size 背景图片大小,支持所有 [CSS background-size](https://developer.mozilla.org/en-US/docs/Web/CSS/background-size) 属性的取值,默认为 `'auto auto'`。 ### repeat 背景图片重复方式,支持所有 [CSS background-repeat](https://developer.mozilla.org/en-US/docs/Web/CSS/background-repeat) 属性的取值,默认为 `'no-repeat'`。 另外,还支持以下几个预定义值: - `watermark`: 水印效果。 - `flip-x`: 水平翻转背景图片。 - `flip-y`: 垂直翻转背景图片。 - `flip-xy`: 水平和垂直翻转背景图片。 ### opacity 背景透明度,取值范围 `[0, 1]`,默认值为 `1`。 ### quality 背景图片质量,取值范围 `[0, 1]`,默认值为 `1`。 ### angle 水印旋转角度,仅当 [repeat](#repeat) 为 `'watermark'` 时有效,默认值为 `20`。 ## 方法 ### drawBackground(...) ```ts drawBackground(options?: Options): this ``` 重绘背景。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | ---------------- | ------ | :--: | ------ | ------------------ | | options.color | string | | - | 背景颜色。 | | options.image | string | | - | 背景图片地址。 | | options.position | string | | - | 背景图片位置。 | | options.size | string | | - | 背景图片大小。 | | options.repeat | string | | - | 背景图片重复方式。 | | options.opacity | string | | - | 背景图片透明度。 | ### updateBackground() ```ts updateBackground(): this ``` 更新背景。 ### clearBackground() ```ts clearBackground(): this ``` 清除背景。 ## 自定义图片重复方式 除了上面 [repeat](#repeat) 支持的几个预定义值外,还可以自定义图片重复方式。 ```ts function watermark(img, options) { const width = img.width const height = img.height const canvas = document.createElement('canvas') canvas.width = width * 3 canvas.height = height * 3 const ctx = canvas.getContext('2d')! const angle = options.angle != null ? -options.angle : -20 const radians = Angle.toRad(angle) const stepX = canvas.width / 4 const stepY = canvas.height / 4 for (let i = 0; i < 4; i += 1) { for (let j = 0; j < 4; j += 1) { if ((i + j) % 2 > 0) { ctx.setTransform(1, 0, 0, 1, (2 * i - 1) * stepX, (2 * j - 1) * stepY) ctx.rotate(radians) ctx.drawImage(img, -width / 2, -height / 2, width, height) } } } return canvas } Graph.registerBackground('watermark', watermark) ``` ### docs/demos/api/background/playground/index.less ```less .background-app { display: flex; height: 388px; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; user-select: none; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/background/playground/settings.tsx ```tsx import React from 'react' import { Graph } from '@antv/x6' import { Input, Checkbox, Select, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (res: Graph.BackgroundManager.Options) => void } export interface State { color: string showImage: boolean repeat: string position: string size: string opacity: number angle?: number } const bgImageDataURL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAYAAADDhn8LAAAKQWlDQ1BJQ0MgUHJvZmlsZQAASA2dlndUU9kWh8+9N73QEiIgJfQaegkg0jtIFQRRiUmAUAKGhCZ2RAVGFBEpVmRUwAFHhyJjRRQLg4Ji1wnyEFDGwVFEReXdjGsJ7601896a/cdZ39nnt9fZZ+9917oAUPyCBMJ0WAGANKFYFO7rwVwSE8vE9wIYEAEOWAHA4WZmBEf4RALU/L09mZmoSMaz9u4ugGS72yy/UCZz1v9/kSI3QyQGAApF1TY8fiYX5QKUU7PFGTL/BMr0lSkyhjEyFqEJoqwi48SvbPan5iu7yZiXJuShGlnOGbw0noy7UN6aJeGjjAShXJgl4GejfAdlvVRJmgDl9yjT0/icTAAwFJlfzOcmoWyJMkUUGe6J8gIACJTEObxyDov5OWieAHimZ+SKBIlJYqYR15hp5ejIZvrxs1P5YjErlMNN4Yh4TM/0tAyOMBeAr2+WRQElWW2ZaJHtrRzt7VnW5mj5v9nfHn5T/T3IevtV8Sbsz55BjJ5Z32zsrC+9FgD2JFqbHbO+lVUAtG0GQOXhrE/vIADyBQC03pzzHoZsXpLE4gwnC4vs7GxzAZ9rLivoN/ufgm/Kv4Y595nL7vtWO6YXP4EjSRUzZUXlpqemS0TMzAwOl89k/fcQ/+PAOWnNycMsnJ/AF/GF6FVR6JQJhIlou4U8gViQLmQKhH/V4X8YNicHGX6daxRodV8AfYU5ULhJB8hvPQBDIwMkbj96An3rWxAxCsi+vGitka9zjzJ6/uf6Hwtcim7hTEEiU+b2DI9kciWiLBmj34RswQISkAd0oAo0gS4wAixgDRyAM3AD3iAAhIBIEAOWAy5IAmlABLJBPtgACkEx2AF2g2pwANSBetAEToI2cAZcBFfADXALDIBHQAqGwUswAd6BaQiC8BAVokGqkBakD5lC1hAbWgh5Q0FQOBQDxUOJkBCSQPnQJqgYKoOqoUNQPfQjdBq6CF2D+qAH0CA0Bv0BfYQRmALTYQ3YALaA2bA7HAhHwsvgRHgVnAcXwNvhSrgWPg63whfhG/AALIVfwpMIQMgIA9FGWAgb8URCkFgkAREha5EipAKpRZqQDqQbuY1IkXHkAwaHoWGYGBbGGeOHWYzhYlZh1mJKMNWYY5hWTBfmNmYQM4H5gqVi1bGmWCesP3YJNhGbjS3EVmCPYFuwl7ED2GHsOxwOx8AZ4hxwfrgYXDJuNa4Etw/XjLuA68MN4SbxeLwq3hTvgg/Bc/BifCG+Cn8cfx7fjx/GvyeQCVoEa4IPIZYgJGwkVBAaCOcI/YQRwjRRgahPdCKGEHnEXGIpsY7YQbxJHCZOkxRJhiQXUiQpmbSBVElqIl0mPSa9IZPJOmRHchhZQF5PriSfIF8lD5I/UJQoJhRPShxFQtlOOUq5QHlAeUOlUg2obtRYqpi6nVpPvUR9Sn0vR5Mzl/OX48mtk6uRa5Xrl3slT5TXl3eXXy6fJ18hf0r+pvy4AlHBQMFTgaOwVqFG4bTCPYVJRZqilWKIYppiiWKD4jXFUSW8koGStxJPqUDpsNIlpSEaQtOledK4tE20Otpl2jAdRzek+9OT6cX0H+i99AllJWVb5SjlHOUa5bPKUgbCMGD4M1IZpYyTjLuMj/M05rnP48/bNq9pXv+8KZX5Km4qfJUilWaVAZWPqkxVb9UU1Z2qbapP1DBqJmphatlq+9Uuq43Pp893ns+dXzT/5PyH6rC6iXq4+mr1w+o96pMamhq+GhkaVRqXNMY1GZpumsma5ZrnNMe0aFoLtQRa5VrntV4wlZnuzFRmJbOLOaGtru2nLdE+pN2rPa1jqLNYZ6NOs84TXZIuWzdBt1y3U3dCT0svWC9fr1HvoT5Rn62fpL9Hv1t/ysDQINpgi0GbwaihiqG/YZ5ho+FjI6qRq9Eqo1qjO8Y4Y7ZxivE+41smsImdSZJJjclNU9jU3lRgus+0zwxr5mgmNKs1u8eisNxZWaxG1qA5wzzIfKN5m/krCz2LWIudFt0WXyztLFMt6ywfWSlZBVhttOqw+sPaxJprXWN9x4Zq42Ozzqbd5rWtqS3fdr/tfTuaXbDdFrtOu8/2DvYi+yb7MQc9h3iHvQ732HR2KLuEfdUR6+jhuM7xjOMHJ3snsdNJp9+dWc4pzg3OowsMF/AX1C0YctFx4bgccpEuZC6MX3hwodRV25XjWuv6zE3Xjed2xG3E3dg92f24+ysPSw+RR4vHlKeT5xrPC16Il69XkVevt5L3Yu9q76c+Oj6JPo0+E752vqt9L/hh/QL9dvrd89fw5/rX+08EOASsCegKpARGBFYHPgsyCRIFdQTDwQHBu4IfL9JfJFzUFgJC/EN2hTwJNQxdFfpzGC4sNKwm7Hm4VXh+eHcELWJFREPEu0iPyNLIR4uNFksWd0bJR8VF1UdNRXtFl0VLl1gsWbPkRoxajCCmPRYfGxV7JHZyqffS3UuH4+ziCuPuLjNclrPs2nK15anLz66QX8FZcSoeGx8d3xD/iRPCqeVMrvRfuXflBNeTu4f7kufGK+eN8V34ZfyRBJeEsoTRRJfEXYljSa5JFUnjAk9BteB1sl/ygeSplJCUoykzqdGpzWmEtPi000IlYYqwK10zPSe9L8M0ozBDuspp1e5VE6JA0ZFMKHNZZruYjv5M9UiMJJslg1kLs2qy3mdHZZ/KUcwR5vTkmuRuyx3J88n7fjVmNXd1Z752/ob8wTXuaw6thdauXNu5Tnddwbrh9b7rj20gbUjZ8MtGy41lG99uit7UUaBRsL5gaLPv5sZCuUJR4b0tzlsObMVsFWzt3WazrWrblyJe0fViy+KK4k8l3JLr31l9V/ndzPaE7b2l9qX7d+B2CHfc3em681iZYlle2dCu4F2t5czyovK3u1fsvlZhW3FgD2mPZI+0MqiyvUqvakfVp+qk6oEaj5rmvep7t+2d2sfb17/fbX/TAY0DxQc+HhQcvH/I91BrrUFtxWHc4azDz+ui6rq/Z39ff0TtSPGRz0eFR6XHwo911TvU1zeoN5Q2wo2SxrHjccdv/eD1Q3sTq+lQM6O5+AQ4ITnx4sf4H++eDDzZeYp9qukn/Z/2ttBailqh1tzWibakNml7THvf6YDTnR3OHS0/m/989Iz2mZqzymdLz5HOFZybOZ93fvJCxoXxi4kXhzpXdD66tOTSna6wrt7LgZevXvG5cqnbvfv8VZerZ645XTt9nX297Yb9jdYeu56WX+x+aem172296XCz/ZbjrY6+BX3n+l37L972un3ljv+dGwOLBvruLr57/17cPel93v3RB6kPXj/Mejj9aP1j7OOiJwpPKp6qP6391fjXZqm99Oyg12DPs4hnj4a4Qy//lfmvT8MFz6nPK0a0RupHrUfPjPmM3Xqx9MXwy4yX0+OFvyn+tveV0auffnf7vWdiycTwa9HrmT9K3qi+OfrW9m3nZOjk03dp76anit6rvj/2gf2h+2P0x5Hp7E/4T5WfjT93fAn88ngmbWbm3/eE8/syOll+AAAH1ElEQVR4Ae2daW/USBCGOxAg4SYh3AJxRERCfOL//4N8JBIJIEDc95EQjnC+vVtWxXjamY5W3lBPS+x43F12++l6XX1ldmJxcfFnIkEAAp0EdnWe5SQEIJAJIBAcAQIFAgikAIcsCCAQfAACBQIIpACHLAggEHwAAgUCCKQAhywIIBB8AAIFAgikAIcsCCAQfAACBQIIpACHLAggEHwAAgUCCKQAhywIIBB8AAIFAgikAIcsCCAQfAACBQIIpACHLAggEHwAAgUCCKQAhywIIBB8AAIFAgikAIcsCCAQfAACBQIIpACHLAggEHwAAgUCCKQAhywIIBB8AAIFAgikAIcsCCAQfAACBQIIpACHLAggEHwAAgUCCKQAhywIIBB8AAIFAgikAIcsCCAQfAACBQIIpACHLAggEHwAAgUCCKQAhywITEZH8O3bt/T8+fP05cuXdPLkyXTgwIHoSHh+RyB8BHn06FF6/PhxevXqVVpZWUk/f/J/xXb+Ef4wvEDW1tYaJ9jY2MiRpDnBQXgC4QVy4sSJxgmOHDmSpqammu8cQCD8GEQCOXz4cPr69Ws6dOgQHgGBTQTCC0Q0FDWIHJv8gi//EgjfxcITIFAiED6CrK+v51ksQdq7d2+6cOHCH7w00/Xp06d8/vTp02lycjI9e/YsvX//Pkee48ePp9nZ2Zz/8uXL9ObNm6Tr7tu3L506dSrNzMz8cU07oes+efIkra6u5ilmlZ+YmEhPnz7NRaanp9O5c+es+KZP2archw8fksqpbrt3787XU0Hd//z585ts/BfV9d27d+njx4+5rMZguv+uXbw3jVN4gWgd5O3bt5nHqG6WHNBmu+REckqtmyjpU0KRuCSKBw8e5PP6j2bF7ty5k+bn59OxY8ea83ag/OXl5Tz+0TmNg3QvObrVSWW6kuqtaWmrh2xVx7Nnzza2pTUdTW3rnyXZS6QSy5UrV7JILS/yJ6+KMVv/4cOH2SkVRXySEEwc7TewFiK7kqKQHNOS3v7fv39Pilh9yYtUZcexlQhNHIpWmpyQvZKEqWuT/iGwuZWh0ktAwrh69Wo6ePBgdiQJRklvejnaxYsXcxfs3r17zZv88+fPnddV98aSumGXLl3K4rt582bvgqWc3JJsL1++nCOZokpfev36dVNE91T3UJFjaWkpn9ei6ZkzZ5oykQ+IIGO2/tzcXBaHzHTsk76rmyYR+bwfP374YvlYgrJxjU7IIRV5NJYojVlUVt0rObQldckUCY4ePdo7Va26aIykpPtZ10/dMd1bSYJWd5H0mxEQxiOgsYYlCUGOacl3u/yx5ftP37XSNcw5VaY0dlB+yVaRrZQkTHXjlCRm3x30dbAxV+laEfIQyJit7B1Kpl4gPs8fd91CUcBSW2iafSqltq2/l3fyrmv4Qb8Xu8r67/4eXdeJco4xyEAtbW9x3d4GyFYV7/B2zn+WbNvX8nY69o6vMdDi4mJTxG/U9OWaAgEPEMhAje6d3EchVWccgYxr23b8rvGR6uAjjb5HTQhkoJb3b3r/5lZ12t/bVfSi6CvbtvXi06zbqNmqPXv2tE1DfkcgAzW7F0j7Le6jS1f1vG27bPtabXs/eaDraOGTNJoAAhnN5j/N8Y7advJ2N6hdEW+rsooiFlXa1+qz9fmauXrx4kU+pelfmwL2ZaIdM4s1UIv7Loyc2raMqDp9axBeIBKHX4j0x12P5meqdB/fRdPaihYJ9a9PaF3X/hvPIZCBWlWO6qdzbfFOXSTbhzWqalq/8AKzlXE5u1+d77KXuGytpH0v7Smz1DddbOX+9k+6WAO2sPr/1qXR/iu90RUB/ELgqOpp1Vy7cZW0d0p2mnnqiz4qr60lthB4//79vKKvCGbikjj279+vouETEWRAF9BfM9qskt7+igTq5tjW+VLVZGvjDtkqAmk3rrbe9yWVMQFoDGM/WmF22l5v17ZzUT8RyIAtLye1zY2qhpyy7+9HrLrajqINijYekdA0ZbuVgbVmrxYWFvIMlheCum2jtubbfaN9TvxeSeV3bgZudUUAbVxs743aSrXathq/3L59O5tKRNeuXSteRuMQ3Vvi8AP4olGgTMYg/4PG1lvcujxbqY6c2sYQihw26JatBGPJum/2vetTZfo2R3bZRTmHQHZgS2vccOvWrabmN27caPZz+elZ6341BTkYmwBjkLGRDW+grpCPDjb7pOhhx6qln0YevtY7swZEkJ3Zbnkwbusfd+/ezdPFmqr1U8SaCiZtjwARZHv8BrPWr5X4xUJN8XpxaKpWP4hH2h4BIsj2+A1mLXFcv349r2Ho79O1UKgulQb7ihxbWQ8ZrPI76MYIZAc1VruqGoR3/Y5Xuxzf6wnQxapnh2UAAggkQCPziPUEEEg9OywDEEAgARqZR6wngEDq2WEZgAACCdDIPGI9AQRSzw7LAAQQSIBG5hHrCSCQenZYBiCAQAI0Mo9YTwCB1LPDMgABBBKgkXnEegIIpJ4dlgEIIJAAjcwj1hNAIPXssAxAAIEEaGQesZ4AAqlnh2UAAggkQCPziPUEEEg9OywDEEAgARqZR6wngEDq2WEZgAACCdDIPGI9AQRSzw7LAAQQSIBG5hHrCSCQenZYBiCAQAI0Mo9YTwCB1LPDMgABBBKgkXnEegIIpJ4dlgEIIJAAjcwj1hP4BWQ+g7ufR9NrAAAAAElFTkSuQmCC' export class Settings extends React.Component { state: State = { color: '#F2F7FA', showImage: false, repeat: 'no-repeat', position: 'center', size: 'auto auto', opacity: 1, angle: 20, } tryToJSON(val: string) { try { return JSON.parse(val) } catch (error) { return val } } notifyChange() { const { showImage, size, position, ...state } = this.state this.props.onChange({ ...state, image: showImage ? bgImageDataURL : undefined, size: this.tryToJSON(size), position: this.tryToJSON(position), }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onShowImageChanged = (e: any) => { this.setState({ showImage: e.target.checked }, () => { this.notifyChange() }) } onRepeatChanged = (repeat: string) => { this.setState({ repeat }, () => { this.notifyChange() }) } onPositionChanged = (position: string) => { this.setState({ position }, () => { this.notifyChange() }) } onSizeChanged = (size: string) => { this.setState({ size }, () => { this.notifyChange() }) } onOpacityChanged = (opacity: number) => { this.setState({ opacity }, () => { this.notifyChange() }) } onWaterAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } render() { return ( Color Show Background Image {this.state.showImage && ( Repeat {this.state.repeat === 'watermark' && ( Watermark Angle
{this.state.angle}
)} Position Size Opacity
{this.state.opacity.toFixed(2)}
)}
) } } ``` #### 标签 - Canonical page: https://x6.antv.antgroup.com/zh/api/model/labels/ - Markdown source: https://x6.antv.antgroup.com/zh/api/model/labels.md - Description: 标签 --- title: "标签" description: "标签" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/model/labels/" version: "3.x" --- X6 中边的标签配置非常灵活,所以这里用单独的篇幅来详细介绍如何使用边的标签。 开始之前,我们先简单了解一下 Edge 实例上操作标签的几个方法。 | 方法签名 | 说明 | |------------------------------------------------------------------|-------------------| | [edge.getLabels()](/zh/api/model/edge#getlabels) | 获取所有标签。 | | [edge.setLabels(...)](/zh/api/model/edge#setlabels) | 设置标签。 | | [edge.insertLabel(...)](/zh/api/model/edge#insertlabel) | 在指定位置插入标签。 | | [edge.appendLabel(...)](/zh/api/model/edge#appendlabel) | 在末尾追加标签。 | | [edge.setLabelAt(...)](/zh/api/model/edge#setlabelat) | 设置指定位置的标签。 | | [edge.getLabelAt(...)](/zh/api/model/edge#getlabelat) | 获取指定位置的标签。 | | [edge.removeLabelAt(...)](/zh/api/model/edge#removelabelat) | 删除指定位置的标签。 | ## 标签定义 标签包含标签 Markup、标签位置、标签样式等,完整的定义如下。 ```ts interface Label { markup?: Markup attrs?: Attr.CellAttrs position?: | number | { distance: number offset?: | number | { x?: number y?: number } angle?: number options?: { absoluteDistance?: boolean reverseDistance?: boolean absoluteOffset?: boolean keepGradient?: boolean ensureLegibility?: boolean } } } ``` - `markup` 标签 Markup。 - `attrs` 标签样式。 - `position` 标签位置。当其值为 `number` 时,相当于设置 `position.distance` 的值。 - `distance` [标签位置](#位置)。 - `offset` [标签偏移](#偏移)。 - `angle` [标签旋转](#旋转)。 ## 默认标签 创建 Edge 时可以通过 [defaultLabel 选项](/zh/tutorial/basic/edge#defaultlabel) 来设置默认标签,其默认值如下: ```ts { markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'text', selector: 'label', }, ], attrs: { text: { fill: '#000', fontSize: 14, textAnchor: 'middle', textVerticalAnchor: 'middle', pointerEvents: 'none', }, rect: { ref: 'label', fill: '#fff', rx: 3, ry: 3, refWidth: 1, refHeight: 1, refX: 0, refY: 0, }, }, position: { distance: 0.5, }, } ``` 该默认标签包含一个 `` 元素(表示标签文本)和一个 `` 元素(表示标签背景),默认居中对齐,并拥有白色圆角背景。由于所有自定义标签都将与该默认标签进行 [merge](https://lodash.com/docs/4.17.15#merge),所以我们可以像下面这样简单提供一个标签的文本属性来添加一个标签。 ```ts edge.appendLabel({ attrs: { text: { text: 'Hello Label', }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ source: { x: 125, y: 125 }, target: { x: 320, y: 125 }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) edge.appendLabel({ attrs: { text: { text: 'Hello Label', }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 标签位置 ### 位置 我们可以通过 Label 的 `position.distance` 选项来指定标签的位置,默认值为 `0.5` 表示标签位于边长度的中心位置。根据取值不同,标签位置的计算方式分下面三种情况。 - 位于 `[0, 1]` 之间时,表示标签位于**从起点开始,沿长度方向,多少相对长度(比例)的位置**。 - 正数表示标签位于**从起点开始,沿边长度方向,偏离起点多少长度的位置**。 - 负数表示标签位于**从终点开始,沿长度方向,偏离终点多少长度的位置**。 ```ts edge.appendLabel({ attrs: { text: { text: '0.25', }, }, position: { distance: 0.25, }, }) edge.appendLabel({ attrs: { text: { text: '150', }, }, position: { distance: 150, }, }) edge.appendLabel({ attrs: { text: { text: '-100', }, }, position: { distance: -100, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ source: { x: 30, y: 80 }, target: { x: 430, y: 80 }, vertices: [{ x: 230, y: 160 }], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) edge.appendLabel({ attrs: { text: { text: '0.25', }, }, position: { distance: 0.25, }, }) edge.appendLabel({ attrs: { text: { text: '150', }, }, position: { distance: 150, }, }) edge.appendLabel({ attrs: { text: { text: '-100', }, }, position: { distance: -100, }, }) let oscillateToggle = false function contract() { edge.animate( { 'source/x': 130, 'source/y': 80, 'target/x': 330, 'target/y': 80, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = true } function oscillate() { edge.animate( { 'source/x': 30, 'source/y': 160, 'vertices/0/x': 230, 'vertices/0/y': 80, 'target/x': 430, 'target/y': 160, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = false } edge.on('animation:finish', () => { if (oscillateToggle) { oscillate() } else { contract() } }) contract() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### 偏移 我们可以通过 Label 的 `position.offset` 选项来设置标签的偏移量,默认值为 `0` 表示不偏移。根据取值不同,标签偏移量的计算方式分为下面三种情况。 - 正数表示标签**沿垂直于边向下的绝对偏移量**。 - 负数表示标签**沿垂直于边向上的绝对偏移量**。 - 坐标对象 `{x: number; y: number }` 表示标签**沿 `x` 和 `y` 两个方向的绝对偏移量**。 ```ts edge.appendLabel({ attrs: { text: { text: 'offset: 40', }, }, position: { distance: 0.66, offset: 40, }, }) edge.appendLabel({ attrs: { text: { text: 'offset: -40', }, }, position: { distance: 0.66, offset: -40, }, }) edge.appendLabel({ attrs: { text: { text: 'offset: { x: -40, y: 80 }', }, }, position: { distance: 0.66, offset: { x: -40, y: 80, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ markup: [ { tagName: 'path', selector: 'line', }, { tagName: 'path', selector: 'offsetLabelPositiveConnector', }, { tagName: 'path', selector: 'offsetLabelNegativeConnector', }, { tagName: 'path', selector: 'offsetLabelAbsoluteConnector', }, { tagName: 'text', selector: 'offsetLabelMarker', }, ], source: { x: 30, y: 80 }, target: { x: 430, y: 80 }, vertices: [{ x: 230, y: 160 }], }) edge.attr({ line: { connection: true, fill: 'none', stroke: '#8f8f8f', strokeWidth: 1, }, offsetLabelMarker: { atConnectionRatio: 0.66, textAnchor: 'middle', textVerticalAnchor: 'middle', text: '*', fill: 'red', stroke: 'black', strokeWidth: 1, fontSize: 32, fontWeight: 'bold', }, offsetLabelPositiveConnector: { atConnectionRatio: 0.66, d: 'M 0 0 0 80', stroke: 'black', strokeDasharray: '5 5', }, offsetLabelNegativeConnector: { atConnectionRatio: 0.66, d: 'M 0 0 0 -80', stroke: 'black', strokeDasharray: '5 5', }, offsetLabelAbsoluteConnector: { atConnectionRatioIgnoreGradient: 0.66, d: 'M 0 0 -40 80', stroke: 'black', strokeDasharray: '5 5', }, }) edge.appendLabel({ attrs: { text: { text: 'offset: 40', }, }, position: { distance: 0.66, offset: 40, }, }) edge.appendLabel({ attrs: { text: { text: 'offset: -40', }, }, position: { distance: 0.66, offset: -40, }, }) edge.appendLabel({ attrs: { text: { text: 'offset: { x: -40, y: 80 }', }, }, position: { distance: 0.66, offset: { x: -40, y: 80, }, }, }) let oscillateToggle = false function contract() { edge.animate( { 'source/x': 130, 'source/y': 80, 'target/x': 330, 'target/y': 80, 'labels/0/position/offset': 80, 'labels/1/position/offset': -80, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = true } function oscillate() { edge.animate( { 'source/x': 30, 'source/y': 160, 'vertices/0/x': 230, 'vertices/0/y': 80, 'target/x': 430, 'target/y': 160, 'labels/0/position/offset': 80, 'labels/1/position/offset': -80, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = false } edge.on('animation:finish', () => { if (oscillateToggle) { oscillate() } else { contract() } }) contract() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### 旋转 我们可以通过 Label 的 `position.angle` 选项来设置标签沿**顺时针方向**的旋转角度,默认值为 `0` 表示不旋转。 **选项** - 当 `position.options.keepGradient` 为 `true` 时,标签的初始旋转角度是标签所在位置的边的角度,后续设置的 `position.angle` 角度是相对于该初始角度的。 - 当 `position.options.ensureLegibility` 为 `true` 时,在必要时将为标签增加 180° 旋转量,以保证标签文本更易读。 ```ts edge.appendLabel({ attrs: { text: { text: '70°\nkeepGradient', }, }, position: { distance: 0.05, angle: 70, options: { keepGradient: true, }, }, }) edge.appendLabel({ attrs: { text: { text: '0°\nkeepGradient', }, }, position: { distance: 0.3, options: { keepGradient: true, }, }, }) edge.appendLabel({ attrs: { text: { text: '45°', }, }, position: { distance: 0.8, angle: 45, }, }) edge.appendLabel({ attrs: { text: { text: '135°', }, }, position: { distance: 0.9, angle: 135, }, }) edge.appendLabel({ attrs: { text: { text: '270°\nkeepGradient', }, }, position: { distance: 0.66, offset: 80, angle: 270, options: { keepGradient: true, }, }, }) edge.appendLabel({ attrs: { text: { text: '270°\nkeepGradient\nensureLegibility', }, }, position: { distance: 0.66, offset: -80, angle: 270, options: { keepGradient: true, ensureLegibility: true, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ markup: [ { tagName: 'path', selector: 'line', }, { tagName: 'path', selector: 'offsetLabelPositiveConnector', }, { tagName: 'path', selector: 'offsetLabelNegativeConnector', }, { tagName: 'path', selector: 'offsetLabelAbsoluteConnector', }, { tagName: 'text', selector: 'offsetLabelMarker', }, ], source: { x: 30, y: 120 }, target: { x: 430, y: 120 }, vertices: [{ x: 230, y: 200 }], }) edge.attr({ line: { connection: true, fill: 'none', stroke: '#8f8f8f', strokeWidth: 1, }, offsetLabelMarker: { atConnectionRatio: 0.66, textAnchor: 'middle', textVerticalAnchor: 'middle', text: '*', fill: 'red', stroke: 'black', strokeWidth: 1, fontSize: 32, fontWeight: 'bold', }, offsetLabelPositiveConnector: { atConnectionRatio: 0.66, d: 'M 0 0 0 80', stroke: 'black', strokeDasharray: '5 5', }, offsetLabelNegativeConnector: { atConnectionRatio: 0.66, d: 'M 0 0 0 -80', stroke: 'black', strokeDasharray: '5 5', }, }) edge.appendLabel({ attrs: { text: { text: '70°\nkeepGradient', }, }, position: { distance: 0.05, angle: 70, options: { keepGradient: true, }, }, }) edge.appendLabel({ attrs: { text: { text: '0°\nkeepGradient', }, }, position: { distance: 0.3, options: { keepGradient: true, }, }, }) edge.appendLabel({ attrs: { text: { text: '45°', }, }, position: { distance: 0.8, angle: 45, }, }) edge.appendLabel({ attrs: { text: { text: '135°', }, }, position: { distance: 0.9, angle: 135, }, }) edge.appendLabel({ attrs: { text: { text: '270°\nkeepGradient', }, }, position: { distance: 0.66, offset: 80, angle: 270, options: { keepGradient: true, }, }, }) edge.appendLabel({ attrs: { text: { text: '270°\nkeepGradient\nensureLegibility', }, }, position: { distance: 0.66, offset: -80, angle: 270, options: { keepGradient: true, ensureLegibility: true, }, }, }) let oscillateToggle = false function contract() { edge.animate( { 'source/x': 130, 'source/y': 120, 'target/x': 330, 'target/y': 120, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = true } function oscillate() { edge.animate( { 'source/x': 30, 'source/y': 200, 'vertices/0/x': 230, 'vertices/0/y': 120, 'target/x': 430, 'target/y': 200, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = false } edge.on('animation:finish', () => { if (oscillateToggle) { oscillate() } else { contract() } }) contract() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 标签样式 我们可以通过 `markup` 和 `attrs` 两个选项来定制标签样式,并支持两个维度的定制。 **方式一**:创建 Edge 时全局覆盖默认标签的定义,影响所有标签。 ```ts const edge = graph.addEdge({ source: { x: 100, y: 40 }, target: { x: 400, y: 40 }, defaultLabel: { markup: [ { tagName: 'ellipse', selector: 'bg', }, { tagName: 'text', selector: 'txt', }, ], attrs: { txt: { fill: '#7c68fc', textAnchor: 'middle', textVerticalAnchor: 'middle', }, bg: { ref: 'txt', refRx: '70%', refRy: '80%', stroke: '#7c68fc', fill: 'white', strokeWidth: 2, }, }, }, }) edge.appendLabel({ attrs: { txt: { text: 'First', }, }, position: { distance: 0.3, }, }) edge.appendLabel({ attrs: { txt: { text: 'Second', }, }, position: { distance: 0.7, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 320, y: 40 }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, defaultLabel: { markup: [ { tagName: 'ellipse', selector: 'bg', }, { tagName: 'text', selector: 'txt', }, ], attrs: { txt: { fill: '#7c68fc', textAnchor: 'middle', textVerticalAnchor: 'middle', }, bg: { ref: 'txt', refRx: '70%', refRy: '80%', stroke: '#7c68fc', fill: 'white', strokeWidth: 2, }, }, }, }) edge.appendLabel({ attrs: { txt: { text: 'First', }, }, position: { distance: 0.3, }, }) edge.appendLabel({ attrs: { txt: { text: 'Second', }, }, position: { distance: 0.7, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` **方式二**:创建单个标签时覆盖默认标签的定义,影响单个标签。 ```ts edge.appendLabel({ markup: [ { tagName: 'circle', selector: 'body', }, { tagName: 'text', selector: 'label', }, { tagName: 'circle', selector: 'asteriskBody', }, { tagName: 'text', selector: 'asterisk', }, ], attrs: { label: { text: '½', fill: '#000', fontSize: 12, textAnchor: 'middle', textVerticalAnchor: 'middle', pointerEvents: 'none', }, body: { ref: 'label', fill: '#fff', stroke: '#000', strokeWidth: 1, refR: 1, refCx: 0, refCy: 0, }, asterisk: { ref: 'label', text: '*', fill: '#ff0000', fontSize: 8, textAnchor: 'middle', textVerticalAnchor: 'middle', pointerEvents: 'none', refX: 16.5, refY: -2, }, asteriskBody: { ref: 'asterisk', fill: '#fff', stroke: '#000', strokeWidth: 1, refR: 1, refCx: '50%', refCy: '50%', refX: 0, refY: 0, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 320, y: 40 }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) edge.appendLabel({ markup: [ { tagName: 'circle', selector: 'body', }, { tagName: 'text', selector: 'label', }, { tagName: 'circle', selector: 'asteriskBody', }, { tagName: 'text', selector: 'asterisk', }, ], attrs: { label: { text: '½', fill: '#000', fontSize: 12, textAnchor: 'middle', textVerticalAnchor: 'middle', pointerEvents: 'none', }, body: { ref: 'label', fill: '#fff', stroke: '#000', strokeWidth: 1, refR: 1, refCx: 0, refCy: 0, }, asterisk: { ref: 'label', text: '*', fill: '#ff0000', fontSize: 8, textAnchor: 'middle', textVerticalAnchor: 'middle', pointerEvents: 'none', refX: 16.5, refY: -2, }, asteriskBody: { ref: 'asterisk', fill: '#fff', stroke: '#000', strokeWidth: 1, refR: 1, refCx: '50%', refCy: '50%', refX: 0, refY: 0, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 字符串标签 当通过 [`updateLabels`](#默认标签) 选项设置[默认标签](#默认标签)后,标签的添加就显得非常简单,看下面代码。 ```ts // 创建节点时指定标签 const edge = graph.addEdge({ source, target, labels: [ { attrs: { label: { text: 'edge label' } }, }, ], }) // 重设标签 edge.setLabels([ { attrs: { label: { text: 'edge label' } }, }, ]) // 追加标签 edge.appendLabel({ attrs: { label: { text: 'edge label' } }, }) ``` 上面代码其实仅仅设置了标签的文本,但代码看起来并不简单,我们不得不提供一个嵌套很深的对象 `{ attrs: { label: { text: 'edge' } } }`,为了解决这个问题,我们提供了一个语法糖,支持直接传入字符串标签,上面代码可以进一步简化为。 ```ts const edge = graph.addEdge({ source, target, labels: ['edge label'], }) edge.setLabels(['edge label']) edge.appendLabel('edge label') ``` 该语法糖在 `Edge` 上定义了一个静态方法 `parseStringLabel`,该方法将字符串标签转换成了 Label 对象。默认的实现如下。 ```ts function parseStringLabel(label: string): Label { return { attrs: { label: { text: label } }, } } ``` 需要注意的是,这个语法糖仅适用于系统的默认标签,也就是说当你通过 `defaultLabel` 选项重新定义默认标签的 `markup` 后,还需要重写 `parseStringLabel` 方法来保证字符串标签的可用性。 ```ts Edge.config({ defaultLabel: { markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'text', selector: 'my-label', // 这里修改了默认的 selector。 }, ], }, }) // 需要同时重新定义 parseStringLabel,来保证字符串标签的可用性。 Edge.parseStringLabel = (label: string) => { return { attrs: { 'my-label': { text: label } }, } } ``` ## 单标签 大多数边都只拥有最多一个标签,所以我们为 `Edge` 定义了一个[自定义选项](/zh/api/model/cell#自定义选项) `label` 来支持传入单标签。 ```ts graph.addEdge({ source, target, label: { attrs: { label: { text: 'edge label' } }, }, }) ``` 当只需要设置标签文本是,也可以使用单标签的字符串形式。 ```ts graph.addEdge({ source, target, label: 'edge label', }) ``` ### docs/demos/api/label/append-label/index.less ```less .append-label-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 250px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/label/label-position/index.less ```less .label-position { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/label/label-offset/index.less ```less .label-offset-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/label/label-rotate/index.less ```less .label-rotate-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/label/label-markup/index.less ```less .label-markup-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 80px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/label/label-attrs/index.less ```less .label-attrs-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 80px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### 节点工具 - Canonical page: https://x6.antv.antgroup.com/zh/api/registry/node-tool/ - Markdown source: https://x6.antv.antgroup.com/zh/api/registry/node-tool.md - Description: 节点工具 --- title: "节点工具" description: "节点工具" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/registry/node-tool/" version: "3.x" --- 节点小工具是一些渲染在节点上的一些小组件,这些小工具通常都附带一些交互功能,如删除按钮,点击按钮时删除对应的节点。我们可以根据下面的一些场景来添加或删除小工具。 ```ts // 创建节点时添加小工具 graph.addNode({ ..., tools: [ { name: 'button-remove', args: { x: 10, y: 10 }, }, ], }) // 创建节点后添加小工具 node.addTools([ { name: 'button-remove', args: { x: 10, y: 10 }, }, ]) // 删除工具 graph.on("node:mouseleave", ({ node }) => { if (node.hasTool("button-remove")) { node.removeTool("button-remove"); } }); ``` 在 X6 中默认提供了以下几个用于节点的小工具: - [button](#button) 在指定位置处渲染一个按钮,支持自定义按钮的点击交互。 - [button-remove](#button-remove) 在指定的位置处,渲染一个删除按钮,点击时删除对应的节点。 - [boundary](#boundary) 根据节点的包围盒渲染一个包围节点的矩形。注意,该工具仅仅渲染一个矩形,不带任何交互。 - [node-editor](#node-editor) 提供节点文本编辑功能。 ## 内置工具 ### button 在指定位置处渲染一个按钮,支持自定义按钮的点击交互。配置如下: | 参数名 | 类型 | 默认值 | 说明 | |-----------------|------------------------------------------------------------------------|--------|--------------------------------------------------------------------------------------------------------| | x | number \| string | `0` | 相对于节点的左上角 X 轴的坐标,小数和百分比表示相对位置。 | | y | number \| string | `0` | 相对于节点的左上角 Y 轴的坐标,小数和百分比表示相对位置。 | | offset | number \| `{ x: number, y: number }` | `0` | 在 `x` 和 `y` 基础上的偏移量。 | | rotate | boolean | - | 是否跟随节点旋转。 | | useCellGeometry | boolean | `true` | 是否使用几何计算的方式来计算元素包围盒,开启后会有性能上的提升,如果出现计算准度问题,请将它设置为 `false`。 | | markup | Markup.JSONMarkup | - | 渲染按钮的 Markup 定义。 | | onClick | `(args: {e: Dom.MouseDownEvent, cell: Cell, view: CellView }) => void` | - | 点击按钮的回调函数。 | ```ts // 鼠标 Hover 时添加按钮 graph.on('node:mouseenter', ({ node }) => { node.addTools({ name: 'button', args: { markup: ..., x: 0, y: 0, offset: { x: 18, y: 18 }, onClick({ view }) { ... }, }, }) }) // 鼠标移开时删除按钮 graph.on('node:mouseleave', ({ node }) => { node.removeTools() // 删除所有的工具 }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Color, NodeView } from '@antv/x6' 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({ x: 40, y: 40, width: 120, height: 48, label: 'source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, tools: [ { name: 'button', args: { markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 14, stroke: '#fe854f', 'stroke-width': 3, fill: 'white', cursor: 'pointer', }, }, { tagName: 'text', textContent: 'Btn A', selector: 'icon', attrs: { fill: '#fe854f', 'font-size': 8, 'text-anchor': 'middle', 'pointer-events': 'none', y: '0.3em', }, }, ], x: '100%', y: '100%', offset: { x: -18, y: -18 }, onClick({ view }: { view: NodeView }) { const node = view.cell const fill = Color.randomHex() node.attr({ body: { fill, }, label: { fill: Color.invert(fill, true), }, }) }, }, }, ], }) const target = graph.addNode({ x: 160, y: 180, width: 120, height: 48, label: 'target', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.on('node:mouseenter', ({ node }) => { if (node === target) { node.addTools({ name: 'button', args: { markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 14, stroke: '#fe854f', 'stroke-width': 3, fill: 'white', cursor: 'pointer', }, }, { tagName: 'text', textContent: 'Btn B', selector: 'icon', attrs: { fill: '#fe854f', 'font-size': 8, 'text-anchor': 'middle', 'pointer-events': 'none', y: '0.3em', }, }, ], x: 0, y: 0, offset: { x: 18, y: 18 }, onClick({ view }: { view: NodeView }) { const node = view.cell const offset = parseInt( node.attr('body/strokeDashoffset') || '0', 10, ) node.attr({ body: { stroke: Color.randomHex(), strokeDasharray: '5, 1', strokeDashoffset: offset + 20, }, }) }, }, }) } }) graph.on('node:mouseleave', ({ cell }) => { if (cell === target) { cell.removeTools() } }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### button-remove 在指定的位置处,渲染一个删除按钮,点击时删除对应的节点。它是上面 `button` 工具的一个特例,所以支持 `button` 的所有配置。 ```ts const source = graph.addNode({ ..., // 添加一个始终显示的删除按钮 tools: [ { name: 'button-remove', args: { x: '100%', y: 0, offset: { x: -10, y: 10 }, }, }, ], }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' 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({ x: 40, y: 40, width: 100, height: 40, label: 'source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, tools: [ { name: 'button-remove', args: { x: '100%', y: 0, offset: { x: -10, y: 10 }, }, }, ], }) const target = graph.addNode({ x: 120, y: 160, width: 100, height: 40, label: 'target', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.on('node:mouseenter', ({ node }) => { if (node === target) { node.addTools({ name: 'button-remove', args: { x: 0, y: 0, offset: { x: 10, y: 10 }, }, }) } }) graph.on('node:mouseleave', ({ node }) => { if (node === target) { node.removeTools() } }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### boundary 根据节点的包围盒渲染一个包围节点的矩形。注意,该工具仅仅渲染一个矩形,不带任何交互。配置如下: | 参数名 | 类型 | 默认值 | 说明 | |-----------------|-------------|----------|--------------------------------------------------------------------------------------------------------| | tagName | string | `rect` | 使用何种图形渲染。 | | rotate | boolean | - | 图形是否跟随节点旋转。 | | padding | SideOptions | `10` | 边距。 | | attrs | KeyValue | `object` | 图形属性。 | | useCellGeometry | boolean | `true` | 是否使用几何计算的方式来计算元素包围盒,开启后会有性能上的提升,如果出现计算准度问题,请将它设置为 `false`。 | 其中 `attrs` 的默认值(默认样式)为: ```ts { fill: 'none', stroke: '#333', 'stroke-width': 0.5, 'stroke-dasharray': '5, 5', 'pointer-events': 'none', } ``` `SideOptions` 的类型定义如下: ```typescript type SideOptions = | number | { vertical?: number horizontal?: number left?: number top?: number right?: number bottom?: number } ``` 工具使用方式如下: ```ts const source = graph.addNode({ ..., tools: [ { name: 'boundary', args: { padding: 5, attrs: { fill: '#7c68fc', stroke: '#333', 'stroke-width': 1, 'fill-opacity': 0.2, }, }, }, ], }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' 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({ x: 40, y: 40, width: 100, height: 40, label: 'source', tools: [ { name: 'boundary', args: { padding: 5, attrs: { fill: '#7c68fc', stroke: '#333', 'stroke-width': 1, 'fill-opacity': 0.2, }, }, }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode({ x: 120, y: 160, width: 100, height: 40, label: 'target', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.on('node:mouseenter', ({ node }) => { if (node === target) { node.addTools({ name: 'boundary', args: { attrs: { fill: '#7c68fc', stroke: '#333', 'stroke-width': 1, 'fill-opacity': 0.2, }, }, }) } }) graph.on('node:mouseleave', ({ node }) => { if (node === target) { node.removeTools() } }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### node-editor 提供节点上文本编辑功能。配置如下: | 参数名 | 类型 | 默认值 | 说明 | |-----------------------|-------------------------------------------------------------------------|--------------------------------|--------------------------------------------------------------| | x | number \| string | - | 相对于节点的左上角 X 轴的坐标,小数和百分比表示相对位置 | | y | number \| string | - | 相对于节点的左上角 Y 轴的坐标,小数和百分比表示相对位置 | | attrs/fontSize | string | `14` | 编辑文本字体大小 | | attrs/color | string | `#000` | 编辑文本字体颜色 | | attrs/fontFamily | string | `Arial, helvetica, sans-serif` | 编辑文本的字体 | | attrs/backgroundColor | string | `#fff` | 编辑区域的背景色 | | getText | string \| `(this: CellView, args: {cell: Cell}) => string` | - | 获取原文本方法,在自定义 `markup` 场景需要自定义 `getText` 方法 | | setText | string \| `(this: CellView, args: {cell: Cell, value: string}) => void` | - | 设置新文本,在自定义 `markup` 场景需要自定义 `setText` 方法 | > **注意** 需要注意的是,2.8.0 版本后不需要在双击事件中去动态添加工具,也就不需要传入事件参数。 ```ts // 2.8.0 版本之前使用方式 graph.on('node:dblclick', ({ node, e }) => { node.addTools({ name: 'node-editor', args: { event: e, }, }) }) // 2.8.0 版本之后使用方式 node.addTools({ name: 'node-editor', }) ``` 还需要注意的是,如果在节点中自定义了 `markup`,往往需要自定义 `getText` 和 `setText` 方法来正确获取和设置编辑文本,这两个配置都支持函数和字符串两种形式,函数比较好理解,字符串其实就是要获取或者设置的文本的属性路径,一般情况下建议使用字符串形式,这样图数据可以完全序列化(因为函数无法序列化),否则可能会出现渲染画布后文本编辑功能异常,比如: ```typescript node.addTools({ name: 'node-editor', args: { getText: 'a/b', setText: 'c/d', }, }) ``` 上面配置表示: - 获取编辑文本:`node.attr('a/b')` - 设置编辑文本:`node.attr('c/d', value)` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, connecting: { connector: 'smooth', }, }) const source = graph.addNode({ x: 40, y: 40, width: 100, height: 40, label: 'source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, tools: ['node-editor'], }) const target = graph.addNode({ x: 180, y: 200, width: 100, height: 40, label: 'target', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, tools: ['node-editor'], }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, labels: [ { position: 0.3, attrs: { label: { text: 'edge label 1', }, }, }, { position: 0.6, attrs: { label: { text: 'edge label 2', }, }, }, ], tools: ['edge-editor'], }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 自定义工具 ### 方式一 继承 `ToolItem` 实现一个工具类,难度较高,要求对 [ToolItem](https://github.com/antvis/X6/blob/master/src/view/tool.ts) 类都有所了解,可以参考上述内置工具的源码,这里不展开叙述。 ```ts Graph.registerNodeTool('button', Button) ``` ### 方式二 继承已经注册的工具,在继承基础上修改配置。我们在 `ToolItem` 基类上提供了一个静态方法 `define` 来快速实现继承并修改配置。 ```ts const MyButton = Button.define({ name: 'my-btn', markup: ..., onClick({ view }) { ... }, }) Graph.registerNodeTool('my-btn', MyButton, true) ``` 同时,我们为 `Graph.registerNodeTool` 方法提供了一种快速继承并指定默认选项的实现: ```ts Graph.registerNodeTool('my-btn', { inherit:'button', // 基类名称,使用已经注册的工具名称。 markup: ..., onClick: ..., }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Color, CellView } from '@antv/x6' Graph.registerNodeTool( 'my-btn', { inherit: 'button', markup: [ { tagName: 'rect', selector: 'button', attrs: { width: 40, height: 20, rx: 4, ry: 4, fill: 'white', stroke: '#fe854f', 'stroke-width': 2, cursor: 'pointer', }, }, { tagName: 'text', selector: 'text', textContent: 'btn', attrs: { fill: '#fe854f', 'font-size': 10, 'text-anchor': 'middle', 'pointer-events': 'none', x: 20, y: 13, }, }, ], onClick({ view }: { view: CellView }) { const node = view.cell const fill = Color.randomHex() node.attr({ body: { fill, }, label: { fill: Color.invert(fill, true), }, }) }, }, 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({ x: 40, y: 40, width: 140, height: 48, label: 'source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, tools: [ { name: 'my-btn', args: { x: '100%', y: '100%', offset: { x: -44, y: -24 }, }, }, ], }) const target = graph.addNode({ x: 160, y: 160, width: 140, height: 48, label: 'target', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/api/node-tool/button/index.less ```less .button-tool-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/node-tool/button-remove/index.less ```less .button-remove-tool-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/node-tool/boundary/index.less ```less .boundary-tool-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/node-tool/node-editor/index.less ```less .node-editor-tool-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/node-tool/custom-button/index.less ```less .custom-button-tool-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### 画布平移 - Canonical page: https://x6.antv.antgroup.com/zh/api/graph/panning/ - Markdown source: https://x6.antv.antgroup.com/zh/api/graph/panning.md - Description: 画布平移 --- title: "画布平移" description: "画布平移" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/graph/panning/" version: "3.x" --- ## 演示 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.initGraph({ enabled: true, modifiers: [], eventTypes: ['leftMouseDown'], }) } initGraph(options: any) { if (this.graph) { this.graph.dispose() } this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, panning: { ...options, }, }) this.graph.addNode({ x: 320, y: 100, width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const source = this.graph.addNode({ x: 80, y: 50, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 240, y: 200, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph.centerContent() } onSettingChanged = (options: State) => { this.initGraph(options) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 配置 普通画布(未使用 `Scroller` 插件)通过 `panning` 选项来支持拖拽平移。 ```ts const graph = new Graph({ panning: true, // 不传该选项时,默认值也为 true }) // 等同于 const graph = new Graph({ panning: { enabled: true, }, }) ``` 支持的选项如下: ```ts interface Options { enabled?: boolean modifiers?: ModifierKey eventTypes?: ('leftMouseDown' | 'rightMouseDown' | 'mouseWheel' | 'mouseWheelDown')[] } ``` ### enabled 是否开启画布平移交互。 ### modifiers 拖拽可能和其他操作冲突,此时可以设置 `modifiers` 参数,设置修饰键后需要按下修饰键并点击鼠标才能触发画布拖拽。 `ModifierKey` 的类型定义如下: ```ts type ModifierKey = string | ('alt' | 'ctrl' | 'meta' | 'shift' | 'space')[] | null ``` 支持以下几种形式: - `alt` 表示按下 `alt`。 - `[alt, ctrl]` 表示按下 `alt` 或 `ctrl`。 - `alt|ctrl` 表示按下 `alt` 或 `ctrl`。 - `alt&ctrl` 表示同时按下 `alt` 和 `ctrl`。 - `alt|ctrl&shift` 表示同时按下 `alt` 和 `shift` 或者同时按下 `ctrl` 和 `shift`。 ### eventTypes 触发画布平移的交互方式。支持三种形式或者他们之间的组合: - `leftMouseDown`: 按下鼠标左键移动进行拖拽 - `rightMouseDown`: 按下鼠标右键移动进行拖拽 - `mouseWheel`: 使用鼠标滚轮滚动拖拽 - `mouseWheelDown`: 按下鼠标滚轮进行拖拽 ## 方法 ### isPannable() ```ts isPannable(): boolean ``` 返回是否启用了画布平移交互功能。 ### enablePanning() ```ts enablePanning(): this ``` 启用画布平移功能。 ### disablePanning() ```ts disablePanning(): this ``` 禁用画布平移功能。 ### togglePanning(...) ```ts togglePanning(enabled?: boolean): this ``` 切换画布平移启用状态。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |---------|---------|:----:|--------|--------------------------------------------------| | enabled | boolean | | - | 是否启用画布平移功能,缺省时切换画布平移的启用状态。 | ### docs/demos/api/panning/playground/index.less ```less .panning-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } } ``` ### docs/demos/api/panning/playground/settings.tsx ```tsx import React from 'react' import { Checkbox, Switch, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { enabled: boolean modifiers: string[] eventTypes: string[] } export class Settings extends React.Component { state: State = { enabled: true, modifiers: [], eventTypes: ['leftMouseDown'], } notifyChange() { this.props.onChange({ ...this.state, }) } onEnabledChanged = (enabled: boolean) => { this.setState({ enabled }, () => { this.notifyChange() }) } onModifiersChange = (modifiers: string[]) => { this.setState({ modifiers }, () => { this.notifyChange() }) } onEventTypesChange = (eventTypes: string[]) => { this.setState({ eventTypes }, () => { this.notifyChange() }) } render() { return ( Enabled Modifier Key Event Type ) } } ``` #### 箭头 - Canonical page: https://x6.antv.antgroup.com/zh/api/model/marker/ - Markdown source: https://x6.antv.antgroup.com/zh/api/model/marker.md - Description: 箭头 --- title: "箭头" description: "箭头" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/model/marker/" version: "3.x" --- 在之前的[教程](/zh/tutorial/basic/edge#使用箭头)中,我们简单介绍了如何使用 [`sourceMarker`](/zh/api/registry/attr#sourcemarker) 和 [`targetMarker`](/zh/api/registry/attr#targetmarker) 两个特殊属性来为边指定起始箭头和终止箭头,并演示了如何使用内置箭头和自定义箭头,接下来我们先列举了每个内置箭头的参数项,然后详细介绍如何使用各种 SVG 元素来自定义箭头,最后介绍如何将自定义箭头注册为内置箭头。 ## 内置箭头 内置箭头是将一些常用的箭头参数化,使用内置箭头时只需要指定箭头名 `name` 和对应的参数即可,填充颜色 `fill` 和边框颜色 `stroke` 默认继承自边,可以通过指定 `fill` 和 `stroke` 属性来覆盖。 ```ts edge.attr({ line: { sourceMarker: 'block', targetMarker: { name: 'ellipse', rx: 10, // 椭圆箭头的 x 半径 ry: 6, // 椭圆箭头的 y 半径 }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import * as React from 'react' import { Graph, Edge } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private edge: Edge componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.edge = this.graph.addEdge({ source: [80, 160], target: [480, 160], attrs: { line: { sourceMarker: 'block', targetMarker: 'block', stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } onMarkerChange = ({ type, ...args }: State) => { this.edge.attr({ line: { sourceMarker: { args, name: type }, targetMarker: { args, name: type }, }, } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 每种内置箭头都有对应的参数,下面将分别介绍。 ### block 实心箭头。 | 参数名 | 类型 | 默认值 | 说明 | |----------|---------|--------|------------------------------------------------------------------------------------| | size | Number | 10 | 箭头大小。 | | width | Number | size | 箭头宽度,当宽高一样时可以直接使用 `size` 替代。 | | height | Number | size | 箭头高度,当宽高一样时可以直接使用 `size` 替代。 | | offset | Number | 0 | 沿边方向的绝对偏移量。 | | open | Boolean | false | 非封闭箭头。 | | ...attrs | Object | { } | 其他参数都将作为箭头 `` 元素的属性,例如可以指定 `‘fill’` 和 `'stroke'` 等属性。 | ### classic 经典箭头。 | 参数名 | 类型 | 默认值 | 说明 | |----------|--------|--------|------------------------------------------------------------------------------------| | size | Number | 10 | 箭头大小。 | | width | Number | size | 箭头宽度,当宽高一样时可以直接使用 `size` 替代。 | | height | Number | size | 箭头高度,当宽高一样时可以直接使用 `size` 替代。 | | offset | Number | 0 | 沿边方向的绝对偏移量。 | | ...attrs | Object | { } | 其他参数都将作为箭头 `` 元素的属性,例如可以指定 `‘fill’` 和 `'stroke'` 等属性。 | ### diamond 菱形箭头。 | 参数名 | 类型 | 默认值 | 说明 | |----------|--------|--------|------------------------------------------------------------------------------------| | size | Number | 10 | 箭头大小。 | | width | Number | size | 箭头宽度,当宽高一样时可以直接使用 `size` 替代。 | | height | Number | size | 箭头高度,当宽高一样时可以直接使用 `size` 替代。 | | offset | Number | 0 | 沿边方向的绝对偏移量。 | | ...attrs | Object | { } | 其他参数都将作为箭头 `` 元素的属性,例如可以指定 `‘fill’` 和 `'stroke'` 等属性。 | ### cross 交叉箭头。 | 参数名 | 类型 | 默认值 | 说明 | |----------|--------|--------|------------------------------------------------------------------------------------| | size | Number | 10 | 箭头大小。 | | width | Number | size | 箭头宽度,当宽高一样时可以直接使用 `size` 替代。 | | height | Number | size | 箭头高度,当宽高一样时可以直接使用 `size` 替代。 | | offset | Number | 0 | 沿边方向的绝对偏移量。 | | ...attrs | Object | { } | 其他参数都将作为箭头 `` 元素的属性,例如可以指定 `‘fill’` 和 `'stroke'` 等属性。 | ### async | 参数名 | 类型 | 默认值 | 说明 | |----------|---------|--------|------------------------------------------------------------------------------------| | width | Number | 10 | 箭头宽度。 | | height | Number | 6 | 箭头高度。 | | offset | Number | 0 | 沿边方向的绝对偏移量。 | | open | Boolean | false | 非封闭箭头。 | | flip | Boolean | false | 是否翻转箭头。 | | ...attrs | Object | { } | 其他参数都将作为箭头 `` 元素的属性,例如可以指定 `‘fill’` 和 `'stroke'` 等属性。 | ### path 自定义 [pathData](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) 的箭头。 | 参数名 | 类型 | 默认值 | 说明 | |----------|--------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------| | d | string | undefined | `` 元素的 [d 属性值](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d),被 `Util.normalizeMarker` 标准化后应用到 `` 元素上。 | | offsetX | Number | 0 | x 方向偏移量。 | | offsetY | Number | 0 | y 方向偏移量。 | | ...attrs | Object | { } | 其他参数都将作为箭头 `` 元素的属性,例如可以指定 `‘fill’` 和 `'stroke'` 等属性。 | ```ts graph.addEdge({ source: { x: 100, y: 40 }, target: { x: 400, y: 40 }, attrs: { line: { stroke: '#31d0c6', sourceMarker: { name: 'path', d: 'M5.5,15.499,15.8,21.447,15.8,15.846,25.5,21.447,25.5,9.552,15.8,15.152,15.8,9.552z', }, targetMarker: { name: 'path', offsetX: 10, d: 'M4.834,4.834L4.833,4.833c-5.889,5.892-5.89,15.443,0.001,21.334s15.44,5.888,21.33-0.002c5.891-5.891,5.893-15.44,0.002-21.33C20.275-1.056,10.725-1.056,4.834,4.834zM25.459,5.542c0.833,0.836,1.523,1.757,2.104,2.726l-4.08,4.08c-0.418-1.062-1.053-2.06-1.912-2.918c-0.859-0.859-1.857-1.494-2.92-1.913l4.08-4.08C23.7,4.018,24.622,4.709,25.459,5.542zM10.139,20.862c-2.958-2.968-2.959-7.758-0.001-10.725c2.966-2.957,7.756-2.957,10.725,0c2.954,2.965,2.955,7.757-0.001,10.724C17.896,23.819,13.104,23.817,10.139,20.862zM5.542,25.459c-0.833-0.837-1.524-1.759-2.105-2.728l4.081-4.081c0.418,1.063,1.055,2.06,1.914,2.919c0.858,0.859,1.855,1.494,2.917,1.913l-4.081,4.081C7.299,26.982,6.379,26.292,5.542,25.459zM8.268,3.435l4.082,4.082C11.288,7.935,10.29,8.571,9.43,9.43c-0.858,0.859-1.494,1.855-1.912,2.918L3.436,8.267c0.58-0.969,1.271-1.89,2.105-2.727C6.377,4.707,7.299,4.016,8.268,3.435zM22.732,27.563l-4.082-4.082c1.062-0.418,2.061-1.053,2.919-1.912c0.859-0.859,1.495-1.857,1.913-2.92l4.082,4.082c-0.58,0.969-1.271,1.891-2.105,2.728C24.623,26.292,23.701,26.983,22.732,27.563z', }, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 320, y: 40 }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, sourceMarker: { name: 'path', d: 'M5.5,15.499,15.8,21.447,15.8,15.846,25.5,21.447,25.5,9.552,15.8,15.152,15.8,9.552z', }, targetMarker: { name: 'path', offsetX: 10, d: 'M4.834,4.834L4.833,4.833c-5.889,5.892-5.89,15.443,0.001,21.334s15.44,5.888,21.33-0.002c5.891-5.891,5.893-15.44,0.002-21.33C20.275-1.056,10.725-1.056,4.834,4.834zM25.459,5.542c0.833,0.836,1.523,1.757,2.104,2.726l-4.08,4.08c-0.418-1.062-1.053-2.06-1.912-2.918c-0.859-0.859-1.857-1.494-2.92-1.913l4.08-4.08C23.7,4.018,24.622,4.709,25.459,5.542zM10.139,20.862c-2.958-2.968-2.959-7.758-0.001-10.725c2.966-2.957,7.756-2.957,10.725,0c2.954,2.965,2.955,7.757-0.001,10.724C17.896,23.819,13.104,23.817,10.139,20.862zM5.542,25.459c-0.833-0.837-1.524-1.759-2.105-2.728l4.081-4.081c0.418,1.063,1.055,2.06,1.914,2.919c0.858,0.859,1.855,1.494,2.917,1.913l-4.081,4.081C7.299,26.982,6.379,26.292,5.542,25.459zM8.268,3.435l4.082,4.082C11.288,7.935,10.29,8.571,9.43,9.43c-0.858,0.859-1.494,1.855-1.912,2.918L3.436,8.267c0.58-0.969,1.271-1.89,2.105-2.727C6.377,4.707,7.299,4.016,8.268,3.435zM22.732,27.563l-4.082-4.082c1.062-0.418,2.061-1.053,2.919-1.912c0.859-0.859,1.495-1.857,1.913-2.92l4.082,4.082c-0.58,0.969-1.271,1.891-2.105,2.728C24.623,26.292,23.701,26.983,22.732,27.563z', }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### circle 圆形箭头。 | 参数名 | 类型 | 默认值 | 说明 | |----------|--------|--------|--------------------------------------------------------------------------------------| | r | Number | 5 | 圆半径。 | | ...attrs | Object | { } | 其他参数都将作为箭头 `` 元素的属性,例如可以指定 `‘fill’` 和 `'stroke'` 等属性。 | ### circlePlus 圆形和加号箭头。 | 参数名 | 类型 | 默认值 | 说明 | |----------|--------|--------|------------------------------------------------------------------------------------------| | r | Number | 5 | 圆半径。 | | ...attrs | Object | { } | 其他参数都将作为箭头 `` 元素(加号)的属性,例如可以指定 `‘fill’` 和 `'stroke'` 等属性。 | ### ellipse 椭圆箭头。 | 参数名 | 类型 | 默认值 | 说明 | |----------|--------|--------|---------------------------------------------------------------------------------------| | rx | Number | 5 | 椭圆 x 轴半径。 | | ry | Number | 5 | 椭圆 y 轴半径。 | | ...attrs | Object | { } | 其他参数都将作为箭头 `` 元素的属性,例如可以指定 `‘fill’` 和 `'stroke'` 等属性。 | ## 自定义箭头 我们可以通过 `tagName` 来指定使用哪种 SVG 元素来渲染箭头,例如下面我们通过 `tagName` 来指定使用 `` 元素来渲染箭头,除 `tagName` 外的其他选项都将作为属性被添加到创建出来的 `` 元素上。箭头的填充颜色 `fill` 和边框颜色 `stroke` 默认继承自边,可以通过指定 `fill` 和 `stroke` 属性来覆盖。 ```ts edge.attr({ line: { sourceMarker: { tagName: 'path', d: 'M 20 -10 0 0 20 10 Z', }, targetMarker: { tagName: 'path', fill: 'yellow', // 使用自定义填充色 stroke: 'green', // 使用自定义边框色 strokeWidth: 2, d: 'M 20 -10 0 0 20 10 Z', }, }, }) ``` 看上面代码,值得注意的是,我们的起始箭头和终止箭头使用了相同的 `'d'` 属性值,这是因为我们会自动计算箭头方向,简单来说,我们只需要定义**向左指向坐标原点**的箭头即可。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ source: [228.84550125020417, 100.76702664502545], target: [416.2834258874138, 72.03741369165368], vertices: [{ x: 300, y: 150 }], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, sourceMarker: { tagName: 'path', d: 'M 20 -10 0 0 20 10 Z', }, targetMarker: { tagName: 'path', stroke: '#873bf4', strokeWidth: 2, fill: 'yellow', d: 'M 20 -10 0 0 20 10 Z', }, }, }, }) function hourHand() { const originX = 300 const originY = 150 const radius = 140 / 1.618 const steps = 60 const offset = (10 + 9.36 / 60) / 12 const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = 2 * Math.PI * (i / steps + offset) - Math.PI / 2 xs.push(originX + radius * Math.cos(theta)) ys.push(originY + radius * Math.sin(theta)) } edge.animate( { 'source/x': xs, 'source/y': ys, }, { delay: 1000, duration: 19000, easing: 'linear', fill: 'none', }, ) } function minuteHand() { const originX = 300 const originY = 150 const radius = 140 const steps = 60 const offset = 9.36 / 60 const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = 2 * Math.PI * (i / steps + offset) - Math.PI / 2 xs.push(originX + radius * Math.cos(theta)) ys.push(originY + radius * Math.sin(theta)) } edge.animate( { 'target/x': xs, 'target/y': ys, }, { delay: 1000, duration: 19000 / 12, iterations: 12, easing: 'linear', fill: 'none', }, ) } let currentAnimations = 0 function run() { currentAnimations = 2 hourHand() minuteHand() } run() edge.on('animation:finish', () => { currentAnimations -= 1 if (currentAnimations === 0) { run() } }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 有时候,我们获取到的 path 元素的 `d` 的坐标可能并不标准,如果直接将其作为箭头来使用就可能出现位置偏离,所以我们在 `Util` 命名空间中提供了 `normalizeMarker` 这个工具方法来标准化 `d` 的坐标。 **方法签名** ```ts Registry.Marker.normalize(d: string, offset: { x?: number; y?: number }): string Registry.Marker.normalize(d: string, offsetX?: number, offsetY?: number): string ``` | 参数名 | 类型 | 说明 | |---------|----------------------------|-----------------------------| | d | string | | | offset | { x?: number; y?: number } | 相对于坐标原点的偏移量 | | offsetX | number | 相对于坐标原点的 x 轴偏移量 | | offsetY | number | 相对于坐标原点的 y 轴偏移量 | 对比下面的起始和终止箭头,很明显起始箭头出现了一定的偏移,使用 `normalizeMarker` 处理后终止箭头的位置就正常了。 ```ts const d = 'M4.834,4.834L4.833,4.833c-5.889,5.892-5.89,15.443,0.001,21.334s15.44,5.888,21.33-0.002c5.891-5.891,5.893-15.44,0.002-21.33C20.275-1.056,10.725-1.056,4.834,4.834zM25.459,5.542c0.833,0.836,1.523,1.757,2.104,2.726l-4.08,4.08c-0.418-1.062-1.053-2.06-1.912-2.918c-0.859-0.859-1.857-1.494-2.92-1.913l4.08-4.08C23.7,4.018,24.622,4.709,25.459,5.542zM10.139,20.862c-2.958-2.968-2.959-7.758-0.001-10.725c2.966-2.957,7.756-2.957,10.725,0c2.954,2.965,2.955,7.757-0.001,10.724C17.896,23.819,13.104,23.817,10.139,20.862zM5.542,25.459c-0.833-0.837-1.524-1.759-2.105-2.728l4.081-4.081c0.418,1.063,1.055,2.06,1.914,2.919c0.858,0.859,1.855,1.494,2.917,1.913l-4.081,4.081C7.299,26.982,6.379,26.292,5.542,25.459zM8.268,3.435l4.082,4.082C11.288,7.935,10.29,8.571,9.43,9.43c-0.858,0.859-1.494,1.855-1.912,2.918L3.436,8.267c0.58-0.969,1.271-1.89,2.105-2.727C6.377,4.707,7.299,4.016,8.268,3.435zM22.732,27.563l-4.082-4.082c1.062-0.418,2.061-1.053,2.919-1.912c0.859-0.859,1.495-1.857,1.913-2.92l4.082,4.082c-0.58,0.969-1.271,1.891-2.105,2.728C24.623,26.292,23.701,26.983,22.732,27.563z' graph.addEdge({ source: { x: 160, y: 40 }, target: { x: 420, y: 40 }, attrs: { line: { stroke: '#31d0c6', sourceMarker: { d, tagName: 'path', }, targetMarker: { tagName: 'path', d: Util.normalizeMarker(d), }, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Util } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const d = 'M4.834,4.834L4.833,4.833c-5.889,5.892-5.89,15.443,0.001,21.334s15.44,5.888,21.33-0.002c5.891-5.891,5.893-15.44,0.002-21.33C20.275-1.056,10.725-1.056,4.834,4.834zM25.459,5.542c0.833,0.836,1.523,1.757,2.104,2.726l-4.08,4.08c-0.418-1.062-1.053-2.06-1.912-2.918c-0.859-0.859-1.857-1.494-2.92-1.913l4.08-4.08C23.7,4.018,24.622,4.709,25.459,5.542zM10.139,20.862c-2.958-2.968-2.959-7.758-0.001-10.725c2.966-2.957,7.756-2.957,10.725,0c2.954,2.965,2.955,7.757-0.001,10.724C17.896,23.819,13.104,23.817,10.139,20.862zM5.542,25.459c-0.833-0.837-1.524-1.759-2.105-2.728l4.081-4.081c0.418,1.063,1.055,2.06,1.914,2.919c0.858,0.859,1.855,1.494,2.917,1.913l-4.081,4.081C7.299,26.982,6.379,26.292,5.542,25.459zM8.268,3.435l4.082,4.082C11.288,7.935,10.29,8.571,9.43,9.43c-0.858,0.859-1.494,1.855-1.912,2.918L3.436,8.267c0.58-0.969,1.271-1.89,2.105-2.727C6.377,4.707,7.299,4.016,8.268,3.435zM22.732,27.563l-4.082-4.082c1.062-0.418,2.061-1.053,2.919-1.912c0.859-0.859,1.495-1.857,1.913-2.92l4.082,4.082c-0.58,0.969-1.271,1.891-2.105,2.728C24.623,26.292,23.701,26.983,22.732,27.563z' graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 320, y: 40 }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, sourceMarker: { d, tagName: 'path', }, targetMarker: { tagName: 'path', d: Util.normalizeMarker(d), }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 除了 `` 我们还可以使用 ``、``、``、``、``、`` 等元素来定义箭头,只需要通过 `tagName` 指定标签名,并设置元素其他必要属性。例如,使用图片来定制箭头也很简单,首先我们把 `tagName` 设置为 `image`,并通过 `xlink:href` 属性指定图片的 URL,并通过 `width` 和 `height` 属性指定图片的大小,然后调节 `y` 属性来使图片居中对齐。 ```ts edge.attr({ line: { sourceMarker: { tagName: 'image', 'xlink:href': 'http://cdn3.iconfinder.com/data/icons/49handdrawing/24x24/left.png', width: 24, height: 24, y: -12, }, targetMarker: { tagName: 'image', 'xlink:href': 'http://cdn3.iconfinder.com/data/icons/49handdrawing/24x24/left.png', width: 24, height: 24, y: -12, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const marker = { tagName: 'image', // https://www.iconfinder.com/icons/15539/arrow_left_previos_icon 'xlink:href': 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFMklEQVRYR+2WaWxUVRiGnzt3lntnptPOdGba6ZRS7ALUsFY2g7SyhKWA0ALKHrBgWBRqQioQKJZdVpHdH5jKokGBgopoBCpbIkvYJIoIspUudGEYmHY6c8eMLCHIojVATLjJ+XfP9z75znve7wg84094xvo8B/jfd8AsQZMq2AP4a+On/9KBZqmyvPpnj+enUhhTG/HgnloB6GFU/8YJM40Gg/mjA0dHK7DiaQHEN0CYkzmke8ao+SOYmTk/MGvrj12A7540gDEE+neJi80ZO/0tZ7v+rwOVZMSkuTddvNIKOPWkAV4a1jBu/3sLummi6xmpqizh/O8VZI3e8UeB60ZroPhJAOiA6juFW5vlrV99k9lDY5BAFDm2+wyjx205dNLnG2AymRJdLtevwJl7QGQguEyACqgCKgDPvbAPMqEzCkY6NdpOB2u8fYHLgKZnXJ19+Wc+bQFBLpk9679k8rsfX7e+3MZrMBrD9+Xnr/W4XMcjjVJilD08tq4zMiY03GTTaWVtQPELJUVlyuWzhZeOFhYVXERZDRy5/xY0jYIBHRvEDewz/NWoUJuRqZPW7T10pXRzS4u5w8ylmd3a9G8D1X7Qacib9AknTonkfr6OJYsXUT/iJimpzTHHRgABwAWuIryuMnzVHnw1ApWlHvZvP8GGlQWVWyuuDQG2CREwPNFoHNa1S6OWKV2StInJDkJC/Cg1XorOlXPxTCUxDevgiLdzo+IqNdfKELwlrFpwmMjkMYyYNY0ZM6bx5tAEHFFq3L8c4dql01Rdr0AjmZBCLaj1JrSGUDSSjE6vpiD/HJPHrT27z+NuFTyCVxJFMXvMoLZpaekRhJo8uCt9+Lw1CHjRaDQoiPj8IKol1JIOQ5jEqnlHCU3sxdD3c8jJzmJAiwtY9T4EKQSjIw5DZCxanRFkAyjgdRXjdZVjtEaD3kBWxyks3nui310PqGB8rzrWKeOyUy3J7euhlqz4FQMBRSEQ8COogj4S8N6oxO8+zcoPjqB/oTudR2WyYtFcsnpL2BwOVDrDrXjz3+piTc1NqsqKCfi9mOomIVscqIUq5o1dw8SNB7LuN2GjeEHMGdYzKSN9UALRCTEIchwIOqrdZfg9blQ6CcF3gSW5uxCdnWiakc6OTWvITtdhDNHj94sEVEGjiqAIBEQBtU6PWjbi93sRUOG7fp4lU75m5s6zfwO4Y8zBaZGm3IG9bHWTW8iExaWgtTVBo9WhUmtwnd7C0tk7uWpuT6u+vfn+szWBpOqTQp1oPXaHCZvDhMlqRh9mxRBuRyOF4PepUNChxsPpfRtZNPs31l9yjX3ULIirhzBncIq9z2v9HMQmp2KMCaauSNnxPJbN2Myuy9HlSZ1TLMcOHjp78PDhhWEQHymKjZ2y2CDSoolwOnRiRIQOq02L3WnBEWPD5ykl/4vz7D+s1Pzgrmz/uGGk0sDbfeuZc9/IkEzNU1rjbDsV9+UCFr+Ty/Sd5XMFWS4ON5uTCwsLB9++f1rACsSroGkYNI4U1UkOvTYxVBYssloreKq07C0vziuBkY8DuBNa7doapeVDu6pf7NivDbb6HViW/SFTtl+Z44WJt9PuJuB7SCTrATvQUA/NvODzQR5Q9E8BgnWj6wvi8mGdQnr0Tjfw7Q6FCZuvzPfChNrOgdq8B3QWmD0+1ZJlCtEwaVvxspsw9mkC/KUlw/jeDvvC3cVXNxQqysCnDhAUlGCkXaVqcUFRRj3i7B/L9m888KBiDqAM8D5W6SE//FeA2ure3fcc4HkH/gSNF7Qt0mUkowAAAABJRU5ErkJggg==', width: 32, height: 32, x: -16, y: -16, } const edge = graph.addEdge({ source: [228.84550125020417, 100.76702664502545], target: [416.2834258874138, 72.03741369165368], vertices: [{ x: 300, y: 150 }], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, sourceMarker: { ...marker }, targetMarker: { ...marker }, }, }, }) function hourHand() { const originX = 300 const originY = 150 const radius = 140 / 1.618 const steps = 60 const offset = (10 + 9.36 / 60) / 12 const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = 2 * Math.PI * (i / steps + offset) - Math.PI / 2 xs.push(originX + radius * Math.cos(theta)) ys.push(originY + radius * Math.sin(theta)) } edge.animate( { 'source/x': xs, 'source/y': ys, }, { delay: 1000, duration: 19000, easing: 'linear', fill: 'none', }, ) } function minuteHand() { const originX = 300 const originY = 150 const radius = 140 const steps = 60 const offset = 9.36 / 60 const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = 2 * Math.PI * (i / steps + offset) - Math.PI / 2 xs.push(originX + radius * Math.cos(theta)) ys.push(originY + radius * Math.sin(theta)) } edge.animate( { 'target/x': xs, 'target/y': ys, }, { delay: 1000, duration: 19000 / 12, iterations: 12, easing: 'linear', fill: 'none', }, ) } let currentAnimations = 0 function run() { currentAnimations = 2 hourHand() minuteHand() } run() edge.on('animation:finish', () => { currentAnimations -= 1 if (currentAnimations === 0) { run() } }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 需要注意的是,当使用 `` 和 `` 来定制箭头时,可以通过设置其 `cx` 属性为对应的轴半径,以避免箭头溢出边的边界;其他元素可以通过 `y` 属性来调节箭头的垂直位置,以便使其垂直居中。 ```ts edge.attr({ line: { sourceMarker: { tagName: 'ellipse', rx: 20, ry: 10, cx: 20, fill: 'rgba(255,0,0,0.3)', }, targetMarker: { tagName: 'circle', r: 12, cx: 12, fill: 'rgba(0,255,0,0.3)', }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ source: [228.84550125020417, 100.76702664502545], target: [416.2834258874138, 72.03741369165368], vertices: [{ x: 300, y: 150 }], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, sourceMarker: { tagName: 'ellipse', rx: 20, ry: 10, cx: 20, fill: 'rgba(255,0,0,0.3)', }, targetMarker: { tagName: 'circle', r: 12, cx: 12, fill: 'rgba(0,255,0,0.3)', }, }, }, }) function hourHand() { const originX = 300 const originY = 150 const radius = 140 / 1.618 const steps = 60 const offset = (10 + 9.36 / 60) / 12 const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = 2 * Math.PI * (i / steps + offset) - Math.PI / 2 xs.push(originX + radius * Math.cos(theta)) ys.push(originY + radius * Math.sin(theta)) } edge.animate( { 'source/x': xs, 'source/y': ys, }, { delay: 1000, duration: 19000, easing: 'linear', fill: 'none', }, ) } function minuteHand() { const originX = 300 const originY = 150 const radius = 140 const steps = 60 const offset = 9.36 / 60 const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = 2 * Math.PI * (i / steps + offset) - Math.PI / 2 xs.push(originX + radius * Math.cos(theta)) ys.push(originY + radius * Math.sin(theta)) } edge.animate( { 'target/x': xs, 'target/y': ys, }, { delay: 1000, duration: 19000 / 12, iterations: 12, easing: 'linear', fill: 'none', }, ) } let currentAnimations = 0 function run() { currentAnimations = 2 hourHand() minuteHand() } run() edge.on('animation:finish', () => { currentAnimations -= 1 if (currentAnimations === 0) { run() } }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 注册箭头 注册是指将生成箭头的方法注册到 X6 体系中,该方法我们称为箭头工厂方法,注册后就可以像使用内置箭头那样来使用箭头。 那么什么时候需要注册箭头呢? - 经典箭头的统一抽象,并提取出关键参数,通过参数来影响箭头的渲染,实现多场景复用。如上面介绍的[内置箭头](#内置箭头)。 - 复杂箭头的统一定义,实现一次定义多处复用,如具有复杂属性和样式配置的箭头。 - 通过语义化的箭头名和参数名,增强代码的可读性。例如我们可以将 `` 箭头的 `xlink:href` 属性使用 `'imageUrl'` 参数名代替,使其更具有语义性。 - 等等... 继续之前,我们先看看箭头工厂方法的定义。 ```ts type Factory = (args: T) => Result type Result = Attr.SimpleAttrs & { tagName?: string children?: Result[] } ``` 箭头工厂方法只有一个参数 `args`,在配置箭头时传入。例如: ```ts edge.attr({ line: { sourceMarker: 'block', targetMarker: { name: 'ellipse', rx: 10, ry: 6, }, }, }) ``` 上面的配置被解析后,分别得到起始和终止箭头的箭头名称和箭头参数。 | 箭头类型 | 箭头名称 name | 箭头参数 args | |--------------|---------------|-------------------| | sourceMarker | block | { } | | targetMarker | ellipse | { rx: 10, ry: 6 } | 在 X6 内部我们通过箭头名称找到对应的工厂方法,并根据提供参数 `args` 调用该方法,然后返回调用结果。结果 `Result` 的结构与上面介绍的自定义箭头的结构一样,通过 `tagName` 来指定使用哪种 SVG 元素渲染箭头,剩余的键值对则作为该元素的属性附加到元素上,并支持通过 `children` 来实现元素嵌套。 了解清楚工厂方法的原理后,我们就可以通过 Graph 提供的静态方法 `registerMarker` 来注册工厂方法。 ``` Graph.registerMarker(name: string, factory: Factory, overwrite?: boolean) ``` | 参数名 | 参数类型 | 默认值 | 说明 | |-----------|----------|--------|-----------------------------------------------------------| | name | String | | 箭头名。 | | factory | Factory | | 箭头工厂方法。 | | overwrite | Boolean | false | 遇到重名时是否覆盖旧工厂方法,设置为 `true` 时覆盖,否则报错。 | 最后,我们来注册一个 image 箭头。 ```ts /** * 参数定义 */ interface ImageMarkerArgs extends Attr.SimpleAttrs { imageUrl: string imageWidth?: number imageHeight?: number } Graph.registerMarker('image', (args: ImageMarkerArgs) => { const { imageUrl, imageWidth, imageHeight, ...attrs } = args return { ...attrs, // 原样返回非特殊涵义的参数 tagName: 'image', // 使用 标签渲染箭头,其余键值对都将作为该元素的属性。 width: imageWidth, height: imageHeight, 'xlink:href': imageUrl, } }) ``` 注册之后,我们可以这样来使用 image 箭头。 ```ts edge.attr({ line: { sourceMarker: { name: 'image', imageUrl: 'http://cdn3.iconfinder.com/data/icons/49handdrawing/24x24/left.png', imageWidth: 24, imageHeight: 24, y: -12, }, targetMarker: { name: 'image', imageUrl: 'http://cdn3.iconfinder.com/data/icons/49handdrawing/24x24/left.png', imageWidth: 24, imageHeight: 24, y: -12, }, }, }) ``` ### docs/demos/api/marker/native/index.less ```less .marker-native-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/marker/native/settings.tsx ```tsx import React from 'react' import { Input, Switch, Checkbox, Select, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { type: string r?: number rx?: number ry?: number width?: number height?: number offset?: number custom?: boolean fill?: string stroke?: string open?: boolean flip?: boolean strokeWidth?: number } export class Settings extends React.Component { state: State = { type: 'block', r: 5, rx: 5, ry: 5, width: 10, height: 10, offset: 0, custom: false, strokeWidth: 1, } notifyChange() { const { custom, type, r, rx, ry, width, height, offset, open, flip, ...others } = this.state if (others.stroke == null) { delete others.strokeWidth } let state: State if (type === 'block') { state = { type, width, height, offset, open, ...others, } } else if (type === 'classic' || type === 'diamond' || type === 'cross') { state = { type, width, height, offset, ...others, } } else if (type === 'async') { state = { type, width, height, offset, open, flip, ...others, } } else if (type === 'circle' || type === 'circlePlus') { state = { type, r, ...others, } } else if (type === 'ellipse') { state = { type, rx, ry, ...others, } } this.props.onChange(state!) } onTypeChanged = (type: string) => { this.setState({ type }, () => { this.notifyChange() }) } onRChanged = (r: number) => { this.setState({ r }, () => { this.notifyChange() }) } onRxChanged = (rx: number) => { this.setState({ rx }, () => { this.notifyChange() }) } onRyChanged = (ry: number) => { this.setState({ ry }, () => { this.notifyChange() }) } onWidthChanged = (width: number) => { this.setState({ width }, () => { this.notifyChange() }) } onHeightChanged = (height: number) => { this.setState({ height }, () => { this.notifyChange() }) } onOffsetChanged = (offset: number) => { this.setState({ offset }, () => { this.notifyChange() }) } onCustomChanged = (e: any) => { this.setState({ custom: e.target.checked }, () => { this.notifyChange() }) } onFillChanged = (e: any) => { this.setState({ fill: e.target.value }, () => { this.notifyChange() }) } onStrokeChanged = (e: any) => { this.setState({ stroke: e.target.value }, () => { this.notifyChange() }) } onStrokeWidthChanged = (strokeWidth: number) => { this.setState({ strokeWidth }, () => { this.notifyChange() }) } onOpenChanged = (open: boolean) => { this.setState({ open }, () => { this.notifyChange() }) } onFlipChanged = (flip: boolean) => { this.setState({ flip }, () => { this.notifyChange() }) } render() { const type = this.state.type return ( Type {(type === 'circle' || type === 'circlePlus') && ( Radius
{this.state.r}
)} {type === 'ellipse' && ( Radius X
{this.state.rx}
Radius Y
{this.state.ry}
)} {(type === 'block' || type === 'classic' || type === 'diamond' || type === 'async' || type === 'cross') && ( Width
{this.state.width}
Height
{this.state.height}
Offset
{this.state.offset}
)} {(type === 'block' || type === 'async') && ( Open )} {type === 'async' && ( Flip )}
Custom fill and stroke color
{this.state.custom && ( Fill Color Stroke Color Stroke Width
{this.state.strokeWidth}
)}
) } } ``` ### docs/demos/api/marker/path/index.less ```less .marker-path-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 80px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/marker/custom/index.less ```less .marker-custom-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/marker/normalize-path/index.less ```less .marker-normalize-path-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 100px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/marker/image/index.less ```less .marker-image-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/marker/tagname/index.less ```less .marker-tagname-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### 边上工具 - Canonical page: https://x6.antv.antgroup.com/zh/api/registry/edge-tool/ - Markdown source: https://x6.antv.antgroup.com/zh/api/registry/edge-tool.md - Description: 边上工具 --- title: "边上工具" description: "边上工具" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/registry/edge-tool/" version: "3.x" --- 由于边通常是一个具有较小宽度的折线/曲线,就不太方便与边进行交互。为了解决这个问题,我们一方面在渲染边的同时还渲染了一个与边同形状的 `` 路径,但宽度更宽且透明的边,来方便用户与边进行交互;另外,我们也可以为边添加一些小工具来增强边的交互能力,如路径点小工具使路径点可以被移动、线段小工具使边中的线段可以被移动等。 场景一:添加指定的小工具。 ```ts // 创建边时添加小工具 graph.addEdge({ source, target, tools: [ { name: 'vertices' }, { name: 'button-remove', args: { distance: 20 }, }, ], }) // 创建边后添加小工具 edge.addTools([ { name: 'vertices' }, { name: 'button-remove', args: { distance: 20 }, }, ]) ``` 场景二:鼠标动态添加/删除小工具。 ```ts graph.on('edge:mouseenter', ({ cell }) => { cell.addTools([ { name: 'vertices' }, { name: 'button-remove', args: { distance: 20 }, }, ]) }) graph.on('edge:mouseleave', ({ cell }) => { if (cell.hasTool('button-remove')) { cell.removeTool('button-remove') } }) ``` 在 X6 中默认提供了以下几个用于边的小工具: - [vertices](#vertices) 路径点工具,在路径点位置渲染一个小圆点,拖动小圆点修改路径点位置,双击小圆点删除路径点,在边上单击添加路径点。 - [segments](#segments) 线段工具。在边的每条线段的中心渲染一个工具条,可以拖动工具条调整线段两端的路径点的位置。 - [boundary](#boundary) 根据边的包围盒渲染一个包围边的矩形。注意,该工具仅仅渲染一个矩形,不带任何交互。 - [button](#button) 在指定位置处渲染一个按钮,支持自定义按钮的点击交互。 - [button-remove](#button-remove) 在指定的位置处,渲染一个删除按钮,点击时删除对应的边。 - [source-arrowhead-和-target-arrowhead](#source-arrowhead-和-target-arrowhead) 在边的起点或终点渲染一个图形(默认是箭头),拖动该图形来修改边的起点或终点。 - [edge-editor](#edge-editor) 提供边上文本编辑功能。 ## 内置工具 ### vertices 路径点工具,在路径点位置渲染一个小圆点,拖动小圆点修改路径点位置,双击小圆点删除路径点,在边上单击添加路径点。配置如下: | 参数名 | 类型 | 默认值 | 说明 | |--------------------|---------------------------|----------|----------------------------------------------------------------------------------------------------------------------| | attrs | KeyValue | `object` | 小圆点的属性。 | | snapRadius | number | `20` | 移动路径点过程中的吸附半径。当路径点与邻近的路径点距离在半径范围内时,将当前路径点吸附到临近路径点。 | | addable | boolean | `true` | 在边上按下鼠标时,是否可以添加新的路径点。 | | removable | boolean | `true` | 是否可以通过双击移除路径点。 | | removeRedundancies | boolean | `true` | 是否自动移除冗余的路径点。 | | stopPropagation | boolean | `true` | 是否阻止工具上的鼠标事件冒泡到边视图上。阻止后鼠标与工具交互时将不会触发边的 `mousedown`、`mousemove` 和 `mouseup` 事件。 | | modifiers | `string \| ModifierKey[]` | - | 按下对应的键盘按键后才能添加路径点,可以解决添加路径点和边点击两种交互重叠现象。 | 其中 `attrs` 的默认值(默认样式)为: ```ts { r: 6, fill: '#333', stroke: '#fff', cursor: 'move', 'stroke-width': 2, } ``` 工具使用方式如下: ```ts // 创建 edge 时添加小工具 const edge1 = graph.addEdge({ ..., tools: [ { name: 'vertices', args: { attrs: { fill: '#666' }, }, }, ] }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, tools: [ { name: 'vertices', args: { attrs: { fill: '#8f8f8f' }, }, }, ], }) const edge2 = graph.addEdge({ source: { x: 40, y: 160 }, target: { x: 380, y: 160 }, vertices: [ { x: 40, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 160 }, ], connector: 'smooth', attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, }) graph.on('edge:mouseenter', ({ cell }) => { if (cell === edge2) { cell.addTools({ name: 'vertices', args: { attrs: { fill: '#8f8f8f' }, }, }) } }) graph.on('edge:mouseleave', ({ cell }) => { if (cell === edge2) { if (cell.hasTool('vertices')) { cell.removeTool('vertices') } } }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### segments 线段工具。在边的每条线段的中心渲染一个工具条,可以拖动工具条调整线段两端的路径点的位置。配置如下: | 参数名 | 类型 | 默认值 | 说明 | |--------------------|---------|----------|----------------------------------------------------------------------------------------------------------------------| | attrs | object | `object` | 元素的属性。 | | precision | number | `0.5` | 线段的两个端点的 X 或 Y 轴的坐标差小于 `precision` 时才渲染工具,默认 `0.5` 表示只对垂直和水平线段渲染工具。 | | threshold | number | `40` | 线段长度超过 `threshold` 时才渲染工具。 | | snapRadius | number | `10` | 调整线段过程中的吸附半径。 | | removeRedundancies | boolean | `true` | 是否自动移除冗余的路径点。 | | stopPropagation | boolean | `true` | 是否阻止工具上的鼠标事件冒泡到边视图上。阻止后鼠标与工具交互时将不会触发边的 `mousedown`、`mousemove` 和 `mouseup` 事件。 | 其中 `attrs` 的默认值(默认样式)为: ```ts { width: 20, height: 8, x: -10, y: -4, rx: 4, ry: 4, fill: '#333', stroke: '#fff', 'stroke-width': 2, } ``` 工具使用方式如下: ```ts graph.addEdge({ ..., tools: [{ name: 'segments', args: { snapRadius: 20, attrs: { fill: '#444', }, }, }] }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, tools: [ { name: 'segments', args: { snapRadius: 20, attrs: { fill: '#8f8f8f', }, }, }, ], }) graph.addEdge({ source: { x: 40, y: 160 }, target: { x: 380, y: 160 }, vertices: [ { x: 40, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 160 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, connector: 'smooth', tools: [ { name: 'segments', args: { snapRadius: 20, attrs: { fill: '#8f8f8f', }, }, }, ], }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### boundary 根据边的包围盒渲染一个包围边的矩形。注意,该工具仅仅渲染一个矩形,不带任何交互。配置如下: | 参数名 | 类型 | 默认值 | 说明 | |-----------------|----------|----------|--------------------------------------------------------------------------------------------------------| | tagName | string | `rect` | 使用何种图形渲染。 | | padding | number | `10` | 边距。 | | attrs | KeyValue | `object` | 图形属性。 | | useCellGeometry | boolean | `true` | 是否使用几何计算的方式来计算元素包围盒,开启后会有性能上的提升,如果出现计算准度问题,请将它设置为 `false`。 | 其中 `attrs` 的默认值(默认样式)为: ```js { fill: 'none', stroke: '#333', 'stroke-width': 0.5, 'stroke-dasharray': '5, 5', 'pointer-events': 'none', } ``` 工具使用方式如下: ```ts graph.addEdge({ ..., tools: [ { name: 'boundary', args: { padding: 5, attrs: { fill: '#7c68fc', stroke: '#333', 'stroke-width': 0.5, 'fill-opacity': 0.2, }, }, }, ] }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, tools: ['boundary'], }) graph.addEdge({ source: { x: 40, y: 160 }, target: { x: 380, y: 160 }, vertices: [ { x: 40, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 160 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, connector: 'smooth', tools: [ { name: 'boundary', args: { padding: 5, useCellGeometry: false, attrs: { fill: '#7c68fc', stroke: '#333', strokeWidth: 0.5, fillOpacity: 0.2, }, }, }, ], }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### button 在指定位置处渲染一个按钮,支持自定义按钮的点击交互。配置如下: | 参数名 | 类型 | 默认值 | 说明 | |----------|------------------------------------------------------------------------|-------------|-----------------------------| | distance | number \| string | `undefined` | 偏离起点的距离或比例。 | | offset | number \| Point.PointLike | `0` | 在 `distance` 基础上的偏移量。 | | rotate | boolean | `undefined` | 是否跟随边旋转。 | | markup | Markup.JSONMarkup | `undefined` | 渲染按钮的 Markup 定义。 | | onClick | `(args: {e: Dom.MouseDownEvent, cell: Cell, view: CellView }) => void` | `undefined` | 点击按钮的回调函数。 | 使用方式如下: ```ts graph.addEdge({ ..., tools: [ { name: 'button', args: { distance: -40, onClick({ view }: any) { // }, }, }, ], }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, EdgeView } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, tools: [ { name: 'button', args: { markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 18, stroke: '#fe854f', strokeWidth: 2, fill: 'white', cursor: 'pointer', }, }, { tagName: 'text', textContent: 'Btn B', selector: 'icon', attrs: { fill: '#fe854f', fontSize: 10, textAnchor: 'middle', pointerEvents: 'none', y: '0.3em', }, }, ], distance: -40, onClick({ view }: { view: EdgeView }) { const edge = view.cell const source = edge.getSource() const target = edge.getTarget() edge.setSource(target) edge.setTarget(source) }, }, }, ], }) graph.addEdge({ source: { x: 40, y: 160 }, target: { x: 380, y: 160 }, vertices: [ { x: 40, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 160 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, connector: 'smooth', tools: { name: 'button', args: { markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 18, stroke: '#fe854f', strokeWidth: 2, fill: 'white', cursor: 'pointer', }, }, { tagName: 'text', textContent: 'Btn A', selector: 'icon', attrs: { fill: '#fe854f', fontSize: 10, textAnchor: 'middle', pointerEvents: 'none', y: '0.3em', }, }, ], distance: -100, offset: { x: 0, y: 0 }, onClick({ view }: { view: EdgeView }) { const edge = view.cell const offset = parseInt( edge.attr('line/strokeDashoffset') || '0', 10, ) edge.attr({ line: { strokeDasharray: '5, 10', strokeDashoffset: offset + 20, }, }) }, }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### button-remove 在指定的位置处,渲染一个删除按钮,点击时删除对应的边。它是上面 `button` 工具的一个特例,所以支持 `button` 的所有配置。使用方式如下: ```ts graph.addEdge({ ..., tools: [ { name: 'button-remove', args: { distance: -40 }, }, ] }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, tools: [ { name: 'button-remove', args: { distance: -40 }, }, ], }) graph.addEdge({ source: { x: 40, y: 160 }, target: { x: 380, y: 160 }, vertices: [ { x: 40, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 160 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, connector: 'smooth', tools: [ { name: 'button-remove', args: { distance: -40 }, }, ], }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### source-arrowhead 和 target-arrowhead 在边的起点或终点渲染一个图形(默认是箭头),拖动该图形来修改边的起点或终点。配置如下: | 参数名 | 类型 | 默认值 | 说明 | |---------|------------------|----------|-------------------| | tagName | string | `path` | 使用何种图形来渲染。 | | attrs | Attr.SimpleAttrs | `object` | 图形的属性。 | 其中 `source-arrowhead` 的属性默认值为 ```ts { d: 'M 10 -8 -10 0 10 8 Z', fill: '#333', stroke: '#fff', 'stroke-width': 2, cursor: 'move', } ``` `target-arrowhead` 的属性默认值为 ```ts { d: 'M -10 -8 10 0 -10 8 Z', fill: '#333', stroke: '#fff', 'stroke-width': 2, cursor: 'move', } ``` 工具使用方式如下: ```ts graph.on('edge:mouseenter', ({ cell }) => { cell.addTools([ 'source-arrowhead', { name: 'target-arrowhead', args: { attrs: { fill: 'red', }, }, }, ]) }) graph.on('edge:mouseleave', ({ cell }) => { cell.removeTools() }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node-with-port', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { in: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, out: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const magnetAvailabilityHighlighter = { name: 'stroke', args: { padding: 3, attrs: { strokeWidth: 3, stroke: '#52c41a', }, }, } const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, highlighting: { magnetAvailable: magnetAvailabilityHighlighter, }, connecting: { snap: true, allowBlank: false, allowLoop: false, allowNode: false, highlight: true, createEdge() { return this.createEdge({ attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) }, validateMagnet({ magnet }) { return magnet.getAttribute('port-group') !== 'in' }, validateConnection({ sourceMagnet, targetMagnet }) { // 只能从输出链接桩创建连接 if ( !sourceMagnet || sourceMagnet.getAttribute('port-group') === 'in' ) { return false } // 只能连接到输入链接桩 if ( !targetMagnet || targetMagnet.getAttribute('port-group') !== 'in' ) { return false } return true }, }, }) const source = graph.addNode({ shape: 'custom-node-with-port', x: 40, y: 40, width: 100, height: 40, label: 'source', ports: [ { id: 'in-1', group: 'in' }, { id: 'in-2', group: 'in' }, { id: 'out-1', group: 'out' }, { id: 'out-2', group: 'out' }, ], }) const target = graph.addNode({ shape: 'custom-node-with-port', x: 140, y: 240, width: 100, height: 40, label: 'target', ports: [ { id: 'in-1', group: 'in' }, { id: 'in-2', group: 'in' }, { id: 'out-1', group: 'out' }, { id: 'out-2', group: 'out' }, ], }) graph.addNode({ shape: 'custom-node-with-port', x: 320, y: 120, width: 100, height: 40, label: 'hello', ports: [ { id: 'in-1', group: 'in' }, { id: 'in-2', group: 'in' }, { id: 'out-1', group: 'out' }, { id: 'out-2', group: 'out' }, ], }) graph.addEdge({ source: { cell: source.id, port: 'out-2' }, target: { cell: target.id, port: 'in-1' }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.on('edge:mouseenter', ({ cell }) => { cell.addTools([ { name: 'source-arrowhead', }, { name: 'target-arrowhead', args: { attrs: { fill: 'red', }, }, }, ]) }) graph.on('edge:mouseleave', ({ cell }) => { cell.removeTools() }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### edge-editor 提供边上文本编辑功能。配置如下: | 参数名 | 类型 | 默认值 | 说明 | |-----------------------|-------------------------------------------------------------------------|--------------------------------|--------------------------------------------------------------| | labelAddable | boolean | true | 点击非文本位置是否新建 label | | attrs/fontSize | string | `14` | 编辑文本字体大小 | | attrs/color | string | `#000` | 编辑文本字体颜色 | | attrs/fontFamily | string | `Arial, helvetica, sans-serif` | 编辑文本的字体 | | attrs/backgroundColor | string | `#fff` | 编辑区域的背景色 | | getText | string \| `(this: CellView, args: {cell: Cell}) => string` | - | 获取原文本方法,在自定义 `markup` 场景需要自定义 `getText` 方法 | | setText | string \| `(this: CellView, args: {cell: Cell, value: string}) => void` | - | 设置新文本,在自定义 `markup` 场景需要自定义 `setText` 方法 | > **注意** 需要注意的是,2.8.0 版本后不需要在双击事件中去动态添加工具,也就不需要传入事件参数。 ```ts // 2.8.0 版本之前使用方式 graph.on('node:dblclick', ({ node, e }) => { edge.addTools({ name: 'edge-editor', args: { event: e, }, }) }) // 2.8.0 版本之后使用方式 edge.addTools({ name: 'edge-editor', }) ``` 还需要注意的是,如果在边中自定义了 `markup`,往往需要自定义 `getText` 和 `setText` 方法来正确获取和设置编辑文本,这两个配置都支持函数和字符串两种形式,函数比较好理解,字符串其实就是要获取或者设置的文本的属性路径,一般情况下建议使用字符串形式,这样图数据可以完全序列化(因为函数无法序列化),否则可能会出现渲染画布后文本编辑功能异常,比如: ```typescript edge.addTools({ name: 'edge-editor', args: { getText: 'a/b', setText: 'c/d', }, }) ``` 上面配置表示: - 获取编辑文本:`edge.attr('a/b')` - 设置编辑文本:`edge.attr('c/d', value)` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, connecting: { connector: 'smooth', }, }) const source = graph.addNode({ x: 40, y: 40, width: 100, height: 40, label: 'source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, tools: ['node-editor'], }) const target = graph.addNode({ x: 180, y: 200, width: 100, height: 40, label: 'target', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, tools: ['node-editor'], }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, labels: [ { position: 0.3, attrs: { label: { text: 'edge label 1', }, }, }, { position: 0.6, attrs: { label: { text: 'edge label 2', }, }, }, ], tools: ['edge-editor'], }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 自定义工具 ### 方式一 继承 `ToolItem` 实现一个工具类,难度较高,要求对 [ToolItem](https://github.com/antvis/X6/blob/master/src/view/tool.ts) 类都有所了解,可以参考上述内置工具的源码,这里不展开叙述。 ```ts Graph.registerEdgeTool('button', Button) ``` ### 方式二 继承已经注册的工具,在继承基础上修改配置。我们在 `ToolItem` 基类上提供了一个静态方法 `define` 来快速实现继承并修改配置。 ```ts import { Vertices } from '@antv/x6/es/registry/tool/vertices' const RedVertices = Vertices.define({ attrs: { fill: 'red', }, }) Graph.registerEdgeTool('red-vertices', RedVertices, true) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerEdgeTool( 'red-vertices', { inherit: 'vertices', attrs: { fill: 'red', }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, tools: ['red-vertices'], }) const edge2 = graph.addEdge({ source: { x: 60, y: 140 }, target: { x: 380, y: 140 }, vertices: [ { x: 40, y: 180 }, { x: 200, y: 140 }, { x: 240, y: 180 }, ], connector: 'smooth', attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, }) graph.on('edge:mouseenter', ({ cell }) => { if (cell === edge2) { cell.addTools([{ name: 'red-vertices' }]) } }) graph.on('edge:mouseleave', ({ cell }) => { if (cell === edge2) { if (cell.hasTool('red-vertices')) { cell.removeTool('red-vertices') } } }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 同时,我们为 `Graph.registerEdgeTool` 方法提供了一种快速继承并指定默认选项的实现: ```ts Graph.registerEdgeTool('red-vertices', { inherit: 'vertices', // 基类名称,使用已经注册的工具名称。 attrs: { // 其他选项,作为继承的类的默认选项。 fill: 'red', }, }) ``` 通过该方法,我们可以快速定义并注册一个圆形的端点 `circle-target-arrowhead`: ```ts Graph.registerEdgeTool('circle-target-arrowhead', { inherit: 'target-arrowhead', tagName: 'circle', attrs: { r: 18, fill: '#31d0c6', 'fill-opacity': 0.3, stroke: '#fe854f', 'stroke-width': 4, cursor: 'move', }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerEdgeTool('circle-target-arrowhead', { inherit: 'target-arrowhead', tagName: 'circle', attrs: { r: 18, fill: '#31d0c6', 'fill-opacity': 0.3, stroke: '#fe854f', 'stroke-width': 4, cursor: 'move', }, }) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 320, y: 40 }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: null, }, }, tools: 'circle-target-arrowhead', }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/api/edge-tool/vertices/index.less ```less .edge-tool-vertices-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/edge-tool/segments/index.less ```less .edge-tool-segments-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/edge-tool/boundary/index.less ```less .edge-tool-boundary-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/edge-tool/button/index.less ```less .edge-tool-button-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/edge-tool/button-remove/index.less ```less .edge-tool-button-remove-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/edge-tool/arrowhead/index.less ```less .edge-tool-arrowhead-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/node-tool/node-editor/index.less ```less .node-editor-tool-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/edge-tool/custom-vertices/index.less ```less .edge-tool-custom-vertices-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/edge-tool/custom-arrowhead/index.less ```less .edge-tool-custom-arrowhead-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### 画布缩放 - Canonical page: https://x6.antv.antgroup.com/zh/api/graph/mousewheel/ - Markdown source: https://x6.antv.antgroup.com/zh/api/graph/mousewheel.md - Description: 画布缩放 --- title: "画布缩放" description: "画布缩放" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/graph/mousewheel/" version: "3.x" --- ## 演示 > 按住 `Command` 键通过滚轮缩放画布。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.initGraph({ zoomAtMousePosition: true, }) } initGraph(options: State) { if (this.graph) { this.graph.dispose() } this.graph = new Graph({ container: this.container, height: 280, background: { color: '#F2F7FA', }, mousewheel: { enabled: true, global: true, modifiers: ['ctrl', 'meta'], ...options, }, }) this.graph.addNode({ x: 200, y: 100, width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const source = this.graph.addNode({ x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 160, y: 180, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph.centerContent() } onSettingsChanged = (options: State) => { this.initGraph(options) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 配置 可以使用 `mousewheel` 配置来对画布进行缩放,经常和修饰键配合使用,使用方式如下。 ```ts const graph = new Graph({ mousewheel: { enabled: true, modifiers: ['ctrl', 'meta'], }, }) ``` 支持的选项如下: ```ts interface MouseWheelOptions { enabled?: boolean global?: boolean factor?: number zoomAtMousePosition?: boolean modifiers?: string | ('alt' | 'ctrl' | 'meta' | 'shift')[] | null guard?: (this: Graph, e: WheelEvent) => boolean } ``` ### enabled 是否开启滚轮缩放交互。 ### factor 滚动缩放因子。默认为 `1.2`。 ### zoomAtMousePosition 是否将鼠标位置作为中心缩放,默认为 `true`。 ### global 是否为全局事件,设置为 `true` 时滚轮事件绑定在 `Document` 上,否则绑定在画布容器上。默认为 `false`。 ### modifiers 修饰键(`alt`、`ctrl`、`meta`、`shift`),设置修饰键后需要按下修饰键并滚动鼠标滚轮时才触发画布缩放。通过设置修饰键可以解决默认滚动行为与画布缩放冲突问题。修饰键支持以下几种形式: - `alt` 表示按下 `alt`。 - `[alt, ctrl]` 表示按下 `alt` 或 `ctrl`。 - `alt|ctrl` 表示按下 `alt` 或 `ctrl`。 - `alt&ctrl` 表示同时按下 `alt` 和 `ctrl`。 - `alt|ctrl&shift` 表示同时按下 `alt` 和 `shift` 或者同时按下 `ctrl` 和 `shift`。 ### guard 判断一个滚轮事件是否应该被处理,返回 `false` 时对应的事件被忽略。 ```ts new Graph({ mousewheel: { enabled: true, guard(e: WheelEvent) { if (e.altKey) { // 当按下 alt 键时,忽略所有滚动事件 return false } return true }, }, }) ``` ## 方法 ### isMouseWheelEnabled() ```ts isMouseWheelEnabled(): boolean ``` 返回是否启用了鼠标滚轮来缩放画布。 ### enableMouseWheel() ```ts enableMouseWheel(): this ``` 启用鼠标滚轮缩放画布。 ### disableMouseWheel() ```ts disableMouseWheel(): this ``` 禁用鼠标滚轮缩放画布。 ### toggleMouseWheel(...) ```ts toggleMouseWheel(enabled?: boolean): this ``` 切换鼠标滚轮缩放画布的启用状态。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | enabled | boolean | | - | 是否启用鼠标滚轮缩放画布,缺省时切换鼠标滚轮缩放画布的启用状态。 | ### docs/demos/api/mousewheel/playground/index.less ```less .mousewheel-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } } ``` ### docs/demos/api/mousewheel/playground/settings.tsx ```tsx import React from 'react' import { Checkbox, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { zoomAtMousePosition: boolean } export class Settings extends React.Component { state: State = { zoomAtMousePosition: true, } notifyChange() { this.props.onChange({ ...this.state, }) } onFixedChanged = (e: any) => { this.setState({ zoomAtMousePosition: e.target.checked }, () => { this.notifyChange() }) } render() { return ( Zoom at Mouse Postion ) } } ``` #### 元素属性 - Canonical page: https://x6.antv.antgroup.com/zh/api/model/attrs/ - Markdown source: https://x6.antv.antgroup.com/zh/api/model/attrs.md - Description: 元素属性 --- title: "元素属性" description: "元素属性" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/model/attrs/" version: "3.x" --- 对于原生 SVG 属性,网上有很多教程可以参考,例如 MDN 提供的 [SVG 属性参考](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute),这里我们将更多聚焦到如何定义和使用特殊属性。特殊属性提供了比[原生 SVG 属性](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute)更加灵活和强大的功能,在应用属性时,原生属性被直接传递给对应的元素,特殊属性则被进一步加工处理,转换为浏览器认识的原生属性后,再传递给对应的元素。 ## 相对大小和相对位置 在定制节点或边时,设置元素的相对大小是一个非常常见需求,我们在 X6 中提供了一系列以 `ref` 为前缀特殊属性,可以通过这些属性来为元素设置相对大小,同时这些属性的计算都是基于节点/边的数据大小,也就是说所有计算都不依赖浏览器的 bbox 计算,所以不存在任何性能问题。 - [`refWidth`](/zh/api/registry/attr#refwidth) 和 [`refHeight`](/zh/api/registry/attr#refheight) 元素大小。 - [`refX`](/zh/api/registry/attr#refx) 和 [`refY`](/zh/api/registry/attr#refy) 元素位置。 - [`refCx`](/zh/api/registry/attr#refcx) 和 [`refCy`](/zh/api/registry/attr#refcy) 椭圆 `` 和圆 `` 中心位置。 - [`refRx`](/zh/api/registry/attr#refrx) 和 [`refRy`](/zh/api/registry/attr#refry) 椭圆 `` 半径。 - [`refR`](/zh/api/registry/attr#refr) 圆 `` 半径。 接下来我们一起来看看如何使用这些相对属性。下面案例中,我们定一个红色椭圆 `e`、一个绿色矩形 `r`、和蓝色圆形 `c` 和一个表示节点大小的矩形 `outline`。 ```ts graph.addNode({ shape: 'custom-rect', x: 160, y: 100, width: 280, height: 120, attrs: { e: { refRx: '50%', // 椭圆 x 轴半径为宽度的一半 refRy: '25%', // 椭圆 y 轴半径为高度的 1/4 refCx: '50%', // 椭圆中心 x 坐标为宽度一半,即位于节点宽度的中心 refCy: 0, // 椭圆中心 y 坐标为 0 refX: '-50%', // 向左偏移宽度一半 refY: '25%', // 向下偏移高度的 1/4 }, r: { refX: '100%', // 矩形 x 轴坐标位于节点右下角 refY: '100%', // 矩形 y 轴坐标位于节点右下角 refWidth: '50%', // 矩形宽度为节点宽的一半 refHeight: '50%', // 矩形高度为节点高度的一半 x: -10, // 向左偏移 10px y: -10, // 向上偏移 10px }, c: { refRCircumscribed: '50%', // 圆半径为节点宽度/高度中较大的那个值的一半 refCx: '50%', // 圆中心 x 坐标位于节点中心 refCy: '50%', // 圆中心 y 坐标位于节点中心 }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-rect', { markup: [ { tagName: 'ellipse', selector: 'e', }, { tagName: 'rect', selector: 'r', }, { tagName: 'circle', selector: 'c', }, { tagName: 'rect', selector: 'outline', }, ], attrs: { e: { stroke: '#8f8f8f', strokeWidth: 1, fill: 'rgba(255,0,0,0.3)', }, r: { stroke: '#8f8f8f', strokeWidth: 1, fill: 'rgba(0,255,0,0.3)', }, c: { stroke: '#8f8f8f', strokeWidth: 1, fill: 'rgba(0,0,255,0.3)', }, outline: { refX: 0, refY: 0, refWidth: '100%', refHeight: '100%', stroke: '#8f8f8f', strokeWidth: 1, strokeDasharray: '5 5', strokeDashoffset: 2.5, fill: 'none', }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, interacting: false, }) const node = graph.addNode({ shape: 'custom-rect', x: 160, y: 100, width: 280, height: 120, attrs: { e: { refRx: '50%', refRy: '25%', refCx: '50%', refCy: 0, refX: '-50%', refY: '25%', }, r: { refX: '100%', refY: '100%', refWidth: '50%', refHeight: '50%', x: -10, // additional x offset y: -10, // additional y offset }, c: { refRCircumscribed: '50%', refCx: '50%', refCy: '50%', }, }, }) let stretchToggle = false function contract() { node.animate( { 'size/width': 40, 'size/height': 40, 'position/x': 280, 'position/y': 130, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) stretchToggle = true } function stretch() { node.animate( { 'size/width': 280, 'size/height': 120, 'position/x': 160, 'position/y': 100, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) stretchToggle = false } stretch() node.on('animation:finish', () => { if (stretchToggle) { stretch() } else { contract() } }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 相对子元素 上面这些属性默认相对于节点的大小进行计算,另外我们可以通过 `ref` 属性来提供一个子元素选择器,这时所有的计算都相对于 `ref` 指代的元素,从而实现相对于子元素的大小和位置。 > **注意** 需要注意的是,设置 `ref` 后,所有计算都依赖子元素在浏览器中的 bbox 测量,所以性能会比相对于节点的方式要慢。 ```ts graph.addNode({ shape: 'custom-text', x: 320, y: 160, width: 280, height: 120, attrs: { label: { text: 'H', }, e: { ref: 'label', refRx: '50%', refRy: '25%', refCx: '50%', refCy: 0, refX: '-50%', refY: '25%', }, r: { ref: 'label', refX: '100%', refY: '100%', x: -10, y: -10, refWidth: '50%', refHeight: '50%', }, c: { ref: 'label', refRCircumscribed: '50%', }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-text', { markup: [ { tagName: 'ellipse', selector: 'e', }, { tagName: 'rect', selector: 'r', }, { tagName: 'circle', selector: 'c', }, { tagName: 'text', selector: 'label', }, { tagName: 'rect', selector: 'outline', }, ], attrs: { label: { textAnchor: 'middle', textVerticalAnchor: 'middle', fontSize: 48, }, e: { stroke: '#8f8f8f', strokeWidth: 1, fill: 'rgba(255,0,0,0.3)', }, r: { stroke: '#8f8f8f', strokeWidth: 1, fill: 'rgba(0,255,0,0.3)', }, c: { stroke: '#8f8f8f', strokeWidth: 1, fill: 'rgba(0,0,255,0.3)', }, outline: { ref: 'label', refX: 0, refY: 0, refWidth: '100%', refHeight: '100%', stroke: '#8f8f8f', strokeWidth: 1, strokeDasharray: '5 5', strokeDashoffset: 2.5, fill: 'none', }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, interacting: false, }) const node = graph.addNode({ shape: 'custom-text', x: 320, y: 160, width: 280, height: 120, attrs: { label: { text: 'H', }, e: { ref: 'label', refRx: '50%', refRy: '25%', refCx: '50%', refCy: 0, refX: '-50%', refY: '25%', }, r: { ref: 'label', refX: '100%', refY: '100%', x: -10, y: -10, refWidth: '50%', refHeight: '50%', }, c: { ref: 'label', refRCircumscribed: '50%', }, }, }) let typeToggle = true function type() { const full = 'Hello, World!' const delay = 1000 const duration = 4000 const steps = full.length - 1 setTimeout(() => { let i = 1 const step = Math.max(1, Math.floor(duration / steps)) const timer = setInterval(() => { node.attr('label/text', full.substring(0, i + 1)) i += 1 if (i >= full.length) { clearInterval(timer) } }, step) }, delay) node.animate( { 'outline/strokeDashoffset': 2.5, }, { delay, duration, easing: 'linear', fill: 'none', }, ) typeToggle = false } function untype() { const full = 'Hello, World!' const delay = 1000 const duration = 4000 const steps = full.length - 1 setTimeout(() => { let i = full.length const step = Math.max(1, Math.floor(duration / steps)) const timer = setInterval(() => { i -= 1 node.attr('label/text', full.substring(0, Math.max(1, i))) if (i <= 1) { clearInterval(timer) } }, step) }, delay) node.animate( { 'outline/strokeDashoffset': 2.5, }, { delay, duration, easing: 'linear', fill: 'none', }, ) typeToggle = true } type() node.on('animation:finish', () => { if (typeToggle) { type() } else { untype() } }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 沿边长度的相对位置 我们提供了下面几个属性来设置边以及相对于边的位置。 - [`connection`](/zh/api/registry/attr#connection) 仅适用于边的 `` 元素,当该属性为 `true` 时,表示将在该元素上渲染边。 - [`atConnectionLength`](/zh/api/registry/attr#atconnectionlengthkeepgradient) 是 [`atConnectionLengthKeepGradient`](/zh/api/registry/attr#atconnectionlengthkeepgradient) 属性的简称,表示将指定元素移动到指定的偏移量的位置处,并自动旋转元素,使其方向与所在位置边的斜率保持一致。 - [`atConnectionRatio`](/zh/api/registry/attr#atconnectionratiokeepgradient) 是 [`atConnectionRatioKeepGradient`](/zh/api/registry/attr#atconnectionratiokeepgradient) 属性的简称,表示将指定元素移动到指定比例 `[0, 1]` 位置处,并自动旋转元素,使其方向与所在位置边的斜率保持一致。 - [`atConnectionLengthIgnoreGradient`](/zh/api/registry/attr#atconnectionlengthignoregradient) 将指定元素移动到指定偏移量的位置处,忽略边的斜率,即不会跟随边自动旋转。 - [`atConnectionRatioIgnoreGradient`](/zh/api/registry/attr#atconnectionratioignoregradient),将指定元素移动到指定比例 `[0, 1]` 位置处,忽略边的斜率,即不会跟随边自动旋转。 ```ts graph.addEdge({ shape: 'custom-edge', source: { x: 100, y: 60 }, target: { x: 500, y: 60 }, vertices: [{ x: 300, y: 160 }], attrs: { symbol: { atConnectionRatio: 0.75, // 沿边长度方向,距离起点 75% 位置处 }, arrowhead: { atConnectionLength: 100, // 沿边长度方向,距离起点 100px 位置处 }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerEdge( 'custom-edge', { markup: [ { tagName: 'path', selector: 'line', }, { tagName: 'path', selector: 'arrowhead', }, { tagName: 'path', selector: 'symbol', }, ], attrs: { line: { connection: true, fill: 'none', stroke: '#8f8f8f', strokeWidth: 1, sourceMarker: { tagName: 'circle', r: 4, fill: 'white', stroke: '#8f8f8f', strokeWidth: 2, }, targetMarker: { tagName: 'circle', r: 4, fill: 'white', stroke: '#8f8f8f', strokeWidth: 2, }, }, arrowhead: { d: 'M -20 -10 0 0 -20 10 Z', fill: '#8f8f8f', stroke: 'none', }, symbol: { d: 'M -20 -20 20 20', stroke: '#8f8f8f', targetMarker: { type: 'path', d: 'M 0 0 10 -5 10 5 Z', fill: '#8f8f8f', stroke: 'none', }, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, interacting: false, }) const edge = graph.addEdge({ shape: 'custom-edge', source: { x: 100, y: 60 }, target: { x: 500, y: 60 }, vertices: [{ x: 300, y: 160 }], attrs: { symbol: { atConnectionRatio: 0.75, }, arrowhead: { atConnectionLength: 100, }, }, }) let oscillateToggle = false function contract() { edge.animate( { 'source/x': 200, 'source/y': 100, 'target/x': 400, 'target/y': 100, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = true } function oscillate() { edge.animate( { 'source/x': 100, 'source/y': 200, 'target/x': 500, 'target/y': 200, 'vertices/0/x': 300, 'vertices/0/y': 60, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = false } contract() edge.on('animation:finish', () => { if (oscillateToggle) { oscillate() } else { contract() } }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ```ts graph.addEdge({ shape: 'custom-edge', source: { x: 100, y: 60 }, target: { x: 500, y: 60 }, vertices: [{ x: 300, y: 160 }], attrs: { relativeLabel: { text: '0.25', atConnectionRatio: 0.25, }, relativeLabelBody: { atConnectionRatio: 0.25, }, absoluteLabel: { text: '150', atConnectionLength: 150, }, absoluteLabelBody: { atConnectionLength: 150, }, absoluteReverseLabel: { text: '-100', atConnectionLength: -100, }, absoluteReverseLabelBody: { atConnectionLength: -100, }, offsetLabelPositive: { y: 40, text: 'keepGradient: 0,40', atConnectionRatio: 0.66, }, offsetLabelPositiveBody: { x: -60, // 0 + -60 y: 30, // 40 + -10 atConnectionRatio: 0.66, }, offsetLabelNegative: { y: -40, text: 'keepGradient: 0,-40', atConnectionRatio: 0.66, }, offsetLabelNegativeBody: { x: -60, // 0 + -60 y: -50, // -40 + -10 atConnectionRatio: 0.66, }, offsetLabelAbsolute: { x: -40, y: 80, text: 'ignoreGradient: -40,80', atConnectionRatioIgnoreGradient: 0.66, }, offsetLabelAbsoluteBody: { x: -110, // -40 + -70 y: 70, // 80 + -10 atConnectionRatioIgnoreGradient: 0.66, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerEdge( 'custom-edge', { markup: [ { tagName: 'path', selector: 'line', }, { tagName: 'path', selector: 'offsetLabelPositiveConnector', }, { tagName: 'path', selector: 'offsetLabelNegativeConnector', }, { tagName: 'path', selector: 'offsetLabelAbsoluteConnector', }, { tagName: 'text', selector: 'offsetLabelMarker', }, { tagName: 'rect', selector: 'relativeLabelBody', }, { tagName: 'text', selector: 'relativeLabel', }, { tagName: 'rect', selector: 'absoluteLabelBody', }, { tagName: 'text', selector: 'absoluteLabel', }, { tagName: 'rect', selector: 'absoluteReverseLabelBody', }, { tagName: 'text', selector: 'absoluteReverseLabel', }, { tagName: 'rect', selector: 'offsetLabelPositiveBody', }, { tagName: 'text', selector: 'offsetLabelPositive', }, { tagName: 'rect', selector: 'offsetLabelNegativeBody', }, { tagName: 'text', selector: 'offsetLabelNegative', }, { tagName: 'rect', selector: 'offsetLabelAbsoluteBody', }, { tagName: 'text', selector: 'offsetLabelAbsolute', }, ], attrs: { line: { connection: true, fill: 'none', stroke: '#8f8f8f', strokeWidth: 2, strokeLinejoin: 'round', targetMarker: { tagName: 'path', d: 'M 10 -5 0 0 10 5 z', }, }, relativeLabel: { textAnchor: 'middle', textVerticalAnchor: 'middle', fill: 'black', fontSize: 12, }, relativeLabelBody: { x: -15, y: -10, width: 30, height: 20, fill: 'white', stroke: 'black', }, absoluteLabel: { textAnchor: 'middle', textVerticalAnchor: 'middle', fill: 'black', fontSize: 12, }, absoluteLabelBody: { x: -15, y: -10, width: 30, height: 20, fill: 'white', stroke: 'black', }, absoluteReverseLabel: { textAnchor: 'middle', textVerticalAnchor: 'middle', fill: 'black', fontSize: 12, }, absoluteReverseLabelBody: { x: -15, y: -10, width: 30, height: 20, fill: 'white', stroke: 'black', }, offsetLabelPositive: { textAnchor: 'middle', textVerticalAnchor: 'middle', fill: 'black', fontSize: 12, }, offsetLabelPositiveBody: { width: 120, height: 20, fill: 'white', stroke: 'black', }, offsetLabelNegative: { textAnchor: 'middle', textVerticalAnchor: 'middle', fill: 'black', fontSize: 12, }, offsetLabelNegativeBody: { width: 120, height: 20, fill: 'white', stroke: 'black', }, offsetLabelAbsolute: { textAnchor: 'middle', textVerticalAnchor: 'middle', fill: 'black', fontSize: 12, }, offsetLabelAbsoluteBody: { width: 140, height: 20, fill: 'white', stroke: 'black', }, offsetLabelMarker: { atConnectionRatio: 0.66, textAnchor: 'middle', textVerticalAnchor: 'middle', text: '*', fill: 'red', stroke: 'black', strokeWidth: 1, fontSize: 32, fontWeight: 'bold', }, offsetLabelPositiveConnector: { atConnectionRatio: 0.66, d: 'M 0 0 0 40', stroke: 'black', strokeDasharray: '5 5', }, offsetLabelNegativeConnector: { atConnectionRatio: 0.66, d: 'M 0 0 0 -40', stroke: 'black', strokeDasharray: '5 5', }, offsetLabelAbsoluteConnector: { atConnectionRatioIgnoreGradient: 0.66, d: 'M 0 0 -40 80', stroke: 'black', strokeDasharray: '5 5', }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, interacting: false, }) const edge = graph.addEdge({ shape: 'custom-edge', source: { x: 100, y: 60 }, target: { x: 500, y: 60 }, vertices: [{ x: 300, y: 160 }], attrs: { relativeLabel: { atConnectionRatio: 0.25, text: '0.25', }, relativeLabelBody: { atConnectionRatio: 0.25, }, absoluteLabel: { atConnectionLength: 150, text: '150', }, absoluteLabelBody: { atConnectionLength: 150, }, absoluteReverseLabel: { atConnectionLength: -100, text: '-100', }, absoluteReverseLabelBody: { atConnectionLength: -100, }, offsetLabelPositive: { atConnectionRatio: 0.66, y: 40, text: 'keepGradient: 0,40', }, offsetLabelPositiveBody: { atConnectionRatio: 0.66, x: -60, // 0 + -60 y: 30, // 40 + -10 }, offsetLabelNegative: { atConnectionRatio: 0.66, y: -40, text: 'keepGradient: 0,-40', }, offsetLabelNegativeBody: { atConnectionRatio: 0.66, x: -60, // 0 + -60 y: -50, // -40 + -10 }, offsetLabelAbsolute: { atConnectionRatioIgnoreGradient: 0.66, x: -40, y: 80, text: 'ignoreGradient: -40,80', }, offsetLabelAbsoluteBody: { atConnectionRatioIgnoreGradient: 0.66, x: -110, // -40 + -70 y: 70, // 80 + -10 }, }, }) let oscillateToggle = false function contract() { edge.animate( { 'source/x': 200, 'source/y': 100, 'target/x': 400, 'target/y': 100, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = true } function oscillate() { edge.animate( { 'source/x': 100, 'source/y': 200, 'target/x': 500, 'target/y': 200, 'vertices/0/x': 300, 'vertices/0/y': 60, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = false } contract() edge.on('animation:finish', () => { if (oscillateToggle) { oscillate() } else { contract() } }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 使用箭头 我们可以使用 [`sourceMarker`](/zh/api/registry/attr#sourcemarker) 和 [`targetMarker`](/zh/api/registry/attr#targetmarker) 两个特殊属性来为边指定起始箭头和终止箭头,详情请参考[这篇教程](/zh/api/model/marker)。 ### docs/demos/api/attrs/ref-node/index.less ```less .attrs-ref-node-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/attrs/ref-elem/index.less ```less .attrs-ref-elem-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/attrs/edge-relative-position/index.less ```less .attrs-edge-relative-position-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/attrs/edge-subelement-labels/index.less ```less .attrs-edge-subelement-labels-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### 路由 - Canonical page: https://x6.antv.antgroup.com/zh/api/registry/router/ - Markdown source: https://x6.antv.antgroup.com/zh/api/registry/router.md - Description: 路由 --- title: "路由" description: "路由" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/registry/router/" version: "3.x" --- 路由将边的路径点 [vertices](/zh/tutorial/basic/edge#vertices) 做进一步转换处理,并在必要时添加额外的点,然后返回处理后的点(不包含边的起点和终点)。例如,经过 [`orth`](#orth) 路由处理后,边的每一条线段都是水平或垂直的正交线段。 X6 中内置了以下几种路由。 | 路由名称 | 说明 | |-----------|--------------------------------------------------------------------------------------------------------------| | normal | [默认路由](#normal),原样返回路径点。 | | orth | [正交路由](#orth),由水平或垂直的正交线段组成。 | | oneSide | [受限正交路由](#oneside),由受限的三段水平或垂直的正交线段组成。 | | manhattan | [智能正交路由](#manhattan),由水平或垂直的正交线段组成,并自动避开路径上的其他节点(障碍)。 | | metro | [智能地铁线路由](#metro),由水平或垂直的正交线段和斜角线段组成,类似地铁轨道图,并自动避开路径上的其他节点(障碍)。 | | er | [实体关系路由](#er),由 `Z` 字形的斜角线段组成。 | 在使用时,可以为边设置路由: ```ts const edge = graph.addEdge({ source, target, router: { name: 'oneSide', args: { side: 'right', }, }, }) ``` 当路由没有参数时,也可以简化为: ```ts const edge = graph.addEdge({ source, target, router: 'oneSide', }) ``` 也可以调用 [`edge.setRouter`]() 方法来设置路由: ```ts edge.setRouter('oneSide', { side: 'right' }) ``` 在创建画布时,可以通过 `connecting` 选项来设置全局默认路由(画布的默认路由是 `'normal'`): ```ts new Graph({ connecting: { router: { name: 'oneSide', args: { side: 'right', }, }, }, }) ``` 当路由没有参数时,也可以简化为: ```ts new Graph({ connecting: { router: 'orth', }, }) ``` 下面我们一起来看看如何使用内置路由,以及如何自定并注册自定义路由。 ## 内置路由 ### normal 系统的默认路由,该路由原样返回传入的 `vertices` 路径点。 ### orth 正交路由,该路由在路径上添加额外的一些点,使边的每一条线段都水平或垂直正交。 支持的参数如下表: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |---------|-------------|:-------:|--------|-------------------------| | padding | SideOptions | 否 | 20 | 设置锚点距离转角的最小距离。 | `SideOptions` 定义如下: ```ts export type SideOptions = | number | { vertical?: number horizontal?: number left?: number top?: number right?: number bottom?: number } ``` 例如: ```ts graph.addEdge({ source, target, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], router: { name: 'orth', args: { padding: { left: 50, }, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect1 = graph.addNode({ x: 30, y: 30, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 300, y: 240, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addNode({ shape: 'circle', x: 80 - 5, y: 200 - 5, width: 10, height: 10, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) graph.addNode({ shape: 'circle', x: 300 - 5, y: 120 - 5, width: 10, height: 10, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source: rect1, target: rect2, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, vertices: [ { x: 80, y: 200 }, { x: 300, y: 120 }, ], router: { name: 'orth', }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### oneSide `oneSide` 路由是正交路由 `orth` 的受限版本,该路生成一个严格的三段路由:从起始节点的 `side` 侧开始,经过中间段,再从终止节点的 `side` 侧结束路由。需要特别注意的是,使用该路由时请不要同时指定 `vertices`,否则路由效果会非常差。 支持的参数如下表: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |---------|------------------------------------------------|:-------:|------------|--------------------------------------| | side | `'left'` \| `'right'` \| `'top'` \| `'bottom'` | 否 | `'bottom'` | 路由的起始/结束方向,默认值为 `'bottom'`。 | | padding | SideOptions | 否 | 20 | 设置锚点距离转角的最小距离。 | 例如: ```ts graph.addEdge({ source, target, router: { name: 'oneSide', args: { side: 'right' }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect1 = graph.addNode({ x: 30, y: 30, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 200, y: 200, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source: rect1, target: rect2, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, router: { name: 'oneSide', args: { side: 'right', padding: 50 }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### manhattan 曼哈顿路由 `manhattan` 路由是正交路由 `orth` 的智能版本,该路由由水平或垂直的正交线段组成,并自动避开路径上的其他节点(障碍)。 我们为该路由算法提供了丰富的选项: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |------------------|--------------------------|:-------:|--------------------------------------|---------------------------------------------------------| | step | number | 否 | `10` | 路由算法步进步长,其值越小计算量越大,推荐使用画布的网格大小。 | | excludeTerminals | ('source' \| 'target')[] | 否 | `[]` | 忽略起始或终止节点,忽略后不参与障碍物计算。 | | excludeShapes | string[] | 否 | `[]` | 忽略指定形状的节点,忽略后不参与障碍物计算。 | | excludeNodes | (Node \| string)[] | 否 | `[]` | 忽略的节点,忽略后不参与障碍物计算。 | | startDirections | string[] | 否 | `['top', 'right', 'bottom', 'left']` | 支持从哪些方向开始路由。 | | endDirections | string[] | 否 | `['top', 'right', 'bottom', 'left']` | 支持从哪些方向结束路由。 | | padding | SideOptions | 否 | - | 设置锚点距离转角的最小距离。 | | fallbackRouter | Router | 否 | `routerPresets.orth` | 无法避开障碍物的场景,降级到指定路由。 | 例如,从 `@antv/x6` 导入 `routerPresets`,显式设置降级路由: ```ts import { routerPresets } from '@antv/x6' graph.addEdge({ source, target, router: { name: 'manhattan', args: { fallbackRouter: routerPresets.orth, startDirections: ['top'], endDirections: ['bottom'], }, }, }) ``` 在使用 `manhattan` 路由和 `Selection` 插件时,如果出现批量拖拽节点卡顿的情况,推荐在 `Selection` 配置中加上 `movingRouterFallback: 'orth'`,将与选中节点相连的边在拖拽过程中临时切换为轻量的 `orth` 路由,避免频繁的路径计算,能有效提升拖拽流畅性: ```ts graph.use( new Selection({ ... // 拖拽期路由降级为 orth 路由 movingRouterFallback: 'orth', }), ) ``` > **注意** manhattan 路由的特性是自动避开路径中的障碍物,如果出现无法避开的情况,就会自动降级到 orth 路由,此时为了让开发者能够发现问题,我们在控制台增加了 warn:Unable to execute manhattan algorithm, use orth instead。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Edge, EdgeView } from '@antv/x6' 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({ x: 80, y: 40, width: 120, height: 50, label: 'Source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode( source.clone().translate(460, 230).setAttrByPath('label/text', 'Target'), ) const obstacles: Node[] = [] const obstacle = graph.addNode({ x: 200, y: 150, width: 120, height: 50, label: 'Obstacle', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) let edge: Edge const update = () => { const edgeView = graph.findViewByCell(edge) as EdgeView edgeView.update() } obstacles.push(obstacle) obstacles.push(graph.addNode(obstacle.clone().translate(280, -110))) obstacles.push(graph.addNode(obstacle.clone().translate(180, 10))) obstacles.forEach((obstacle) => obstacle.on('change:position', update)) edge = graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, router: { name: 'manhattan', args: { startDirections: ['top'], endDirections: ['bottom'], }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### metro 地铁路由 `metro` 是曼哈顿路由 `manhattan` 的一个变种,它由水平或垂直的正交线段和斜角线段组成,类似地铁轨道图,并自动避开路径上的其他节点(障碍)。其选项与 [manhattan](#manhattan)一样,但 `maxDirectionChange` 的默认值为 `45`,表示路由线段的最大倾斜角度为 `45` 度。 例如: ```ts graph.addEdge({ source, target, router: { name: 'metro', args: { startDirections: ['top'], endDirections: ['bottom'], }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Edge, EdgeView } from '@antv/x6' 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({ x: 80, y: 40, width: 120, height: 50, label: 'Source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode( source.clone().translate(460, 230).setAttrByPath('label/text', 'Target'), ) const obstacles: Node[] = [] const obstacle = graph.addNode({ x: 200, y: 150, width: 120, height: 50, label: 'Obstacle', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) let edge: Edge const update = () => { const edgeView = graph.findViewByCell(edge) as EdgeView edgeView.update() } obstacles.push(obstacle) obstacles.push(graph.addNode(obstacle.clone().translate(280, -110))) obstacles.push(graph.addNode(obstacle.clone().translate(180, 10))) obstacles.forEach((obstacle) => obstacle.on('change:position', update)) edge = graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, router: { name: 'metro', args: { startDirections: ['top'], endDirections: ['bottom'], }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### er 实体关系路由 `er` 由 Z 字形的斜角线段组成,常用于表示 ER 图中的实体之间的连线。 支持的参数如下表: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |-----------|------------------------------------------|---------|--------|---------------------------------------------------------------------------------------------| | offset | number \|'center' | 否 | `32` | 路由的第一个点和最后一个点与节点之间的距离。当取值为 `'center'` 时,节点距离的中心作为路由点坐标。 | | min | number | 否 | `16` | 路由的第一个点和最后一个点与节点之间的最小距离。 | | direction | `'T'`\|`'B'`\|`'L'`\|`'R'`\|`'H'`\|`'V'` | 否 | - | 路由方向,缺省时将自动选择最优方向。 | 例如: ```ts graph.addEdge({ source, target, router: { name: 'er', args: { offset: 24, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Edge, Graph } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private edge: Edge componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect1 = graph.addNode({ x: 40, y: 30, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 240, y: 150, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.edge = graph.addEdge({ source: rect1, target: rect2, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, router: { name: 'er', args: this.getERArgs(defaults), }, }) graph.centerContent() } getERArgs(state: State) { const { center, offset, min, direction } = state return { min, direction: direction || undefined, offset: center ? 'center' : offset, } } updateConnection = (state: State) => { this.edge.prop('router/args', this.getERArgs(state)) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 自定义路由 除了内置路由,我们还可以按照一定规则来创建自定义路由,例如,实现随机的路由: ```ts // 路由参数 interface RandomRouterArgs { bounces?: number } function randomRouter( vertices: Point.PointLike[], args: RandomRouterArgs, view: EdgeView, ) { const bounces = args.bounces || 20 const points = vertices.map((p) => Point.create(p)) for (var i = 0; i < bounces; i++) { const sourceCorner = view.sourceBBox.getCenter() const targetCorner = view.targetBBox.getCenter() const randomPoint = Point.random( sourceCorner.x, targetCorner.x, sourceCorner.y, targetCorner.y, ) points.push(randomPoint) } return points } Graph.registerRouter('random', randomRouter) edge.setRouter('random', { bounces: 3 }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Point } from '@antv/x6' Graph.registerRouter( 'random', (vertices, args, view) => { const BOUNCES = args.bounces || 20 const points = vertices.map((p) => Point.create(p)) for (let i = 0; i < BOUNCES; i += 1) { const sourceCorner = view.sourceBBox.getCenter() const targetCorner = view.targetBBox.getCenter() const randomPoint = Point.random( sourceCorner.x, targetCorner.x, sourceCorner.y, targetCorner.y, ) points.push(randomPoint) } return points }, 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({ x: 30, y: 30, width: 100, height: 40, label: 'Source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode( source.clone().translate(300, 200).attr('label/text', 'Target'), ) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, router: { name: 'random', args: { bounces: 3, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/api/router/orth/index.less ```less .orth-router-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/router/oneside/index.less ```less .oneside-router-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/router/manhattan/index.less ```less .manhattan-router-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 380px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/router/metro/index.less ```less .metro-router-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 380px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/router/er/index.less ```less .er-router-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/router/er/settings.tsx ```tsx import React from 'react' import { Switch, Radio, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { offset: number min: number center: boolean direction: '' | 'T' | 'B' | 'L' | 'R' | 'H' | 'V' } export const defaults: State = { offset: 32, min: 16, center: false, direction: '', } export class Settings extends React.Component { state: State = defaults notifyChange() { this.props.onChange(this.state) } onCenterChanged = (center: boolean) => { this.setState({ center }, () => { this.notifyChange() }) } onOffsetChanged = (offset: number) => { this.setState({ offset }, () => { this.notifyChange() }) } onMinChanged = (min: number) => { this.setState({ min }, () => { this.notifyChange() }) } onDirectionChanged = (e: any) => { const direction = e.target.value this.setState({ direction }, () => { this.notifyChange() }) } render() { return ( offset
{this.state.offset}
min
{this.state.min}
direction NONE L R H T B V
) } } ``` ### docs/demos/api/router/random/index.less ```less .random-router-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### 视口变换 - Canonical page: https://x6.antv.antgroup.com/zh/api/graph/transform/ - Markdown source: https://x6.antv.antgroup.com/zh/api/graph/transform.md - Description: 视口变换 --- title: "视口变换" description: "视口变换" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/graph/transform/" version: "3.x" --- ## 配置 ### scaling 通过 scaling 配置画布的最小或最大缩放级别。 ```ts new Graph({ scaling: { min: 0.05, // 默认值为 0.01 max: 12, // 默认值为 16 }, }) ``` ## 方法 ### resize(...) ```ts resize(width?: number, height?: number): this ``` 设置画布大小。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | ------ | ------ | :--: | ------ | ------------------------------ | | width | number | | | 画布宽度,缺省时宽度保持不变。 | | height | number | | | 画布高度,缺省时高度保持不变。 | ### zoom(...) ```ts zoom(): number ``` 获取画布缩放比例。 ```ts zoom(factor: number, options?: ZoomOptions): this ``` 缩放画布。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | factor | number | ✓ | | 缩放比例。 | | options.absolute | boolean | | `false` | 是否为绝对缩放, | | options.minScale | number | | - | 最小缩放比例。 | | options.maxScale | number | | - | 最大缩放比例。 | | options.scaleGrid | number | | - | 修正缩放比例为 `scaleGrid` 的整倍数。 | | options.center | Point.PointLike | | - | 缩放中心。 | 当 `options.absolute` 为 `true` 时,表示将画布缩放到 `factor` 代表的值,否则 `factor` 表示放大/缩小的因子,当 `factor` 为正数时表示画布放大画布,当 `factor` 为负数时表示缩小画布。 ### zoomTo(...) ```ts zoomTo(factor: number, options?: ZoomOptions): this ``` 缩放画布到指定的比例。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | factor | number | ✓ | | 缩放比例。 | | options.minScale | number | | - | 最小缩放比例。 | | options.maxScale | number | | - | 最大缩放比例。 | | options.scaleGrid | number | | - | 修正缩放比例为 `scaleGrid` 的整倍数。 | | options.center | Point.PointLike | | - | 缩放中心。 | ### zoomToFit(...) ```ts zoomToFit(options?: Options): this ``` 缩放画布内容,使画布内容充满视口。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | rect | Rectangle.RectangleLike | ✓ | | 矩形区域。 | | options.padding | number \| `{ left: number, top: number, right: number, bottom: number }` | | - | 边距。 | | options.contentArea | Rectangle.RectangleLike | | - | 内容区域,默认获取画布内容区域。 | | options.viewportArea | Rectangle.RectangleLike | | - | 视口区域,默认获取画布视口。 | | options.scaleGrid | number | | - | 修正缩放比例为 `scaleGrid` 的整倍数。 | | options.minScale | number | | - | 最小缩放比例。 | | options.maxScale | number | | - | 最大缩放比例。 | | options.minScaleX | number | | - | X 轴方向的最小缩放比例。 | | options.maxScaleX | number | | - | X 轴方向的最大缩放比例。 | | options.minScaleY | number | | - | Y 轴方向的最小缩放比例。 | | options.maxScaleY | number | | - | Y 轴方向的最大缩放比例。 | | options.preserveAspectRatio | boolean | | `false` | 是否保持长宽比。 | | options.useCellGeometry | boolean | | `true` | 是否使用节点/边的几何信息(Model)计算包围盒。 | ### rotate(...) ```ts rotate(): { angle: number cx?: number cy?: number } ``` 获取画布的旋转角度和旋转中心。 ```ts rotate(angle: number, cx?: number, cy?: number): this ``` 旋转画布。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | ----- | ------ | :--: | ------ | ----------------------------------- | | angle | number | ✓ | | 旋转角度。 | | cx | number | | - | 旋转中心 x 坐标,默认使用画布中心。 | | cy | number | | - | 旋转中心 y 坐标,默认使用画布中心。 | ### translate(...) ```ts translate(): { tx: number ty: number } ``` 获取画布的平移量。 ```ts translate(tx: number, ty: number): this ``` 平移画布。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | ---- | ------ | :--: | ------ | ------------ | | tx | number | ✓ | | X 轴平移量。 | | ty | number | ✓ | | Y 轴平移量。 | ### getContentArea(...) ```ts getContentArea(options?: Transform.GetContentAreaOptions): Rectangle ``` 获取画布内容的矩形区域,使用[画布本地坐标](/zh/api/graph/coordinate#clienttolocal)表示。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | options.useCellGeometry | boolean | | `true` | 是否使用节点/边的几何信息(Model)来计算画布内容大小。 | ### getContentBBox(...) ```ts getContentBBox(options?: Transform.GetContentAreaOptions): Rectangle ``` 获取画布内容的矩形区域,使用[画布坐标](/zh/api/graph/coordinate#localtograph)表示。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | options.useCellGeometry | boolean | | `true` | 是否使用节点/边的几何信息(Model)来计算画布内容大小。 | ### center(...) ```ts center(options?: CenterOptions): this ``` 将画布中心与视口中心对齐。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | options.padding | number \| Padding | | - | 边距,只在 `scroller` 画布中生效 | ### centerPoint(...) ```ts centerPoint(x?: number | null, y?: number | null, options?: CenterOptions): this ``` 将 `x` 和 `y` 指定的点(相对于画布)与视口中心对齐。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | x | number | | - | 相对一画布的 x 轴坐标。 | | y | number | | - | 相对一画布的 y 轴坐标。 | | options.padding | number \| Padding | | - | 边距,只在 `scroller` 画布中生效 | ```ts graph.centerPoint(100, 200) graph.centerPoint(100, null, { padding: { left: 100 } }) graph.centerPoint(null, 200, { padding: { left: 100 } }) ``` ### centerContent(...) ```ts centerContent(options?: PositionContentOptions): this ``` 将画布内容中心与视口中心对齐。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | options.padding | number \| Padding | | - | 边距,只在 `scroller` 画布中生效 | | options.useCellGeometry | boolean | | `true` | 是否通过节点/边的几何信息(Model)计算内容区域。 | ```ts graph.centerContent() graph.centerContent({ padding: { left: 100 } }) ``` ### centerCell(...) ```ts centerCell(options?: CenterOptions): this ``` 将节点/边的中心与视口中心对齐。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | cell | Cell | ✓ | | 节点/边。 | | options.padding | number \| Padding | | - | 边距,只在 `scroller` 画布中生效 | ```ts graph.centerCell(cell) graph.centerCell(cell, { padding: { left: 100 } }) ``` ### positionContent(...) ```ts positionContent(pos: Position, options?: PositionContentOptions): this ``` 将 `pos` 代表的画布内容 BBox 位置与对应的画布视口位置对齐。如 `pos` 为 `'bottom-left'` 时,表示将画布内容的左下角与视口的左下角对齐。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | pos | Position | ✓ | | 对齐位置。 | | options.padding | number \| Padding | | - | 边距,只在 `scroller` 画布中生效 | | options.useCellGeometry | boolean | | `true` | 是否通过节点/边的几何信息(Model)计算内容区域。 | 支持的对齐位置有: ```ts type Position = | 'center' | 'top' | 'top-right' | 'top-left' | 'right' | 'bottom-right' | 'bottom' | 'bottom-left' | 'left' ``` ### positionCell(...) ```ts positionCell(cell: Cell, pos: Direction, options?: CenterOptions): this ``` 将 `pos` 代表的节点/边 BBox 位置与对应的画布视口位置对齐。如 `pos` 为 `'bottom-left'` 时,表示将节点/边的左下角与视口的左下角对齐。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | cell | Cell | ✓ | | 节点/边。 | | pos | Position | ✓ | | 对齐位置。 | | options.padding | number \| Padding | | - | 边距,只在 `scroller` 画布中生效 | ```ts type Position = | 'center' | 'top' | 'top-right' | 'top-left' | 'right' | 'bottom-right' | 'bottom' | 'bottom-left' | 'left' ``` ### positionRect(...) ```ts positionRect(rect: Rectangle.RectangleLike, pos: Direction, options?: CenterOptions): this ``` 将 `pos` 代表的矩形位置与对应的画布视口位置对齐。如 `pos` 为 `'bottom-left'` 时,表示将矩形的左下角与视口的左下角对齐。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | rect | Rectangle.RectangleLike | ✓ | | 矩形区域。 | | pos | Position | ✓ | | 对齐位置。 | | options.padding | number \| Padding | | - | 边距,只在 `scroller` 画布中生效 | ```ts type Position = | 'center' | 'top' | 'top-right' | 'top-left' | 'right' | 'bottom-right' | 'bottom' | 'bottom-left' | 'left' ``` ### positionPoint(...) ```ts positionPoint(point: Point.PointLike, x: number | string, y: number | string options?: CenterOptions): this ``` 将 `point` 指定的点(相对于画布)与 `x` 和 `y` 代表的画布视口位置对齐。 | 名称 | 类型 | 必选 | 默认值 | 描述 | | --- | --- | :-: | --- | --- | | point | Point.PointLike | ✓ | | 被对齐的点。 | | x | number \| string | ✓ | | 视口 x 位置,支持百分比和负值。 | | y | number \| string | ✓ | | 视口 y 位置,支持百分比和负值。 | | options.padding | number \| Padding | | - | 边距,只在 scroller 画布中生效 | ```ts // 将画布的左上角与视口中的点 [100, 50] 对齐 graph.positionPoint({ x: 0, y: 0 }, 100, 50) // 将画布上的点 { x: 30, y: 80 } 与离视口左侧 25% 和离视口底部 40px 的位置对齐 graph.positionPoint({ x: 30, y: 80 }, '25%', -40) // 将画布上的点 { x: 30, y: 80 } 与离视口右侧 25% 和离视口顶部 40px 的位置对齐 graph.positionPoint({ x: 30, y: 80 }, '-25%', 40) ``` #### 交互 - Canonical page: https://x6.antv.antgroup.com/zh/api/model/interaction/ - Markdown source: https://x6.antv.antgroup.com/zh/api/model/interaction.md - Description: 交互 --- title: "交互" description: "交互" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/model/interaction/" version: "3.x" --- X6 最吸引开发者的地方是具备非常完整的交互定制能力,在此基础上,我们可以实现非常多的复杂效果。下面列举常见的交互行为。 ## 连线 通过配置 `connecting` 可以实现丰富的连线交互。使用方式如下: ```typescript const graph = new Graph({ ..., connecting: { snap: true, } }) ``` 下面分别介绍 `connecting` 支持的配置。 ### snap ```typescript snap: boolean | { radius: number, anchor: 'center' | 'bbox' } ``` 当 `snap` 设置为 `true` 或者 `false` 代表开启和关闭连线过程中自动吸附,开启时距离目标 `50px` 是触发吸附。可以通过配置 `radius` 属性来自定义吸附半径。 ```ts const graph = new Graph({ connecting: { snap: true, }, }) // 等价于 const graph = new Graph({ connecting: { snap: { radius: 50, }, }, }) ``` 当计算distance用于判断是否吸附到节点时,默认基于节点的中心,可以通过配置 `anchor` 为 `bbox` 改为基于节点的包围盒子计算距离。 ### allowBlank ```typescript allowBlank: boolean | ((this: Graph, args: ValidateConnectionArgs) => boolean) ``` 是否允许连接到画布空白位置的点,默认为 `true`,也支持通过函数的方式来动态调整。 ```typescript const graph = new Graph({ connecting: { allowBlank() { // 根据条件返回 true or false return true }, }, }) ``` ### allowLoop ```typescript allowLoop: boolean | ((this: Graph, args: ValidateConnectionArgs) => boolean) ``` 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,默认为 `true` 。 ### allowNode ```typescript allowNode: boolean | ((this: Graph, args: ValidateConnectionArgs) => boolean) ``` 是否允许边连接到节点(非节点上的连接桩),默认为 `true` 。 ### allowEdge ```typescript allowEdge: boolean | ((this: Graph, args: ValidateConnectionArgs) => boolean) ``` 是否允许边链接到另一个边,默认为 `true` 。 ### allowPort ```typescript allowPort: boolean | ((this: Graph, args: ValidateConnectionArgs) => boolean) ``` 是否允许边链接到连接桩,默认为 `true` 。 ### allowMulti ```typescript allowMulti: boolean | 'withPort' | ((this: Graph, args: ValidateConnectionArgs) => boolean) ``` 是否允许在相同的起始节点和终止之间创建多条边,默认为 `true` 。当设置为 `false` 时,在起始和终止节点之间只允许创建一条边,当设置为 `'withPort'` 时,在起始和终止节点的相同连接桩之间只允许创建一条边(即,起始和终止节点之间可以创建多条边,但必须要要链接在不同的连接桩上)。 ### highlight ```typescript highlight: boolean ``` 拖动边时,是否高亮显示所有可用的连接桩或节点,默认值为 `false` 。一般都会与 [highlighting](/zh/tutorial/basic/interacting#高亮) 联合使用。 ### anchor ```typescript anchor: NodeAnchorOptions ``` 当连接到节点时,通过 [ `anchor` ](/zh/api/registry/node-anchor) 来指定被连接的节点的锚点,默认值为 `center` 。 #### sourceAnchor ```typescript sourceAnchor?: NodeAnchorOptions ``` 当连接到节点时,通过 `sourceAnchor` 来指定源节点的锚点。 ### targetAnchor ```typescript targetAnchor?: NodeAnchorOptions ``` 当连接到节点时,通过 `targetAnchor` 来指定目标节点的锚点。 ### edgeAnchor ```typescript edgeAnchor: EdgeAnchorOptions ``` 当连接到边时,通过 [ `edgeAnchor` ](/zh/api/registry/edge-anchor) 来指定被连接的边的锚点,默认值为 `ratio` 。 ### sourceEdgeAnchor ```typescript sourceEdgeAnchor?: EdgeAnchorOptions ``` 当连接到边时,通过 `sourceEdgeAnchor` 来指定源边的锚点。 ### targetEdgeAnchor ```typescript targetEdgeAnchor?: EdgeAnchorOptions ``` 当连接到边时,通过 `targetEdgeAnchor` 来指定目标边的锚点。 ### connectionPoint ```typescript connectionPoint: ConnectionPointOptions ``` 指定[连接点](/zh/api/registry/connector),默认值为 `boundary` 。 ### sourceConnectionPoint ```typescript sourceConnectionPoint?: ConnectionPointOptions ``` 连接源的连接点。 ### targetConnectionPoint ```typescript targetConnectionPoint?: ConnectionPointOptions ``` 连接目标的连接点。 ### router ```typescript router: string | Router.NativeItem | Router.ManaualItem ``` [路由](/zh/api/registry/router)将边的路径点 `vertices` 做进一步转换处理,并在必要时添加额外的点,然后返回处理后的点,默认值为 `normal` 。 ### connector ```typescript connector: string | Connector.NativeItem | Connector.ManaualItem ``` [连接器](/zh/api/registry/connector)将起点、路由返回的点、终点加工为 `path` 元素的 d 属性,决定了边渲染到画布后的样式,默认值为 `normal` 。 ### createEdge ```typescript createEdge?: ( this: Graph, args: { sourceCell: Cell sourceView: CellView sourceMagnet: Element }, ) => Nilable | void ``` 通过该方法可以自定义新建的边的样式。 ### validateMagnet ```typescript validateMagnet?: ( this: Graph, args: { cell: Cell view: CellView magnet: Element e: Dom.MouseDownEvent | Dom.MouseEnterEvent }, ) => boolean ``` 点击 `magnet` 时 根据 `validateMagnet` 返回值来判断是否新增边,触发时机是 `magnet` 被按下,如果返回 `false` ,则没有任何反应,如果返回 `true` ,会在当前 `magnet` 创建一条新的边。 ### validateConnection ```typescript validateConnection: (this: Graph, args: ValidateConnectionArgs) => boolean ``` 在移动边的时候判断连接是否有效,如果返回 `false` ,当鼠标放开的时候,不会连接到当前元素,否则会连接到当前元素。 ### validateEdge ```typescript validateEdge?: ( this: Graph, args: { edge: Edge type: Edge.TerminalType previous: Edge.TerminalData }, ) => boolean ``` 当停止拖动边的时候根据 `validateEdge` 返回值来判断边是否生效,如果返回 `false` , 该边会被清除。 ## 组合 通过 `embedding` 可以将一个节点拖动到另一个节点中,使其成为另一节点的子节点,默认禁用。支持的配置如下: ### enabled ```typescript enabled?: boolean ``` 是否允许节点之间嵌套,默认值为 `false` 。 ### findParent ```typescript findParent?: | 'bbox' | 'center' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' | ((this: Graph, args: { node: Node; view: NodeView }) => Cell[]) ``` 在节点被移动时通过 `findParent` 指定的方法返回父节点。默认值为 `bbox` 。 ### frontOnly ```typescript frontOnly?: boolean ``` 如果 `frontOnly` 为 `true` ,则只能嵌入显示在最前面的节点,默认值为 `true` 。 ### validate ```typescript validate: ( this: Graph, args: { child: Node parent: Node childView: CellView parentView: CellView }, ) => boolean ``` `validate` 为判断节点能否被嵌入父节点的函数,默认返回 `true` 。 ## 限制 限制节点和边的交互行为,`interacting` 支持的配置如下: ```typescript export type Interacting = | boolean | InteractionMap | ((this: Graph, cellView: CellView) => InteractionMap | boolean) ``` - `boolean` 节点或边是否可交互 - `InteractionMap` 节点或边的交互细节,支持以下属性: - `'nodeMovable'` 节点是否可以被移动。 - `'magnetConnectable'` 当在具有 `'magnet'` 属性的元素上按下鼠标开始拖动时,是否触发连线交互。 - `'edgeMovable'` 边是否可以被移动。 - `'edgeLabelMovable'` 边的标签是否可以被移动。 - `'arrowheadMovable'` 边的起始/终止箭头是否可以被移动。 - `'vertexMovable'` 边的路径点是否可以被移动。 - `'vertexAddable'` 是否可以添加边的路径点。 - `'vertexDeletable'` 边的路径点是否可以被删除。 - `(this: Graph, cellView: CellView) => InteractionMap | boolean` ```ts const graph = new Graph({ container: this.container, width: 800, height: 1400, grid: 10, interacting: function (cellView: CellView) { if (cellView.cell.getProp('customLinkInteractions')) { return { vertexAdd: false } } return true }, }) ``` ## 高亮 可以通过 `highlighting` 选项来指定触发某种交互时的高亮样式,如: ```ts new Graph({ highlighting: { // 当连接桩可以被链接时,在连接桩外围渲染一个 2px 宽的红色矩形框 magnetAvailable: { name: 'stroke', args: { padding: 4, attrs: { 'stroke-width': 2, stroke: 'red', }, }, }, }, }) ``` 支持的 `highlighting` 配置项有: - `'default'` 默认高亮选项,当以下几种高亮配置缺省时被使用。 - `'embedding'` 拖动节点进行嵌入操作过程中,节点可以被嵌入时被使用。 - `'nodeAvailable'` 连线过程中,节点可以被链接时被使用。 - `'magnetAvailable'` 连线过程中,连接桩可以被链接时被使用。 - `'magnetAdsorbed'` 连线过程中,自动吸附到连接桩时被使用。 上面 `magnetAvailable.name` 其实是高亮器的名称,X6 内置了 `stroke` 和 `className` 两种高亮器,详细信息参考 [Highlighter](/zh/api/registry/highlighter)。 ## 移动范围 可以在全局配置 `translating` 来限制节点的移动范围。 ```ts const graph = new Graph({ translating: { restrict: true, }, }) ``` ### restrict 节点的可移动范围。支持以下两种方式: - `boolean` 如果设置为 `true`, 节点不能移动超出画布区域 - `Rectangle.RectangleLike | (arg: CellView) => Rectangle.RectangleLike` 指定一个节点的移动范围 ```ts const graph = new Graph({ translating: { restrict: { x: 0, y: 0, width: 100, height: 100, }, }, }) ``` #### 连接器 - Canonical page: https://x6.antv.antgroup.com/zh/api/registry/connector/ - Markdown source: https://x6.antv.antgroup.com/zh/api/registry/connector.md - Description: 连接器 --- title: "连接器" description: "连接器" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/registry/connector/" version: "3.x" --- 连接器将起点、路由返回的点、终点加工为 `` 元素的 [`d`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) 属性,决定了边渲染到画布后的样式。X6 中内置了以下几种连接器。 | 连接器 | 说明 | |----------|-------------------------------------------------------------------------------------| | normal | [简单连接器](#normal),用直线连接起点、路由点和终点。 | | smooth | [平滑连接器](#smooth),用三次贝塞尔曲线线连接起点、路由点和终点。 | | rounded | [圆角连接器](#rounded),用直线连接起点、路由点和终点,并在线段连接处用圆弧链接(倒圆角)。 | | jumpover | [跳线连接器](#jumpover),用直线连接起点、路由点和终点,并在边与边的交叉处用跳线符号链接。 | 可以为某条边设置路由: ```ts const edge = graph.addEdge({ source, target, connector: { name: 'rounded', args: { radius: 20, }, }, }) ``` 当没有连接器参数时,可以简化为: ```ts const edge = graph.addEdge({ source, target, connector: 'rounded', }) ``` 也可以调用 `edge.setConnector` 来设置连接器: ```ts edge.setConnector('rounded', { radius: 20 }) ``` 在创建画布时,可以通过 `connecting` 选项来设置全局默认连接器(默认为 `'normal'`): ```ts new Graph({ connecting: { connector: { name: 'rounded', args: { radius: 20, }, }, }, }) ``` 当路由没有参数时,也可以简化为: ```ts new Graph({ connecting: { connector: 'rounded', }, }) ``` 下面我们一起来看看如何使用内置连接器,以及如何自定并注册自定义连接器。 ## 内置连接器 ### normal 系统的默认连接器,将起点、路由点、终点通过直线按顺序连接。 支持的参数如下表: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|----------|:-------:|---------|-------------------------------------------------------------| | raw | boolean | 否 | `false` | 是否返回一个 `Path` 对象,默认值为 `false` 返回序列化后的字符串。 | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect1 = graph.addNode({ x: 30, y: 30, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 300, y: 240, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source: rect1, target: rect2, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, connector: { name: 'normal', }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### smooth 平滑连接器,通过三次贝塞尔链接起点、路由点和终点。 支持的参数如下表: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |-----------|------------|:-------:|---------|--------------------------------------------------------------| | raw | boolean | 否 | `false` | 是否返回一个 `Path` 对象,默认值为 `false` 返回序列化后的字符串。 | | direction | `H` \| `V` | 否 | - | 保持水平连接或者保持垂直连接,不设置会根据起点和终点位置动态计算。 | 例如: ```ts graph.addEdge({ source: rect1, target: rect2, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], connector: 'smooth', }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect1 = graph.addNode({ x: 30, y: 30, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 300, y: 240, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source: rect1, target: rect2, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, connector: { name: 'smooth', }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### rounded 圆角连接器,将起点、路由点、终点通过直线按顺序连接,并在线段连接处通过圆弧连接(倒圆角)。 支持的参数如下表: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|----------|:-------:|---------|-------------------------------------------------------------| | radius | number | 否 | `10` | 倒角半径。 | | raw | boolean | 否 | `false` | 是否返回一个 `Path` 对象,默认值为 `false` 返回序列化后的字符串。 | 例如: ```ts graph.addEdge({ source: rect1, target: rect2, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], connector: { name: 'rounded', args: { radius: 10, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Edge } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private edge: Edge componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect1 = graph.addNode({ x: 30, y: 30, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 300, y: 220, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.edge = graph.addEdge({ source: rect1, target: rect2, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], connector: { name: 'rounded', }, }) graph.centerContent() } onAttrsChanged = (args: State) => { this.edge.setConnector('rounded', args) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### jumpover 跳线连接器,用直线连接起点、路由点和终点,并在边与边的交叉处用跳线符号链接。 支持的参数如下表: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|---------------------------|:-------:|---------|-------------------------------------------------------------| | type | 'arc' \| 'gap' \| 'cubic' | 否 | `'arc'` | 跳线类型。 | | size | number | 否 | `5` | 跳线大小。 | | radius | number | 否 | `0` | 倒角半径。 | | raw | boolean | 否 | `false` | 是否返回一个 `Path` 对象,默认值为 `false` 返回序列化后的字符串。 | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Edge } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private edge: Edge componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const source = graph.addNode({ x: 24, y: 70, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode({ x: 24, y: 160, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.edge = graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, vertices: [ { x: 760, y: 150 }, { x: 760, y: 240 }, ], connector: { name: 'jumpover', args: { type: 'arc', size: 5, radius: 0, }, }, }) const rect = graph.addNode({ x: 160, y: 24, width: 70, height: 30, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) for (let i = 0; i < 6; i += 1) { const source = rect.clone().translate(i * 100, i * 10) graph.addNode(source) const target = source.clone().translate(0, 200) graph.addNode(target) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } } onAttrsChanged = (args: State) => { this.edge.setConnector('jumpover', args) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 自定义连接器 连接器是一个具有如下签名的函数,返回 `Path` 对象或序列化后的字符串。 ```ts export type Definition = ( this: EdgeView, // 边的视图 sourcePoint: Point.PointLike, // 起点 targetPoint: Point.PointLike, // 终点 routePoints: Point.PointLike[], // 路由返回的点 args: T, // 参数 edgeView: EdgeView, // 边的视图 ) => Path | string ``` | 参数名 | 参数类型 | 参数说明 | |-------------|-------------------|-------------| | this | EdgeView | 边的视图。 | | sourcePoint | Point.PointLike | 起点。 | | targetPoint | Point.PointLike | 终点。 | | routePoints | Point.PointLike[] | 路由返回的点。 | | args | T | 连接器参数。 | | edgeView | EdgeView | 边的视图。 | 我来定义一个 `wobble` 连接器: ```ts export interface WobbleArgs { spread?: number raw?: boolean } function wobble( sourcePoint: Point.PointLike, targetPoint: Point.PointLike, vertices: Point.PointLike[], args: WobbleArgs, ) { const spread = args.spread || 20 const points = [...vertices, targetPoint].map((p) => Point.create(p)) let prev = Point.create(sourcePoint) const path = new Path(Path.createSegment('M', prev)) for (let i = 0, n = points.length; i < n; i += 1) { const next = points[i] const distance = prev.distance(next) let d = spread while (d < distance) { const current = prev.clone().move(next, -d) current.translate( Math.floor(7 * Math.random()) - 3, Math.floor(7 * Math.random()) - 3, ) path.appendSegment(Path.createSegment('L', current)) d += spread } path.appendSegment(Path.createSegment('L', next)) prev = next } return args.raw ? path : path.serialize() } ``` 然后注册连接器: ```ts Graph.registerConnector('wobble', wobble) ``` 注册以后我们就可以通过连接器名称来使用: ```ts edge.setConnector('wobble', { spread: 16 }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Path, Point } from '@antv/x6' export interface WobbleArgs { spread?: number raw?: boolean } Graph.registerConnector( 'wobble', (sourcePoint, targetPoint, vertices, args: WobbleArgs) => { const spread = args.spread || 20 const points = [...vertices, targetPoint].map((p) => Point.create(p)) let prev = Point.create(sourcePoint) const path = new Path() path.appendSegment(Path.createSegment('M', prev)) for (let i = 0, n = points.length; i < n; i += 1) { const next = points[i] const distance = prev.distance(next) let d = spread while (d < distance) { const current = prev.clone().move(next, -d) current.translate( Math.floor(7 * Math.random()) - 3, Math.floor(7 * Math.random()) - 3, ) path.appendSegment(Path.createSegment('L', current)) d += spread } path.appendSegment(Path.createSegment('L', next)) prev = next } return path }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect1 = graph.addNode({ x: 30, y: 30, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 300, y: 240, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source: rect1, target: rect2, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], connector: { name: 'wobble', args: { spread: 16, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/api/connector/normal/index.less ```less .connector-normal-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/connector/smooth/index.less ```less .connector-smooth-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/connector/rounded/index.less ```less .connector-rounded-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 320px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/connector/rounded/settings.tsx ```tsx import React from 'react' import { Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { radius: number } export class Settings extends React.Component { state: State = { radius: 10, } notifyChange() { this.props.onChange(this.state) } onRadiusChanged = (radius: number) => { this.setState({ radius }, () => { this.notifyChange() }) } render() { return ( radius
{this.state.radius}
) } } ``` ### docs/demos/api/connector/jumpover/index.less ```less .connector-jumpover-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/connector/jumpover/settings.tsx ```tsx import React from 'react' import { Slider, Radio, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { type: string size: number radius: number } export class Settings extends React.Component { state: State = { type: 'arc', size: 5, radius: 0, } notifyChange() { this.props.onChange(this.state) } onSizeChanged = (size: number) => { this.setState({ size }, () => { this.notifyChange() }) } onRadiusChanged = (radius: number) => { this.setState({ radius }, () => { this.notifyChange() }) } onTypeChange = (e: any) => { this.setState({ type: e.target.value }, () => { this.notifyChange() }) } render() { return ( type arc cubic gap size
{this.state.size}
radius
{this.state.radius}
) } } ``` ### docs/demos/api/connector/wobble/index.less ```less .connector-wobble-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### 坐标系 - Canonical page: https://x6.antv.antgroup.com/zh/api/graph/coordinate/ - Markdown source: https://x6.antv.antgroup.com/zh/api/graph/coordinate.md - Description: 坐标系 --- title: "坐标系" description: "坐标系" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/graph/coordinate/" version: "3.x" --- ## 演示 ```ts import './index.less' import { createRoot } from 'react-dom/client' import * as React from 'react' import { Graph } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private v: HTMLDivElement private h: HTMLDivElement private c: HTMLDivElement componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const source = this.graph.addNode({ x: 80, y: 80, width: 80, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ x: 240, y: 240, width: 80, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.onChanged(defaults) this.start() } componentWillUnmount() { document.removeEventListener('mousemove', this.onMouseMove) } onMouseMove = (e: MouseEvent) => { const target = e.target as HTMLElement if ( this.container.contains(target) || this.container === target || target === this.v || target === this.h || target === this.c ) { const pageX = e.pageX const pageY = e.pageY const clientX = e.clientX const clientY = e.clientY this.v.style.left = `${pageX + 2}px` this.h.style.top = `${pageY + 2}px` this.c.style.left = `${pageX + 10}px` this.c.style.top = `${pageY + 10}px` const p1 = this.graph.pageToLocal(pageX, pageY) const p2 = this.graph.localToGraph(p1) this.c.innerHTML = `
Page(pageX, pageY): ${pageX} x ${pageY}
Client(clientX, clientY): ${clientX} x ${clientY}
Local Point: ${p1.x} x ${p1.y}
Graph Point: ${p2.x} x ${p2.y}
` } else { this.v.style.left = `${-1000}px` this.h.style.top = `${-1000}px` this.c.style.left = `${-10000}px` this.c.style.top = `${-10000}px` } } start() { const root = document.getElementById('container')! const v = document.createElement('div') const h = document.createElement('div') const c = document.createElement('div') v.style.position = 'absolute' v.style.width = '1px' v.style.top = '0' v.style.bottom = '0' v.style.left = '-100px' v.style.zIndex = `99` v.style.borderLeft = '1px dashed red' h.style.position = 'absolute' h.style.height = '1px' h.style.left = '0' h.style.right = '0' h.style.top = '-100px' h.style.zIndex = `99` h.style.borderTop = '1px dashed red' c.style.position = 'absolute' c.style.display = 'inline-block' c.style.fontSize = '12px' c.style.zIndex = `99` c.style.padding = '4px 8px' c.style.borderRadius = '2px' c.style.lineHeight = '20px' c.style.background = '#f6ffed' c.style.border = '1px solid #b7eb8f' root.appendChild(v) root.appendChild(h) root.appendChild(c) this.v = v this.h = h this.c = c document.addEventListener('mousemove', this.onMouseMove) } onChanged = (settgins: State) => { this.graph.scale(settgins.scale) this.graph.translate(settgins.tx, settgins.ty) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 在位置计算中,我们经常需要做坐标转换,在 X6 中,存在两个坐标系,一个是画布本地坐标系 `local`,一个是画布坐标系 `graph`,有时候还要涉及到浏览器坐标系,这里统一做一个解释: - `local`:画布本地坐标系,默认情况下和 `graph` 坐标系一致,但是会随着画布的缩放和平移发生改变。画布中所有节点的坐标都是以 `local` 坐标系为准。 - `graph`:画布坐标系,也就是我们看到的画布视口,它不会随着画布缩放和平移而改变。 - `client`:浏览器坐标系,鼠标事件中的 `e.clinetX`、`e.clientY` 就是相对于浏览器坐标系。 - `page`:页面坐标系,与 `client` 相比,`page` 会考虑页面水平和垂直方向滚动。鼠标事件中的 `e.pageX`、`e.pageY` 就是相对于页面坐标系。 ## 方法 ### pageToLocal(...) ```ts pageToLocal(rect: Rectangle.RectangleLike): Rectangle pageToLocal(x: number, y: number, width: number, height: number): Rectangle pageToLocal(p: Point.PointLike): Point pageToLocal(x: number, y: number): Point ``` 将页面坐标转换为画布本地坐标。 ### localToPage(...) ```ts localToPage(rect: Rectangle.RectangleLike): Rectangle localToPage(x: number, y: number, width: number, height: number): Rectangle localToPage(p: Point.PointLike): Point localToPage(x: number, y: number): Point ``` 将画布本地坐标转换为页面坐标。 ### clientToLocal(...) ```ts clientToLocal(rect: Rectangle.RectangleLike): Rectangle clientToLocal(x: number, y: number, width: number, height: number): Rectangle clientToLocal(p: Point.PointLike): Point clientToLocal(x: number, y: number): Point ``` 将页面的浏览器坐标转换画布本地坐标。 ### localToClient(...) ```ts localToClient(rect: Rectangle.RectangleLike): Rectangle localToClient(x: number, y: number, width: number, height: number): Rectangle localToClient(p: Point.PointLike): Point localToClient(x: number, y: number): Point ``` 将画布本地坐标转换为浏览器坐标。 ### localToGraph(...) ```ts localToGraph(rect: Rectangle.RectangleLike): Rectangle localToGraph(x: number, y: number, width: number, height: number): Rectangle localToGraphPoint(p: Point.PointLike): Point localToGraphPoint(x: number, y: number): Point ``` 将画布本地坐标转换为画布坐标。 ### graphToLocal(...) ```ts graphToLocal(rect: Rectangle.RectangleLike): Rectangle graphToLocal(x: number, y: number, width: number, height: number): Rectangle graphToLocal(p: Point.PointLike): Point graphToLocal(x: number, y: number): Point ``` 将画布坐标转换为画布本地坐标。 ### snapToGrid(...) ```ts snapToGrid(p: Point.PointLike): Point snapToGrid(x: number, y: number): Point ``` 将浏览器坐标转换为画布[本地坐标](#clienttolocal)并对齐到画布网格。 ### docs/demos/api/coord/playground/index.less ```less .bar { position: absolute; top: 48px; right: 0; left: 0; height: 2px; background: linear-gradient(to right, #9198e5, #e66465); } .coord-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { top: 64px; z-index: 10; width: 336px; padding: 0 8px; } .app-content { flex: 1; height: 318px; margin-right: 8px; overflow: auto; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } #elapsed { padding: 0 7px; color: #52c41a; font-size: 12px; font-variant: tabular-nums; line-height: 20px; white-space: nowrap; background: #f6ffed; border-color: #b7eb8f; border-radius: 2px; } } ``` ### docs/demos/api/coord/playground/settings.tsx ```tsx import React from 'react' import { Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { scale: number tx: number ty: number } export const defaults: State = { scale: 1, tx: 0, ty: 0, } export class Settings extends React.Component { state: State = defaults notifyChange() { this.props.onChange(this.state) } onScaleChanged = (scale: number) => { this.setState({ scale }, () => { this.notifyChange() }) } onTxChanged = (tx: number) => { this.setState({ tx }, () => { this.notifyChange() }) } onTyChanged = (ty: number) => { this.setState({ ty }, () => { this.notifyChange() }) } render() { return ( scale
{this.state.scale}
translateX
{this.state.tx}
translateY
{this.state.ty}
) } } ``` #### 视图 - Canonical page: https://x6.antv.antgroup.com/zh/api/mvc/view/ - Markdown source: https://x6.antv.antgroup.com/zh/api/mvc/view.md - Description: 视图 --- title: "视图" description: "视图" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/mvc/view/" version: "3.x" --- ## 配置 ### async 是否是异步渲染的画布。异步渲染不会阻塞 UI,对需要添加大量节点和边时的性能提升非常明显。但需要注意的是,一些同步操作可能会出现意外结果,比如获取某个节点的视图、获取节点/边的包围盒等,因为这些同步操作触发时异步渲染可能并没有完成。 ### virtual 是否只渲染可视区域内的元素。支持传入 `boolean` 或对象 `VirtualOptions`,默认为 `false`。当传入为布尔值 `true` 时,仅渲染当前可视区域内和缓冲边距(默认值:120px)范围内的元素。 当传入对象 `VirtualOptions` 时: - `enabled`: 是否启用虚拟渲染开关。 - `margin`: 缓冲边距(单位 px)。在可视窗口边界外额外预渲染该边距范围内的元素,以减少拖拽/缩放过程中的重绘抖动,提高交互连续性。 示例: ```ts new Graph({ container: el, virtual: { enabled: true, margin: 200 }, }) ``` 使用虚拟渲染,在拖拽、缩放或滚动画布时,会根据画布窗口大小动态加载剩余元素。在元素数量很大的场景,能显著提升性能。 ### magnetThreshold 鼠标移动多少次后才触发连线,或者设置为 'onleave' 时表示鼠标移出元素时才触发连线,默认为 `0`。 ### moveThreshold 触发 `'mousemove'` 事件之前,允许鼠标移动的次数,默认为 `0`。 ### clickThreshold 当鼠标移动次数超过指定的数字时,将不触发鼠标点击事件,默认为 `0`。 ### preventDefaultContextMenu 是否禁用画布的默认右键,默认为 `true`。 ### preventDefaultBlankAction 在画布空白位置响应鼠标事件时,是否禁用鼠标默认行为,默认为 `true`。 ### onPortRendered ```ts ( this: Graph, args: { node: Node port: Port container: Element selectors?: Markup.Selectors labelContainer: Element labelSelectors?: Markup.Selectors contentContainer: Element contentSelectors?: Markup.Selectors }, ) => void ``` 当某个连接桩渲染完成时触发的回调,参数如下: | 名称 | 类型 | 非空 | 描述 | |------------------|------------------|:----:|---------------------------------------| | node | Node | ✓ | 节点实例。 | | port | Port | ✓ | 连接桩选项。 | | container | Element | ✓ | 连接桩的容器元素。 | | selectors | Markup.Selectors | | 连接桩 Markup 渲染后的选择器键值对。 | | labelContainer | Element | ✓ | 连接桩标签的容器元素。 | | labelSelectors | Markup.Selectors | | 连接桩标签 Markup 渲染后的选择器键值对。 | | contentContainer | Element | ✓ | 连接桩内容的容器元素。 | | contentSelectors | Markup.Selectors | | 连接桩内容 Markup 渲染后的选择器键值对。 | 例如,我们可以渲染一个 React 类型的连接桩。 ```tsx const graph = new Graph({ container: this.container, onPortRendered(args) { const selectors = args.contentSelectors const container = selectors && selectors.foContent if (container) { ReactDOM.render(
, container, ) } }, }) ``` ### onEdgeLabelRendered ```ts type OnEdgeLabelRenderedArgs = { edge: Edge label: Edge.Label container: Element selectors: Markup.Selectors } ( this: Graph, args: OnEdgeLabelRenderedArgs, ) => void | ((args: OnEdgeLabelRenderedArgs) => void) ``` 当边的文本标签渲染完成时触发的回调,并且它可以返回一个清理函数,该函数会在标签被销毁时执行,参数如下: | 名称 | 类型 | 非空 | 描述 | |-----------|------------------|:----:|-------------------------------------| | edge | Edge | ✓ | 边实例。 | | label | Edge.Label | ✓ | 文本标签选项。 | | container | Element | ✓ | 文本标签容器。 | | selectors | Markup.Selectors | ✓ | 文本标签 Markup 渲染后的选择器键值对。 | 我们可以在定义 Label 的 Markup 时添加 `` 元素来支持 HTML 和 React 的渲染能力。 ```tsx const graph = new Graph({ container: this.container, onEdgeLabelRendered: (args) => { const { selectors } = args const content = selectors.foContent as HTMLDivElement if (content) { content.style.display = 'flex' content.style.alignItems = 'center' content.style.justifyContent = 'center' ReactDOM.render(, content) } // 并且它可以返回一个清理函数,该函数会在标签被销毁时执行 return (edgeLabelRenderedArgs: typeof args) => { // Remove event listeners... } }, }) // 这段代码的主要目的是测试 onEdgeLabelRendered 的清理函数在特定场景下的触发情况 const edge = graph.addEdge({ source: [170, 160], target: [550, 160], labels: [ { attrs: { text: { text: "Custom Label", }, }, }, ], }); setTimeout(() => { edge.prop("labels", ["Updated Label"]); }, 2000); ``` ### createCellView ```ts (this: Graph, cell: Cell) => CellView | null | undefined ``` 自定义元素的视图,可以返回一个 `CellView`,会替换默认的视图,如果返回 `null`,则不会渲染,如果返回 `undefined`,会按照默认方式渲染。 ## 方法 ### findView(...) ```ts findView(ref: Cell | Element): CellView | null ``` 根据节点/边或元素查找对应的视图。 ### findViewByCell(...) ```ts findViewByCell(cellId: string | number): CellView | null findViewByCell(cell: Cell | null): CellView | null ``` 根据节点/边 ID 或实例查找对应的视图。 ### findViewByElem(...) ```ts findViewByElem(elem: string | Element | undefined | null): CellView | null ``` 根据元素选择器或元素对象查找对应的视图。 ### findViewsFromPoint(...) ```ts findViewsFromPoint(x: number, y: number): CellView[] findViewsFromPoint(p: Point.PointLike): CellView[] ``` 返回节点/边的包围盒包含指定点的视图。 ### findViewsInArea(...) ```ts findViewsInArea( x: number, y: number, width: number, height: number, options?: FindViewsInAreaOptions, ): CellView[] findViewsInArea( rect: Rectangle.RectangleLike, options?: FindViewsInAreaOptions, ): CellView[] ``` 返回节点/边的包围盒与指定矩形相交的视图,当 `options.strict` 为 `true` 时需要节点/边的包围盒完全包含指定的矩形。 ### findViews(...) ```ts findViews(ref: Point.PointLike | Rectangle.RectangleLike): CellView[] ``` 返回节点/边的包围盒包含指定的点或与指定的矩形相交的视图。 #### 节点的锚点 - Canonical page: https://x6.antv.antgroup.com/zh/api/registry/node-anchor/ - Markdown source: https://x6.antv.antgroup.com/zh/api/registry/node-anchor.md - Description: 节点的锚点 --- title: "节点的锚点" description: "节点的锚点" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/registry/node-anchor/" version: "3.x" --- 当连接到节点时,可以通过 `NodeAnchor` 来指定被连接的节点的锚点,锚点与连接点 [ConnectionPoint](/zh/api/registry/connection-point) 共同确定了边的起点和终点。 - 起点:从第一个路径点或目标节点的中心(没有路径点时)画一条参考线到源节点的锚点,然后根据 [connectionPoint](/zh/api/registry/connection-point) 指定的交点计算方法,计算参考线与图形的交点,该交点就是边的起点。 - 终点:从最后一个路径点或源节点的中心(没有路径点时)画一条参考线到目标节点的锚点,然后根据 [connectionPoint](/zh/api/registry/connection-point) 指定的交点计算方法,计算参考线与图形的交点,该交点就是边的终点。 X6 内置了以下几种锚点定义。 - [center](#center) 边链接的元素的中心点(默认值)。 - [top](#top) 边链接的元素的顶部中心点。 - [bottom](#bottom) 边链接的元素的底部中心点。 - [left](#left) 边链接的元素的左侧中心点。 - [right](#right) 边链接的元素的右侧中心点。 - [midSide](#midside) 边链接的元素的最近侧中心点。 - [topLeft](#topleft) 边链接的元素的左上角。 - [topRight](#topright) 边链接的元素的右上角。 - [bottomLeft](#bottomleft) 边链接的元素的左下角。 - [bottomRight](#bottomright) 边链接的元素的右下角。 - [orth](#orth) 正交点。 - [nodeCenter](#nodecenter) 节点的中心点。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Edge } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private edge1: Edge private edge2: Edge componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, interacting: false, }) const rect1 = graph.addNode({ x: 160, y: 80, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 460, y: 80, width: 100, height: 40, markup: [ { tagName: 'rect', selector: 'body' }, { tagName: 'rect', selector: 'port' }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, port: { x: 10, y: 10, width: 30, height: 10, strokeWidth: 1, stroke: '#fe8550', }, }, }) const edge1 = graph.addEdge({ source: { x: 100, y: 100 }, target: { cell: rect1.id }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, }) const edge2 = graph.addEdge({ source: { x: 320, y: 100 }, target: { cell: rect2, selector: 'port' }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, }) function animate() { const animateEdge = ( edge: Edge, origin: { x: number; y: number }, radius: number, steps: number, ) => { const p = edge.getSourcePoint() const start = Math.atan2(p.y - origin.y, p.x - origin.x) const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = start + (2 * Math.PI * i) / steps xs.push(origin.x + radius * Math.cos(theta)) ys.push(origin.y + radius * Math.sin(theta)) } edge.animate( { 'source/x': xs, 'source/y': ys, }, { duration: 5000, easing: 'linear', fill: 'forwards', iterations: Infinity, }, ) } const steps = 60 animateEdge(edge1, { x: 210, y: 100 }, 140, steps) animateEdge(edge2, { x: 485, y: 95 }, 120, steps) } animate() edge1.on('animation:finish', animate) this.edge1 = edge1 this.edge2 = edge2 } onAttrsChanged = ({ type }: State) => { this.edge1.prop('target/anchor', { name: type }) this.edge2.prop('target/anchor', { name: type }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 可以在创建边时指定锚点: ```ts const edge = graph.addEdge({ source: { cell: 'source-id', anchor: { name: 'midSide', args: { dx: 10, }, }, }, target: { cell: 'target-id', anchor: 'orth', // 没有参数时可以简化写法 }, }) ``` 创建之后可以调用 `edge.setSource` 和 `edge.setTarget` 方法来修改锚点: ```ts edge.setSource({ cell: 'source-id', anchor: { name: 'midSide', args: { dx: 10, }, }, }) ``` 在创建画布时,可以通过 `connecting` 选项来设置全局默认的锚点: ```ts new Graph({ connecting: { anchor: { name: 'midSide', args: { dx: 10, }, }, }, }) ``` 没有参数时可以简化为: ```ts new Graph({ connecting: { anchor: 'midSide', }, }) ``` ## 内置锚点 ### center 元素中心点,支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | 否 | `0` | X 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | dy | number \| string | 否 | `0` | Y 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | rotate | boolean | 否 | `false` | 是否使用元素跟随节点旋转后的包围盒,默认不考虑旋转角度。 | ### top 元素顶部中心点,支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | 否 | `0` | X 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | dy | number \| string | 否 | `0` | Y 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | rotate | boolean | 否 | `false` | 是否使用元素跟随节点旋转后的包围盒,默认不考虑旋转角度。 | ### bottom 元素底部中心点,支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | 否 | `0` | X 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | dy | number \| string | 否 | `0` | Y 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | rotate | boolean | 否 | `false` | 是否使用元素跟随节点旋转后的包围盒,默认不考虑旋转角度。 | ### left 元素左侧中心点,支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | 否 | `0` | X 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | dy | number \| string | 否 | `0` | Y 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | rotate | boolean | 否 | `false` | 是否使用元素跟随节点旋转后的包围盒,默认不考虑旋转角度。 | ### right 元素右侧中心点,支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | 否 | `0` | X 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | dy | number \| string | 否 | `0` | Y 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | rotate | boolean | 否 | `false` | 是否使用元素跟随节点旋转后的包围盒,默认不考虑旋转角度。 | ### midSide 最靠近边的那一侧中心点,支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |-----------|------------|:-------:|---------|--------------------------------------------------------------------------------------| | padding | number | 否 | `0` | 通过放大元素的包围盒 `padding` 像素,来偏移中心点。 | | rotate | boolean | 否 | `false` | 是否使用元素跟随节点旋转后的包围盒,默认不考虑旋转角度。 | | direction | 'H' \| 'V' | 否 | - | 连接点的方向,比如如果设置为 `H`,只能连接到节点的左侧或者右侧的中心点,会根据位置自动判断。 | ### topLeft 元素左上角,支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | 否 | `0` | X 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | dy | number \| string | 否 | `0` | Y 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | rotate | boolean | 否 | `false` | 是否使用元素跟随节点旋转后的包围盒,默认不考虑旋转角度。 | ### topRight 元素右上角,支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | 否 | `0` | X 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | dy | number \| string | 否 | `0` | Y 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | rotate | boolean | 否 | `false` | 是否使用元素跟随节点旋转后的包围盒,默认不考虑旋转角度。 | ### bottomLeft 元素左下角,支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | 否 | `0` | X 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | dy | number \| string | 否 | `0` | Y 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | rotate | boolean | 否 | `false` | 是否使用元素跟随节点旋转后的包围盒,默认不考虑旋转角度。 | ### bottomRight 元素右下角,支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | 否 | `0` | X 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | dy | number \| string | 否 | `0` | Y 轴偏移量,支持数字绝对偏移量和百分比相对偏移量。 | | rotate | boolean | 否 | `false` | 是否使用元素跟随节点旋转后的包围盒,默认不考虑旋转角度。 | ### orth 正交点,支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |---------|----------|:-------:|--------|---------| | padding | number | 否 | `0` | X 轴偏移量。 | ### nodeCenter 节点的中心,支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|----------|:-------:|--------|---------| | dx | number | 否 | `0` | X 轴偏移量。 | | dy | number | 否 | `0` | Y 轴偏移量。 | ## 自定义锚点 锚点定义是一个具有如下签名的函数,返回锚点。 ```ts export type Definition = ( this: EdgeView, nodeView: NodeView, magnet: SVGElement, ref: Point.PointLike | SVGElement, args: T, type: Edge.TerminalType, ) => Point ``` | 参数名 | 参数类型 | 参数说明 | |----------|-------------------------------|-------------------| | this | EdgeView | 边的视图。 | | nodeView | NodeView | 连接的节点视图。 | | magnet | SVGElement | 连接的节点上的元素。 | | ref | Point.PointLike \| SVGElement | 参照点/元素。 | | args | T | 参数。 | | type | Edge.TerminalType | 边的终端类型。 | 完成锚点定义后,我们先注册锚点: ```ts Graph.registerAnchor('custom-anchor', ...) ``` 注册以后我们就可以通过锚点名称来使用: ```ts new Graph({ connecting: { anchor: { name: 'custom-anchor', }, }, }) ``` ### docs/demos/api/node-anchor/playground/index.less ```less .node-anchor-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/node-anchor/playground/settings.tsx ```tsx import React from 'react' import { Radio, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { type: string } export class Settings extends React.Component { state: State = { type: 'center', } notifyChange() { this.props.onChange(this.state) } onChange = (e: any) => { this.setState({ type: e.target.value }, () => { this.notifyChange() }) } render() { return ( center nodeCenter orth midSide top bottom left right topLeft topRight bottomLeft bottomRight ) } } ``` #### 边的锚点 - Canonical page: https://x6.antv.antgroup.com/zh/api/registry/edge-anchor/ - Markdown source: https://x6.antv.antgroup.com/zh/api/registry/edge-anchor.md - Description: 边的锚点 --- title: "边的锚点" description: "边的锚点" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/registry/edge-anchor/" version: "3.x" --- 当边连接到边时,可以通过 EdgeAnchor 来指定被连接的边上的锚点,锚点与连接点 [ConnectionPoint](/zh/api/registry/connection-point) 共同确定了边的起点和终点。 - 起点:从第一个路径点或目标节点的中心(没有路径点时)画一条参考线到源节点的锚点,然后根据 [connectionPoint](/zh/api/registry/connection-point) 指定的交点计算方法,计算参考线与图形的交点,该交点就是边的起点。 - 终点:从最后一个路径点或源节点的中心(没有路径点时)画一条参考线到目标节点的锚点,然后根据 [connectionPoint](/zh/api/registry/connection-point) 指定的交点计算方法,计算参考线与图形的交点,该交点就是边的终点。 X6 内置了以下几种锚点定义。 - [ratio](#ratio) 默认值,锚点位于被连接的边的指定比例处。 - [length](#length) 锚点位于被连接的边的指定长度处。 - [closest](#closest) 使用距离参照点最近的点作为锚点。 - [orth](#orth) 正交锚点。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Edge } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private edge: Edge componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, interacting: false, }) const rect = graph.addEdge({ source: { x: 40, y: 80 }, target: { x: 360, y: 80 }, vertices: [ { x: 120, y: 120 }, { x: 200, y: 40 }, { x: 280, y: 120 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) const edge = graph.addEdge({ source: { x: 100, y: 100 }, target: { cell: rect.id }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, }) function animate() { const origin = { x: 200, y: 100 } const radius = 140 const steps = 60 const p = edge.getSourcePoint() const start = Math.atan2(p.y - origin.y, p.x - origin.x) const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = start + (2 * Math.PI * i) / steps xs.push(origin.x + radius * Math.cos(theta)) ys.push(origin.y + radius * Math.sin(theta)) } edge.animate( { 'source/x': xs, 'source/y': ys, }, { duration: 5000, easing: 'linear', fill: 'forwards', }, ) } animate() edge.on('animation:finish', animate) this.edge = edge } onAttrsChanged = ({ type, value }: State) => { const anchor = type === 'ratio' || type === 'length' ? { name: type, args: { [type]: value, }, } : { name: type } this.edge.prop('target/anchor', anchor) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 内置锚点 ### ratio 锚点位于被连接的边的指定比例处。支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|----------|:-------:|--------|--------------------------------------------| | ratio | number | 否 | `0.5` | 距离边起点多少比例位置处,默认位于边长度的中心。 | ### length 锚点位于被连接的边的指定长度处。支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|----------|:-------:|--------|----------------------------------------------------| | length | number | 否 | `20` | 距离边的起点多少长度位置处,默认位于偏离起点 `20px` 处。 | ### closest 距离参照点最近的点作为锚点。 ### orth 正交锚点。支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |------------|------------------|:-------:|-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | fallbackAt | number \| string | 否 | `undefined` | 当没有正交点时,使用 `fallbackAt` 指定的点作为锚点。
当 `fallbackAt` 为百分比字符串时,表示锚点位于距离起点多少比例位置处。
当 `fallbackAt` 为数字时,表示锚点位于距离起点多少长度位置处。 | ## 自定义锚点 边的锚点定义是一个具有如下签名的函数,返回锚点。 ```ts export type Definition = ( this: EdgeView, view: EdgeView, magnet: SVGElement, ref: Point.PointLike | SVGElement, args: T, ) => Point ``` | 参数名 | 参数类型 | 参数说明 | |--------|-------------------------------|---------------| | this | EdgeView | 边的视图。 | | view | EdgeView | 连接的边的视图。 | | magnet | SVGElement | 连接的边的元素。 | | ref | Point.PointLike \| SVGElement | 参照点/元素。 | | args | T | 参数。 | 完成锚点定义后,我们先注册锚点: ```ts Graph.registerEdgeAnchor('custom-anchor', ...) ``` 注册以后我们就可以通过锚点名称来使用: ```ts new Graph({ connecting: { edgeAnchor: { name: 'custom-anchor', }, }, }) ``` ### docs/demos/api/edge-anchor/playground/index.less ```less .edge-anchor-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/edge-anchor/playground/settings.tsx ```tsx import React from 'react' import { Radio, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { type: string value: number } export class Settings extends React.Component { state: State = { type: 'ratio', value: 50, } notifyChange() { this.props.onChange(this.state) } onAngleChanged = (value: number) => { this.setState({ value }, () => { this.notifyChange() }) } onChange = (e: any) => { this.setState({ type: e.target.value }, () => { this.notifyChange() }) } render() { return ( ratio length orth closest {(this.state.type === 'ratio' || this.state.type === 'length') && ( {this.state.type === 'ratio' ? 'ratio' : 'length'}
{this.state.value} {this.state.type === 'ratio' ? '%' : ''}
)}
) } } ``` #### 连接点 - Canonical page: https://x6.antv.antgroup.com/zh/api/registry/connection-point/ - Markdown source: https://x6.antv.antgroup.com/zh/api/registry/connection-point.md - Description: 连接点 --- title: "连接点" description: "连接点" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/registry/connection-point/" version: "3.x" --- 连接点 ConnectionPoint 与锚点 [Anchor](/zh/api/registry/node-anchor) 共同确定了边的起点或终点。 - 起点:从第一个路径点或目标节点的中心(没有路径点时)画一条参考线到源节点的锚点,然后根据 `connectionPoint` 指定的交点计算方法,计算参考线与图形的交点,该交点就是边的起点。 - 终点:从最后一个路径点或源节点的中心(没有路径点时)画一条参考线到目标节点的锚点,然后根据 `connectionPoint` 指定的交点计算方法,计算参考线与图形的交点,该交点就是边的终点。 X6 内置了以下几种连接点计算方法。 - [boundary](#boundary) 默认值,与链接图形的边框的交点。 - [bbox](#bbox) 与链接元素的包围盒的交点。 - [rect](#rect) 与链接元素的旋转后的矩形区域的交点。 - [anchor](#anchor) 使用锚点作为连接点。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Edge, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private ellipse: Node private edge1: Edge private edge2: Edge componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, interacting: false, }) const rect = graph.addNode({ x: 160, y: 160, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const ellipse = graph.addNode({ shape: 'ellipse', x: 460, y: 160, width: 100, height: 40, angle: 45, markup: [ { tagName: 'ellipse', selector: 'body' }, { tagName: 'rect', selector: 'outline' }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', stroke: '#8f8f8f', strokeWidth: 1, }, outline: { refWidth: '100%', refHeight: '100%', strokeWidth: 1, strokeDasharray: '5 5', stroke: '#8f8f8f', fill: 'none', }, }, }) let bbox: Node const updateBBox = () => { if (bbox) { bbox.remove() } const rect = ellipse.findView(graph)!.getBBox() bbox = graph.addNode({ x: rect.x, y: rect.y, width: rect.width, height: rect.height, attrs: { body: { strokeWidth: 1, strokeDasharray: '5 5', stroke: '#8f8f8f', fill: 'none', }, }, }) } ellipse.on('change:angle', updateBBox) updateBBox() const edge1 = graph.addEdge({ source: { x: 100, y: 180 }, target: { cell: rect.id }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, }) const edge2 = graph.addEdge({ source: { x: 320, y: 180 }, target: { cell: ellipse.id }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, }) function animate() { const animateEdge = ( edge: Edge, origin: { x: number; y: number }, radius: number, steps: number, ) => { const p = edge.getSourcePoint() const start = Math.atan2(p.y - origin.y, p.x - origin.x) const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = start + (2 * Math.PI * i) / steps xs.push(origin.x + radius * Math.cos(theta)) ys.push(origin.y + radius * Math.sin(theta)) } edge.animate( { 'source/x': xs, 'source/y': ys, }, { duration: 5000, easing: 'linear', fill: 'forwards', }, ) } const steps = 60 animateEdge(edge1, { x: 210, y: 180 }, 120, steps) animateEdge(edge2, { x: 485, y: 180 }, 120, steps) } animate() edge1.on('animation:finish', animate) this.edge1 = edge1 this.edge2 = edge2 this.ellipse = ellipse } updateBBox() {} onAttrsChanged = ({ anchor, connectionPoint, angle }: State) => { this.edge1.prop('target/anchor', { name: anchor }) this.edge1.prop('target/connectionPoint', { name: connectionPoint }) this.edge2.prop('target/anchonr', { name: anchor }) this.edge2.prop('target/connectionPoint', { name: connectionPoint }) this.ellipse.rotate(angle, { absolute: true }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 可以在创建边时指定连接点: ```ts const edge = graph.addEdge({ source: { cell: 'source-id', connectionPoint: { name: 'boundary', args: { sticky: true, }, }, }, target: { cell: 'target-id', connectionPoint: 'boundary', // 没有参数时可以简化写法 }, }) ``` 创建之后可以调用 `edge.setSource` 和 `edge.setTarget` 方法来修改连接点: ```ts edge.setSource({ cell: 'source-id', connectionPoint: { name: 'boundary', args: { sticky: true, }, }, }) ``` 在创建画布时,可以通过 `connecting` 选项来设置全局默认的连接点: ```ts new Graph({ connecting: { connectionPoint: { name: 'boundary', args: { sticky: true, }, }, }, }) ``` 没有参数时可以简化为: ```ts new Graph({ connecting: { connectionPoint: 'boundary', }, }) ``` ## 内置连接点 ### boundary 自动识别链接图形的边框,并计算参照点与锚点(Anchor)形成的直线与边框的交点。如 `` 元素将被识别为椭圆,并求椭圆与直线的交点,求得的交点为边的连接点。不能识别的元素(`text`、`` 等)使用图形的包围盒代替,这与使用 `'bbox'` 求得的连接点一样。 支持的参数如下表: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |-------------|---------------------------|:-------:|-------------|--------------------------------------------------------------------------------------------------------------------| | offset | number \| Point.PointLike | 否 | `0` | 连接点的偏移量。 | | stroked | boolean | 否 | `true` | 是否考虑图形的边框宽度。 | | insideout | boolean | 否 | `true` | 当参考线位于图形内部且没有交点时,是否延长参考线求交点,默认为 `true`。 | | extrapolate | boolean | 否 | `false` | 当参考线位于图形外部且没有交点时,是否延长参考线求交点,延长后也可能没有交点,默认为 `false`。此参数的优先级高于 `sticky`。 | | sticky | boolean | 否 | `false` | 当参考线位于图形外部且没有交点时,是否使用边框上离参考线最最近的点作为交点,默认为 `false`。 | | precision | number | 否 | `2` | 交点计算的精度。 | | selector | string | 否 | `undefined` | 选择器,用于标识一个元素,使用该元素的边框来计算交点。默认使用节点中第一个不在 `` 元素内的子元素。 | ### anchor 将锚点作为连接点,支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |--------|---------------------------|:-------:|--------|-------------| | offset | number \| Point.PointLike | 否 | `0` | 连接点的偏移量。 | ### bbox 图形的包围盒与参考线的交点,支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |---------|---------------------------|:-------:|---------|---------------------| | offset | number \| Point.PointLike | 否 | `0` | 连接点的偏移量。 | | stroked | boolean | 否 | `false` | 是否考虑图形的边框宽度。 | ### rect 图形旋转后的矩形区域与参考线的交点,支持如下参数: | 参数名 | 参数类型 | 是否必选 | 默认值 | 参数说明 | |---------|---------------------------|:-------:|---------|---------------------| | offset | number \| Point.PointLike | 否 | `0` | 连接点的偏移量。 | | stroked | boolean | 否 | `false` | 是否考虑图形的边框宽度。 | ## 自定义连接点 连接点定义是一个具有如下签名的函数,返回连接点。 ```ts export type Definition = ( line: Line, view: NodeView, magnet: SVGElement, args: T, ) => Point ``` | 参数名 | 参数类型 | 参数说明 | |----------|------------|-------------------| | line | Line | 参考线。 | | nodeView | NodeView | 连接的节点视图。 | | magnet | SVGElement | 连接的节点上的元素。 | | args | T | 参数。 | 完成连接点定义后,我们先注册连接点: ```ts Graph.registerConnectionPoint('custom-connection-point', ...) ``` 注册以后我们就可以通过连接点名称来使用: ```ts new Graph({ connecting: { connectionPoint: 'custom-connection-point' }, }) ``` ### docs/demos/api/connection-point/playground/index.less ```less .connection-point-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/connection-point/playground/settings.tsx ```tsx import React from 'react' import { Radio, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { anchor: string connectionPoint: string angle: number } export class Settings extends React.Component { state: State = { anchor: 'center', connectionPoint: 'boundary', angle: 45, } notifyChange() { this.props.onChange(this.state) } onAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } onAnchorChange = (e: any) => { this.setState({ anchor: e.target.value }, () => { this.notifyChange() }) } onConnectionPointChange = (e: any) => { this.setState({ connectionPoint: e.target.value }, () => { this.notifyChange() }) } render() { return ( anchor center nodeCenter orth midSide top bottom left right topLeft topRight bottomLeft bottomRight connectionPoint boundary anchor bbox rect angle
{this.state.angle}°
) } } ``` #### 连接桩布局 - Canonical page: https://x6.antv.antgroup.com/zh/api/registry/port-layout/ - Markdown source: https://x6.antv.antgroup.com/zh/api/registry/port-layout.md - Description: 连接桩布局 --- title: "连接桩布局" description: "连接桩布局" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/registry/port-layout/" version: "3.x" --- 连接桩布局算法是一个函数具有如下签名的函数,返回连接桩相对于节点的相对位置。例如,某节点在画布的位置是 `{ x: 30, y: 40 }`,如果返回的某个连接桩的位置是 `{ x: 2, y: 4 }`,那么该连接桩渲染到画布后的位置是 `{ x: 32, y: 44 }`。 ```ts type Definition = ( portsPositionArgs: T[], // 连接桩中指定的布局算法参数 elemBBox: Rectangle, // 节点的包围盒 groupPositionArgs: T, // group 中定义的默认布局算法参数 ) => Result[] interface Result { position: Point.PointLike // 相对于节点的位置 angle?: number // 旋转角度 } ``` 需要注意的是,配置连接桩 `ports` 时,我们只能通过 `groups` 选项来配置布局算法,而在 `items` 中可以提供可选的布局算法参数 `args` 来影响布局结果。 ```ts graph.addNode( ..., ports: { // 连接桩分组 groups: { group1: { position: { name: 'xxx', // 布局算法名称 args: { }, // 布局算法的默认参数 }, }, }, // 连接桩定义 items: [ { groups: 'group1', args: { }, // 覆盖 group1 中指定的默认参数 }, ], }, ) ``` 下面我们一起来看看如何使用内置的连接桩布局算法,以及如何自定并注册自定义布局算法。 ## 内置布局 ### absolute 绝对定位,通过 `args` 指定连接桩的位置。 ```ts interface AbsoluteArgs { x?: string | number y?: string | number angle?: number } ``` | 名称 | 类型 | 必选 | 默认值 | 描述 | |-------|------------------|:----:|--------|----------------------| | x | string \| number | | `0` | 连接桩在 X 轴相对位置。 | | y | string \| number | | `0` | 连接桩在 Y 轴相对位置。 | | angle | number | | `0` | 连接桩旋转角度。 | 当 `x` 和 `y` 为百分比字符串或位于 `[0, 1]` 之间时,表示在宽度和高度方向的百分比偏移量,否则表示绝对偏移量。 ```ts graph.addNode({ ports: { groups: { group1: { position: { name: 'absolute', args: { x: 0, y: 0 }, }, }, }, items: [ { group: 'group1', args: { x: '60%', y: 32, angle: 45, }, }, ], }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ x: 120, y: 48, width: 280, height: 120, markup: [ { tagName: 'rect', selector: 'body', }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, }, ports: { groups: { group1: { attrs: { circle: { r: 6, magnet: true, stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, text: { fontSize: 12, fill: '#888', }, }, position: { name: 'absolute', }, }, }, items: [ { id: 'port1', group: 'group1', args: { x: 0, y: 60 }, attrs: { text: { text: '{ x: 0, y: 60 }' }, }, }, { id: 'port2', group: 'group1', args: { x: 0.6, y: 32, angle: 45 }, markup: [ { tagName: 'path', selector: 'path', }, ], zIndex: 10, attrs: { path: { d: 'M -6 -8 L 0 8 L 6 -8 Z', magnet: true, fill: '#8f8f8f', }, text: { text: '{ x: 0.6, y: 32, angle: 45 }', fill: '#888' }, }, }, { id: 'port3', group: 'group1', args: { x: '100%', y: '100%' }, attrs: { text: { text: "{ x: '100%', y: '100%' }" }, }, label: { position: { name: 'right', }, }, }, ], }, }) } onAttrsChanged = (args: State) => { this.node.portProp('port2', { args, attrs: { text: { text: `{ x: ${args.x}, y: ${args.y}, angle: ${args.angle} }` }, }, } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### left, right, top, bottom 连接桩沿矩形指定边线均匀布局,`left`、`right`、`top` 和 `bottom` 四个布局对矩形形状的节点非常友好,可以通过 `args` 来设置偏移量和旋转角度。 ```ts interface SideArgs { dx?: number dy?: number angle?: number x?: number y?: number } ``` | 名称 | 类型 | 必选 | 默认值 | 描述 | |--------|---------|:----:|---------|---------------------------------------| | strict | boolean | | `false` | 是否严格等分均匀分布。 | | dx | number | | `0` | 沿 X 轴方向的偏移量。 | | dy | number | | `0` | 沿 Y 轴方向的偏移量。 | | angle | number | | `0` | 连接桩的旋转角度。 | | x | number | | - | 用指定的 X 坐标覆盖计算结果中的 X 坐标。 | | y | number | | - | 用指定的 Y 坐标覆盖计算结果中的 Y 坐标。 | ```ts graph.addNode({ ports: { groups: { group1: { position: 'left', }, }, items: [ { group: 'group1', args: { dx: 2, }, }, ], }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ x: 160, y: 110, width: 280, height: 120, attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, label: { text: 'left', fill: '#888' }, }, ports: { groups: { group1: { attrs: { circle: { r: 6, magnet: true, stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, text: { fontSize: 12, fill: '#888', }, }, position: { name: 'left', }, }, }, items: [ { id: 'port1', group: 'group1', }, { id: 'port2', group: 'group1', args: { angle: 45 }, markup: [ { tagName: 'path', selector: 'path', }, ], attrs: { path: { d: 'M -6 -8 L 0 8 L 6 -8 Z', magnet: true, fill: '#8f8f8f', }, }, }, { id: 'port3', group: 'group1', args: {}, }, ], }, }) } onAttrsChanged = ({ position, strict, ...args }: State) => { this.node.prop('ports/groups/group1/position', { name: position, args: { strict }, }) this.node.attr('label/text', position) this.node.portProp('port2', { args } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### line 连接桩沿线段均匀分布。 ```ts interface LineArgs { start?: Point.PointLike end?: Point.PointLike dx?: number dy?: number angle?: number x?: number y?: number } ``` | 名称 | 类型 | 必选 | 默认值 | 描述 | |--------|-----------------|:----:|---------|---------------------------------------| | start | Point.PointLike | | | 线段起点。 | | end | Point.PointLike | | | 线段终点。 | | strict | boolean | | `false` | 是否严格等分均匀分布。 | | dx | number | | `0` | 沿 X 轴方向的偏移量。 | | dy | number | | `0` | 沿 Y 轴方向的偏移量。 | | angle | number | | `0` | 连接桩的旋转角度。 | | x | number | | - | 用指定的 X 坐标覆盖计算结果中的 X 坐标。 | | y | number | | - | 用指定的 Y 坐标覆盖计算结果中的 Y 坐标。 | ```ts graph.addNode({ ports: { groups: { group1: { position: { name: 'line', args: { start: { x: 10, y: 10 }, end: { x: 210, y: 10 }, }, }, }, }, items: [ { group: 'group1', args: { dx: 2, }, }, ], }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ x: 160, y: 80, width: 280, height: 120, markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'path', selector: 'line', }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, line: { d: 'M 0 0 280 120', stroke: '#8f8f8f', strokeDasharray: '5 5', }, }, ports: { groups: { group1: { attrs: { circle: { r: 6, magnet: true, stroke: '#8f8f8f', strokeWidth: 2, fill: '#fff', }, text: { fontSize: 12, fill: '#888', }, }, position: { name: 'line', args: { start: { x: 0, y: 0 }, end: { x: 280, y: 120 }, }, }, }, }, items: [ { id: 'port1', group: 'group1', }, { id: 'port2', group: 'group1', args: { angle: 45 }, markup: [ { tagName: 'path', selector: 'path', }, ], attrs: { path: { d: 'M -6 -8 L 0 8 L 6 -8 Z', magnet: true, fill: '#8f8f8f', }, }, }, { id: 'port3', group: 'group1', args: {}, }, ], }, }) } onAttrsChanged = ({ strict, ...args }: State) => { this.node.prop('ports/groups/group1/position', { name: 'line', args: { strict }, }) this.node.portProp('port2', { args } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### ellipse 沿圆弧分布的连接桩,从 `start` 指定的角度开始,以 `step` 为步长均匀分布。 ```ts interface EllipseArgs { start?: number step?: number compensateRotate?: boolean dr?: number dx?: number dy?: number angle?: number x?: number y?: number } ``` | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|--------|:----:|---------|---------------------------------------| | start | number | | | 起始角度。 | | step | number | | `20` | 步长。 | | compensateRotate | number | | `false` | 是否沿圆弧修正连接桩的旋转角度。 | | dr | number | | `0` | 沿半径方向的偏移量。 | | dx | number | | `0` | 沿 X 轴方向的偏移量。 | | dy | number | | `0` | 沿 Y 轴方向的偏移量。 | | angle | number | | `0` | 连接桩的旋转角度。 | | x | number | | - | 用指定的 X 坐标覆盖计算结果中的 X 坐标。 | | y | number | | - | 用指定的 Y 坐标覆盖计算结果中的 Y 坐标。 | ```ts const node = graph.addNode({ ports: { groups: { group1: { position: { name: 'ellipse', args: { start: 45, }, }, }, }, }, }) Array.from({ length: 10 }).forEach((_, index) => { node.addPort({ id: `${index}`, group: 'group1', attrs: { text: { text: index } }, }) }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Point } from '@antv/x6' import { Settings, State } from './settings.tsx' function getPathData(deg: number) { const center = new Point(180, 100) const start = new Point(180, 0) const ratio = center.x / center.y const p = start .clone() .rotate(90 - deg, center) .scale(ratio, 1, center) return `M ${center.x} ${center.y} ${p.x} ${p.y}` } export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ x: 120, y: 90, width: 360, height: 200, shape: 'ellipse', markup: [ { tagName: 'ellipse', selector: 'body' }, { tagName: 'text', selector: 'label' }, { tagName: 'path', selector: 'line' }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, line: { d: getPathData(45), stroke: '#8f8f8f', strokeDasharray: '5 5', }, }, ports: { groups: { group1: { markup: [ { tagName: 'rect', selector: 'rect', }, { tagName: 'circle', selector: 'dot', }, ], attrs: { rect: { magnet: true, stroke: '#8f8f8f', fill: 'rgba(255,255,255,0.8)', strokeWidth: 1, width: 16, height: 16, x: -8, y: -8, }, dot: { fill: '#8f8f8f', r: 2, }, text: { fontSize: 12, fill: '#888', }, }, label: { position: 'radial', }, position: { name: 'ellipse', args: { start: 45, }, }, }, }, }, }) Array.from({ length: 10 }).forEach((_, index) => { this.node.addPort({ id: `${index}`, group: 'group1', attrs: { text: { text: index } }, }) }) this.node.portProp('0', { attrs: { rect: { stroke: '#8f8f8f' }, dot: { fill: '#8f8f8f' }, }, }) } onAttrsChanged = ({ start, step, compensateRotate, ...args }: State) => { this.node.prop('ports/groups/group1/position/args', { start, step, compensateRotate, }) this.node.attr({ line: { d: getPathData(start) }, }) this.node.portProp('0', { args, } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### ellipseSpread 沿椭圆均匀分布的连接桩,从 `start` 指定的角度开始均匀分布。 ```ts interface EllipseSpreadArgs { start?: number compensateRotate?: boolean dr?: number dx?: number dy?: number angle?: number x?: number y?: number } ``` | 名称 | 类型 | 必选 | 默认值 | 描述 | |------------------|--------|:----:|---------|---------------------------------------| | start | number | | | 起始角度。 | | compensateRotate | number | | `false` | 是否沿圆弧修正连接桩的旋转角度。 | | dr | number | | `0` | 沿半径方向的偏移量。 | | dx | number | | `0` | 沿 X 轴方向的偏移量。 | | dy | number | | `0` | 沿 Y 轴方向的偏移量。 | | angle | number | | `0` | 连接桩的旋转角度。 | | x | number | | - | 用指定的 X 坐标覆盖计算结果中的 X 坐标。 | | y | number | | - | 用指定的 Y 坐标覆盖计算结果中的 Y 坐标。 | ```ts const node = graph.addNode({ ports: { groups: { group1: { position: { name: 'ellipseSpread', args: { start: 45, }, }, }, }, }, }) Array.from({ length: 36 }).forEach(function (_, index) { ellipse.addPort({ group: 'group1', id: `${index}`, attrs: { text: { text: index } }, }) }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ x: 120, y: 90, width: 360, height: 200, shape: 'ellipse', attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, }, ports: { groups: { group1: { markup: [ { tagName: 'rect', selector: 'rect', }, { tagName: 'circle', selector: 'dot', }, ], attrs: { rect: { magnet: true, stroke: '#8f8f8f', fill: 'rgba(255,255,255,0.8)', strokeWidth: 1, width: 16, height: 16, x: -8, y: -8, }, dot: { fill: '#8f8f8f', r: 2, }, text: { fontSize: 12, fill: '#888', }, }, label: { position: 'radial', }, position: { name: 'ellipseSpread', args: { start: 45, }, }, }, }, }, }) Array.from({ length: 10 }).forEach((_, index) => { this.node.addPort({ id: `${index}`, group: 'group1', attrs: { text: { text: index } }, }) }) this.node.portProp('0', { attrs: { rect: { stroke: '#8f8f8f' }, dot: { fill: '#8f8f8f' }, }, }) } onAttrsChanged = ({ start, compensateRotate, ...args }: State) => { this.node.prop('ports/groups/group1/position/args', { start, compensateRotate, }) this.node.portProp('0', { args, } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 自定义连接桩布局 连接桩布局算法是一个函数具有如下签名的函数,返回每个连接桩相对于节点的相对位置。例如,某节点在画布的位置是 `{ x: 30, y: 40 }`,如果返回的某个连接桩的位置是 `{ x: 2, y: 4 }`,那么该连接桩渲染到画布后的位置是 `{ x: 32, y: 44 }`。 ```ts type Definition = ( portsPositionArgs: T[], // 连接桩中指定的布局算法参数 elemBBox: Rectangle, // 节点的包围盒 groupPositionArgs: T, // group 中定义的默认布局算法参数 ) => Result[] interface Result { position: Point.PointLike // 相对于节点的位置 angle?: number // 旋转角度 } ``` 所以我们可以按照上面规则来创建自定义布局算法,例如,实现一个正弦分布的布局算法: ```ts function sin(portsPositionArgs, elemBBox) { return portsPositionArgs.map((_, index) => { const step = -Math.PI / 8 const y = Math.sin(index * step) * 50 return { position: { x: index * 12, y: y + elemBBox.height, }, angle: 0, } }) } ``` 布局算法实现后,需要注册到系统,注册后就可以像内置布局算法那样来使用。 ```ts Graph.registerPortLayout('sin', sin) ``` 注册以后,我们就可以像内置布局算法那样来使用: ```ts const rect = graph.addNode({ ports: { groups: { sin: { position: { name: 'sin', args: { start: 45, }, }, attrs: { rect: { fill: '#fe854f', width: 11, }, text: { fill: '#fe854f', }, circle: { fill: '#fe854f', r: 5, magnet: true, }, }, }, }, }, }) Array.from({ length: 24 }).forEach(() => { rect.addPort({ group: 'sin' }) }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerPortLayout( 'sin', (portsPositionArgs, elemBBox) => { return portsPositionArgs.map((_, index) => { const step = -Math.PI / 8 const y = Math.sin(index * step) * 50 return { position: { x: index * 12, y: y + elemBBox.height, }, angle: 0, } }) }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect = graph.addNode({ x: 120, y: 40, width: 280, height: 120, attrs: { body: { fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, }, ports: { groups: { sin: { attrs: { circle: { r: 6, magnet: true, stroke: '#8f8f8f', strokeWidth: 1, fill: '#fe854f', }, }, position: 'sin', }, }, }, }) Array.from({ length: 24 }).forEach(() => { rect.addPort({ group: 'sin' }) }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/api/port-layout/absolute/index.less ```less .port-absolute-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-layout/absolute/settings.tsx ```tsx import React from 'react' import { Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { x: number y: number angle: number } export class Settings extends React.Component { state: State = { x: 0.6, y: 32, angle: 45, } notifyChange() { this.props.onChange(this.state) } onXChanged = (x: number) => { this.setState({ x }, () => { this.notifyChange() }) } onYChanged = (y: number) => { this.setState({ y }, () => { this.notifyChange() }) } onAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } render() { return ( x
{this.state.x}
y
{this.state.y}
angle
{this.state.angle}
) } } ``` ### docs/demos/api/port-layout/side/index.less ```less .port-side-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-layout/side/settings.tsx ```tsx import React from 'react' import { Radio, Switch, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { position: string strict: boolean dx: number dy: number angle: number } export class Settings extends React.Component { state: State = { position: 'left', strict: false, dx: 0, dy: 0, angle: 45, } notifyChange() { this.props.onChange(this.state) } onDxChanged = (dx: number) => { this.setState({ dx }, () => { this.notifyChange() }) } onDyChanged = (dy: number) => { this.setState({ dy }, () => { this.notifyChange() }) } onAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } onStrictChange = (strict: boolean) => { this.setState({ strict }, () => { this.notifyChange() }) } onPositionChange = (e: any) => { this.setState( { position: e.target.value, }, () => { this.notifyChange() }, ) } render() { return ( position left right top bottom strict dx
{this.state.dx}
dy
{this.state.dy}
angle
{this.state.angle}
) } } ``` ### docs/demos/api/port-layout/line/index.less ```less .port-line-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-layout/line/settings.tsx ```tsx import React from 'react' import { Slider, Switch, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { strict: boolean dx: number dy: number angle: number } export class Settings extends React.Component { state: State = { strict: false, dx: 0, dy: 0, angle: 45, } notifyChange() { this.props.onChange(this.state) } onStrictChange = (strict: boolean) => { this.setState({ strict }, () => { this.notifyChange() }) } onDxChanged = (dx: number) => { this.setState({ dx }, () => { this.notifyChange() }) } onDyChanged = (dy: number) => { this.setState({ dy }, () => { this.notifyChange() }) } onAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } render() { return ( strict dx
{this.state.dx}
dy
{this.state.dy}
angle
{this.state.angle}
) } } ``` ### docs/demos/api/port-layout/ellipse/index.less ```less .port-ellipse-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-layout/ellipse/settings.tsx ```tsx import React from 'react' import { Slider, Checkbox, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { start: number step: number compensateRotate: boolean dr: number dx: number dy: number angle: number } export class Settings extends React.Component { state: State = { start: 45, step: 20, compensateRotate: false, dr: 0, dx: 0, dy: 0, angle: 45, } notifyChange() { this.props.onChange(this.state) } onStartChanged = (start: number) => { this.setState({ start }, () => { this.notifyChange() }) } onStepChanged = (step: number) => { this.setState({ step }, () => { this.notifyChange() }) } onCompensateRotateChange = (e: any) => { this.setState({ compensateRotate: e.target.checked }, () => { this.notifyChange() }) } onDxChanged = (dx: number) => { this.setState({ dx }, () => { this.notifyChange() }) } onDrChanged = (dr: number) => { this.setState({ dr }, () => { this.notifyChange() }) } onDyChanged = (dy: number) => { this.setState({ dy }, () => { this.notifyChange() }) } onAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } render() { return ( start
{this.state.start}
step
{this.state.step}
compensateRotate dr
{this.state.dr}
dx
{this.state.dx}
dy
{this.state.dy}
angle
{this.state.angle}
) } } ``` ### docs/demos/api/port-layout/ellipse-spread/index.less ```less .port-ellipse-spread-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-layout/ellipse-spread/settings.tsx ```tsx import React from 'react' import { Slider, Checkbox, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { start: number compensateRotate: boolean dr: number dx: number dy: number angle: number } export class Settings extends React.Component { state: State = { start: 45, compensateRotate: false, dr: 0, dx: 0, dy: 0, angle: 45, } notifyChange() { this.props.onChange(this.state) } onStartChanged = (start: number) => { this.setState({ start }, () => { this.notifyChange() }) } onCompensateRotateChange = (e: any) => { this.setState({ compensateRotate: e.target.checked }, () => { this.notifyChange() }) } onDxChanged = (dx: number) => { this.setState({ dx }, () => { this.notifyChange() }) } onDrChanged = (dr: number) => { this.setState({ dr }, () => { this.notifyChange() }) } onDyChanged = (dy: number) => { this.setState({ dy }, () => { this.notifyChange() }) } onAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } render() { return ( start
{this.state.start}
compensateRotate dr
{this.state.dr}
dx
{this.state.dx}
dy
{this.state.dy}
angle
{this.state.angle}
) } } ``` ### docs/demos/api/port-layout/sin/index.less ```less .port-sin-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### 连接桩标签布局 - Canonical page: https://x6.antv.antgroup.com/zh/api/registry/port-label-layout/ - Markdown source: https://x6.antv.antgroup.com/zh/api/registry/port-label-layout.md - Description: 连接桩标签布局 --- title: "连接桩标签布局" description: "连接桩标签布局" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/registry/port-label-layout/" version: "3.x" --- 连接桩标签布局算法是一个具有如下签名的函数,返回标签相对于连接桩的位置和旋转角度。 ```ts type Definition = ( portPosition: Point, // 连接桩的位置 elemBBox: Rectangle, // 节点的包围盒 args: T, // 标签位置参数 ) => Result interface Result { position: Point.PointLike // 标签相对于位置 angle: number // 标签的旋转角度 attrs: Attr.CellAttrs // 标签的属性 } ``` 创建连接桩时,可以在群组 `groups` 中指定该组的标签定位,也可以在 `items` 中覆盖群组中的定义: ```ts graph.addNode( ..., ports: { // 连接桩分组 groups: { group1: { label: { position: { // 标签布局算法 name: 'xxx', // 标签布局算法名称 args: { ... }, // 标签布局算法参数 }, }, }, }, // 连接桩定义 items: [ { groups: 'group1', label: { position: { // 覆盖 group1 中指定的标签定位算法 name: 'xxx', args: { ... }, } }, } ], }, ) ``` 下面我们一起来看看如何使用内置的标签布局算法,以及如何自定并注册自定义布局算法。 ## 内置连接桩标签布局 ### Side 标签位于连接桩的某一侧。 - `'left'` 标签位于连接桩左侧。 - `'right'` 标签位于连接桩右侧。 - `'top'` 标签位于连接桩顶部。 - `'bottom'` 标签位于连接桩底部。 可以通过 `args` 来指定标签的位置和旋转角度。 ```ts interface SideArgs { x?: number y?: number angle?: number attrs?: Attr.CellAttrs } ``` | 名称 | 类型 | 必选 | 默认值 | 描述 | |-------|----------------|:----:|--------|-----------------------------------------| | x | number | | - | 用指定的 X 坐标替换计算结果中的 X 坐标。 | | y | number | | - | 用指定的 Y 坐标替换计算结果中的 Y 坐标。 | | angle | number | | - | 用指定的旋转角度替换计算结果中的旋转角度。 | | attrs | Attr.CellAttrs | | - | 标签属性。 | ```ts label: { position: { name : 'right', args: { y: 10, // 强制指定 y 坐标为 10,替换计算结果中的 y 坐标 attrs: { text: { fill: 'red', // 设置标签颜色为红色 }, }, }, }, } ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ shape: 'rect', x: 80, y: 68, width: 240, height: 80, attrs: { label: { text: 'left', fill: '#6a6c8a', }, body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { a: { position: { name: 'top', args: { dr: 0, dx: 0, dy: -10, }, }, label: { position: { name: 'left', }, }, attrs: { circle: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, r: 10, }, text: { fill: '#6a6c8a', }, }, }, }, }, }) Array.from({ length: 3 }).forEach((_, index) => { const label = index === 2 ? { position: { args: { x: 20, y: -20 } }, } : {} const stroke = index === 2 ? { stroke: 'red' } : {} const fill = index === 2 ? { fill: 'red' } : {} this.node.addPort({ label, group: 'a', attrs: { circle: { magnet: true, ...stroke }, text: { text: `P${index}`, ...fill }, }, }) }) } onAttrsChanged = ({ position }: State) => { this.node.prop('ports/groups/a/label/position/name', position) this.node.attr('label/text', position) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### Inside/Outside 标签位于节点的内部或者外部,支持一下四种布局: - `'inside'` 标签位于节点内围(靠近边线的内侧)。 - `'outside'` 标签位于节点外围(靠近边线的外侧)。 - `'insideOriented'` 标签位于节点内围,而且根据坐所在位自动调整文本的方向。 - `'outsideOriented'` 标签位于节点外围,而且根据坐所在位自动调整文本的方向。 支持通过 `args.offset` 来设置标签沿节点中到标签位置方向的偏移量。 ```ts interface InOutArgs { offset?: number x?: number y?: number angle?: number attrs?: Attr.CellAttrs } ``` | 名称 | 类型 | 必选 | 默认值 | 描述 | |--------|----------------|:----:|--------|-----------------------------------------| | offset | number | | `15` | 从节点中心到标签位置的方向上的偏移量。 | | x | number | | - | 用指定的 X 坐标替换计算结果中的 X 坐标。 | | y | number | | - | 用指定的 Y 坐标替换计算结果中的 Y 坐标。 | | angle | number | | - | 用指定的旋转角度替换计算结果中的旋转角度。 | | attrs | Attr.CellAttrs | | - | 标签属性。 | ```ts label: { position: { name : 'outside', }, } ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ shape: 'ellipse', x: 70, y: 85, width: 260, height: 100, attrs: { label: { text: 'outside', fill: '#888', fontSize: 12, }, body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { a: { position: { name: 'ellipseSpread', args: { compensateRotate: true, }, }, label: { position: { name: 'outside', }, }, attrs: { circle: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, r: 10, magnet: true, }, text: { fill: '#6a6c8a', fontSize: 12, }, }, }, }, }, }) Array.from({ length: 10 }).forEach((_, index) => { this.node.addPort({ attrs: { text: { text: `P ${index}` } }, group: 'a' }) }) } onAttrsChanged = ({ position, offset }: State) => { this.node.prop('ports/groups/a/label/position', { name: position, args: { offset }, }) this.node.attr('label/text', position) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### Radial 将标签放在圆形或椭圆形节点的外围。支持一下两种布局: - `'radial'` 标签位于圆形或椭圆形节点的外围。 - `'radialOriented'` 标签位于圆形或椭圆形节点的外围,并使标签文本自动沿圆弧方向旋转。 ```ts interface RadialArgs { offset?: number x?: number y?: number angle?: number attrs?: Attr.CellAttrs } ``` | 名称 | 类型 | 必选 | 默认值 | 描述 | |--------|----------------|:----:|--------|-----------------------------------------| | offset | number | | `20` | 从节点中心到标签位置的方向上的偏移量。 | | x | number | | - | 用指定的 X 坐标替换计算结果中的 X 坐标。 | | y | number | | - | 用指定的 Y 坐标替换计算结果中的 Y 坐标。 | | angle | number | | - | 用指定的旋转角度替换计算结果中的旋转角度。 | | attrs | Attr.CellAttrs | | - | 标签属性。 | ```ts label: { position: { name : 'radial', }, } ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ shape: 'ellipse', x: 70, y: 50, width: 260, height: 100, attrs: { label: { text: 'outside', fill: '#888', fontSize: 12, }, body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { a: { position: { name: 'ellipseSpread', args: { compensateRotate: true, }, }, label: { position: { name: 'outside', }, }, attrs: { circle: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, r: 10, magnet: true, }, text: { fill: '#6a6c8a', fontSize: 12, }, }, }, }, }, }) Array.from({ length: 10 }).forEach((_, index) => { this.node.addPort({ attrs: { text: { text: `P ${index}` } }, group: 'a' }) }) } onAttrsChanged = ({ position, offset }: State) => { this.node.prop('ports/groups/a/label/position', { name: position, args: { offset }, }) this.node.attr('label/text', position) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 自定义连接桩标签布局 连接桩标签定位是一个具有如下签名的函数,返回标签相对于连接桩的位置和旋转角度。 ```ts type Definition = ( portPosition: Point, // 连接桩的位置 elemBBox: Rectangle, // 节点的包围盒 args: T, // 标签位置参数 ) => Result interface Result { position: Point.PointLike // 标签相对于位置 angle: number // 标签的旋转角度 attrs: Attr.CellAttrs // 标签的属性 } ``` 所以我们可以按照上面规则来创建自定义布局算法,例如,实现一个位于连接桩右下角的布局: ```ts function bottomRight(portPosition, elemBBox, args) { const dx = args.dx || 10 const dy = args.dy || 10 return { position: { portPosition.x + dx, portPosition.y + dy, } } } ``` 布局算法实现后,需要注册到系统,注册后就可以像内置布局算法那样来使用。 ```ts Graph.registerPortLabelLayout('bottomRight', bottomRight) ``` 注册以后,我们就可以像内置布局算法那样来使用: ```ts const rect = graph.addNode({ ports: { groups: { group1: { position: { name: 'top', }, label: { position: { name: 'bottomRight', }, }, }, }, items: [ { id: 'port1', group: 'group1' }, { id: 'port2', label: { position: 'bottomRight' } }, ], }, }) ``` ### docs/demos/api/port-label-layout/side/index.less ```less .port-label-side-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-label-layout/side/settings.tsx ```tsx import React from 'react' import { Radio, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { position: string dx: number dy: number angle: number } export class Settings extends React.Component { state: State = { position: 'left', dx: 0, dy: 0, angle: 45, } notifyChange() { this.props.onChange(this.state) } onDxChanged = (dx: number) => { this.setState({ dx }, () => { this.notifyChange() }) } onDyChanged = (dy: number) => { this.setState({ dy }, () => { this.notifyChange() }) } onAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } onPositionChange = (e: any) => { this.setState( { position: e.target.value, }, () => { this.notifyChange() }, ) } render() { const radioStyle = { display: 'block', height: '36px', lineHeight: '36px', } return ( left right top bottom ) } } ``` ### docs/demos/api/port-label-layout/inside-outside/index.less ```less .port-label-inside-outside-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-label-layout/inside-outside/settings.tsx ```tsx import React from 'react' import { Radio, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { position: string offset: number } export class Settings extends React.Component { state: State = { position: 'outside', offset: 15, } notifyChange() { this.props.onChange(this.state) } onOffsetChanged = (offset: number) => { this.setState({ offset }, () => { this.notifyChange() }) } onPositionChange = (e: any) => { this.setState( { position: e.target.value, }, () => { this.notifyChange() }, ) } render() { const radioStyle = { display: 'block', height: '36px', lineHeight: '36px', } return ( inside outside insideOriented outsideOriented offset
{this.state.offset}
) } } ``` ### docs/demos/api/port-label-layout/radial/index.less ```less .port-label-radial-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-label-layout/radial/settings.tsx ```tsx import React from 'react' import { Radio, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { position: string offset: number } export class Settings extends React.Component { state: State = { position: 'radial', offset: 20, } notifyChange() { this.props.onChange(this.state) } onOffsetChanged = (offset: number) => { this.setState({ offset }, () => { this.notifyChange() }) } onPositionChange = (e: any) => { this.setState( { position: e.target.value, }, () => { this.notifyChange() }, ) } render() { const radioStyle = { display: 'block', height: '36px', lineHeight: '36px', } return ( radial radialOriented offset
{this.state.offset}
) } } ``` #### 属性 - Canonical page: https://x6.antv.antgroup.com/zh/api/registry/attr/ - Markdown source: https://x6.antv.antgroup.com/zh/api/registry/attr.md - Description: 属性 --- title: "属性" description: "属性" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/registry/attr/" version: "3.x" --- X6 提供了非常多的内置属性以及自定义属性方式。 ## 内置属性 ### ref 指向一个元素的选择器,指向的元素作为参考元素。 ```typescript graph.addNode({ ..., markup: [ { tagName: 'rect', selector: 'body,' }, { tagName: 'rect', selector: 'custom,' } ], attrs: { body: { width: 100, height: 50, }, // custom 元素的长度和宽度是 body 的一半 custom: { ref: 'body', refWidth: 0.5, refHeight: 0.5, } } }) ``` ### refX 设置元素 `x` 坐标,目标 `x` 坐标相对于 [`ref`](#ref) 指代的元素的左上角 `x` 坐标。 - 当其值在 `[0, 1]` 之间或为百分比(如 `50%`)时,表示目标 `x` 坐标是参照 `x` 坐标相对于参照元素宽度百分比的相对偏移量。例如 `refX: 0.5` 表示,目标 `x` 坐标在参照 `x` 坐标基础上,向右偏移参照宽度的 `50%`。 - 当其值 `<0` 或 `>1` 时,表示目标 `x` 坐标是参照 `x` 坐标的绝对偏移量。例如 `refX: 20` 表示,目标 `x` 坐标在参照 `x` 坐标基础上,向右偏移 `20px`。 ### refX2 与 [`refX`](#refx) 一样,当需要同时指定相对偏移量和绝对偏移量时使用。 ```ts { refX: '50%', refX2: 20, } ``` 上面代码表示,目标 `x` 坐标在参照 `x` 坐标的基础上,向右偏移参照元素宽度的 `50%` 并加上 `20px`。 ### refY 设置元素 `y` 坐标,目标 `y` 坐标相对于 [`ref`](#ref) 指代的参照元素的左上角 `y` 坐标(参照 `y` 坐标) - 当其值在 `[0, 1]` 之间或为百分比(如 `50%`)时,表示目标 `y` 坐标是参照 `y` 坐标相对于参照元素高度百分比的相对偏移量。例如 `refY: 0.5` 表示,目标 `y` 坐标在参照 `y` 坐标基础上,向下偏移参照高度的 `50%`。 - 当其值 `<0` 或 `>1` 时,表示目标 `y` 坐标是参照 `y` 坐标的绝对偏移量。例如 `refY: 20` 表示,目标 `y` 坐标在参照 `y` 坐标基础上,向下偏移 `20px`。 ### refY2 与 [`refY`](#refy) 一样,当需要同时指定相对偏移量和绝对偏移量时使用。 ```ts { refY: '50%', refY2: 20, } ``` 上面代码表示,目标 `y` 坐标在参照 `y` 坐标的基础上,向下偏移参照元素高度的 `50%` 并加上 `20px`。 ### refDx 设置元素 `x` 坐标,目标 `x` 坐标相对于 [`ref`](#ref) 指代的参照元素的右下角 `x` 坐标(参照 `x` 坐标) - 当其值在 `[0, 1]` 之间或为百分比(如 `50%`)时,表示目标 `x` 坐标是参照 `x` 坐标相对于参照元素宽度百分比的相对偏移量。例如 `refDx: 0.5` 表示,目标 `x` 坐标在参照 `x` 坐标基础上,向右偏移参照宽度的 `50%`。 - 当其值 `<0` 或 `>1` 时,表示目标 `x` 坐标是参照 `x` 坐标的绝对偏移量。例如 `refDx: 20` 表示,目标 `x` 坐标在参照 `x` 坐标基础上,向右偏移 `20px`。 ### refDy 设置元素 `y` 坐标,目标 `y` 坐标相对于 [`ref`](#ref) 指代的参照元素的右下角 `y` 坐标(参照 `y` 坐标) - 当其值在 `[0, 1]` 之间或为百分比(如 `50%`)时,表示目标 `y` 坐标是参照 `y` 坐标相对于参照元素高度百分比的相对偏移量。例如 `refDy: 0.5` 表示,目标 `y` 坐标在参照 `y` 坐标基础上,向下偏移参照高度的 `50%`。 - 当其值 `<0` 或 `>1` 时,表示目标 `y` 坐标是参照 `y` 坐标的绝对偏移量。例如 `refDy: 20` 表示,目标 `y` 坐标在参照 `y` 坐标基础上,向下偏移 `20px`。 ### refWidth 设置元素宽度,宽度计算相对于 [`ref`](#ref) 指代的参照元素的宽度(参照宽度) - 当其值在 `[0, 1]` 之间或为百分比(如 `75%`)时,表示元素的宽度是参照宽度百分之多少。例如 `refWidth: 0.75` 表示元素的宽度是参照宽度的 `75%`。 - 当其值 `<0` 或 `>1` 时,表示元素的宽度在参照宽度的基础上减少或增加多少。例如 `refWidth: 20` 表示元素比相对元素宽 `20px`。 > **注意** 该属性只适用于那些支持宽度 `width` 和高度 `height` 的元素,如 `` 元素。 ### refWidth2 与 [`refWidth`](#refwidth) 一样,当需要同时指定绝对宽度和相对宽度时使用。 ```ts { refWidth: '75%', refWidth2: 20, } ``` 上面代码表示目标宽度是参照宽度 `75%` 并加上 `20px`。 ### refHeight 设置元素高度,高度计算相对于 [`ref`](#ref) 指代的参照元素的高度(参照高度) - 当其值在 `[0, 1]` 之间或为百分比(如 `75%`)时,表示元素的高度是参照高度百分之多少。例如 `refHeight: 0.75` 表示元素的高度是参照高度的 `75%`。 - 当其值 `<0` 或 `>1` 时,表示元素的高度在参照高度的基础上减少或增加多少。例如 `refHeight: 20` 表示元素比相对元素高 `20px`。 > **注意** 该属性只适用于那些支持宽度 `width` 和高度 `height` 的元素,如 `` 元素。 ### refHeight2 与 [`refHeight`](#refheight) 一样,当需要同时指定绝对宽度和相对宽度时使用。 ```ts { refHeight: '75%', refHeight2: 20, } ``` 上面代码表示目标高度是参照高度 `75%` 并加上 `20px`。 ### refCx 设置元素中心 `x` 坐标,即[原生 `cx` 属性](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/cx),目标值计算相对于 [`ref`](#ref) 指代的参照元素的宽度(参照宽度) - 当其值在 `[0, 1]` 之间或为百分比(如 `75%`)时,表示元素的 `cx` 是参照宽度百分之多少。例如 `refCx: 0.75` 表示元素中心 `x` 坐标位于参照宽度的 `75%` 处。 - 当其值 `<0` 或 `>1` 时,表示元素的 `cx` 是在参照宽度的基础上减少或增加多少。例如 `refCx: 20` 表示元素中心 `x` 坐标位于参照宽度加 `20px` 处。 > **注意** 该属性只适用于那些支持 `cx` 和 `cy` 属性的元素,如 `` 元素。 ### refCy 设置元素中心 `y` 坐标,即[原生 `cy` 属性](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/cy),目标值计算相对于 [`ref`](#ref) 指代的参照元素的高度(参照高度) - 当其值在 `[0, 1]` 之间或为百分比(如 `75%`)时,表示元素的 `cy` 是参照高度百分之多少。例如 `refCy: 0.75` 表示元素中心 `y` 坐标位于参照高度的 `75%` 处。 - 当其值 `<0` 或 `>1` 时,表示元素的 `cy` 是在参照宽度的基础上减少或增加多少。例如 `refCy: 20` 表示元素中心 `y` 坐标位于参照高度加 `20px` 处。 > **注意** 该属性只适用于那些支持 `cx` 和 `cy` 属性的元素,如 `` 元素。 ### refRx 设置元素的 [`rx` 属性](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/rx),目标值计算相对于 [`ref`](#ref) 指代的参照元素的宽度(参照宽度) - 当其值在 `[0, 1]` 之间或为百分比(如 `75%`)时,表示元素的 `rx` 是参照宽度百分之多少。例如 `refRx: 0.75` 表示元素的 `rx` 是参照宽度的 `75%`。 - 当其值 `<0` 或 `>1` 时,表示元素的 `rx` 是在参照宽度的基础上减少或增加多少。例如 `refRx: 20` 表示元素的 `rx` 是参照宽度加 `20px`。 > **注意** 该属性只适用于那些支持 `rx` 和 `ry` 属性的元素,如 `` 元素。 ### refRy 设置元素的 [`ry` 属性](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/ry),目标值计算相对于 [`ref`](#ref) 指代的参照元素的高度(参照高度) - 当其值在 `[0, 1]` 之间或为百分比(如 `75%`)时,表示元素的 `ry` 是参照高度百分之多少。例如 `refRy: 0.75` 表示元素的 `ry` 是参照高度的 `75%`。 - 当其值 `<0` 或 `>1` 时,表示元素的 `ry` 是在参照宽度的基础上减少或增加多少。例如 `refRy: 20` 表示元素的 `ry` 是参照高度加 `20px`。 > **注意** 该属性只适用于那些支持 `rx` 和 `ry` 属性的元素,如 `` 元素。 ### refRCircumscribed 设置元素的 [`r` 属性](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/r),目标值相对于 [`ref`](#ref) 指代的参照元素的**对角线长度**(参照长度) - 当其值在 `[0, 1]` 之间或为百分比(如 `75%`)时,表示 `r` 是参照长度百分之多少。例如 `refRCircumscribed: 0.75` 表示 `r` 是参照长度的 `75%`。 - 当其值 `<0` 或 `>1` 时,表示 `r` 是在参照长度的基础上减少或增加多少。例如 `refRCircumscribed: 20` 表示 `r` 是参照长度加 `20px`。 > **注意** 该属性只适用于那些支持 `r` 属性的元素,如 `` 元素。 ### refRInscribed _简称_:**`refR`** 设置元素的 [`r` 属性](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/r),目标值相对于 [`ref`](#ref) 指代的参照元素的**宽高的最小值**(参照长度) - 当其值在 `[0, 1]` 之间或为百分比(如 `75%`)时,表示 `r` 是参照长度百分之多少。例如 `refRInscribed: 0.75` 表示 `r` 是参照长度的 `75%`。 - 当其值 `<0` 或 `>1` 时,表示 `r` 是在参照长度的基础上减少或增加多少。例如 `refRInscribed: 20` 表示 `r` 是参照长度加 `20px`。 > **注意** 该属性只适用于那些支持 `r` 属性的元素,如 `` 元素。 ### refDKeepOffset 设置 `` 元素的 [`d` 属性](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d)。通过缩放原始的 pathData 使目标 `` 元素的大小与 [`ref`](#ref) 指代的参照元素的大小一样,通过平移原始的 pathData 使目标 `` 元素的起点坐标与 [`ref`](#ref) 指代的参照元素的起点坐标对齐。 同时,提供的 pathData 的偏移量将被保留,这意味着如果提供的 pathData 的左上角不在坐标原点 `0, 0`,当 `` 元素被渲染到画布后这个偏移量将被保留。 ```ts import { Graph, Node } from '@antv/x6' const Path = Node.define({ markup: [{ tagName: 'path' }], attrs: { path: { refDKeepOffset: 'M 10 10 30 10 30 30 z', // path offset is 10,10 fill: 'red', stroke: 'black', }, }, }) const container = document.getElementById('container')! const graph = new Graph({ container, width: 800, height: 80, grid: true, }) const path = new Path().resize(40, 40).addTo(graph) const view = graph.findView(path) console.log(view.findOne('path').getAttribute('d')) // 'M 10 10 50 10 50 50 z' ``` ### refDResetOffset _简称_:**`refD`** 设置 `` 元素的 [`d` 属性](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d)。通过缩放原始的 pathData 使目标 `` 元素的大小与 [`ref`](#ref) 指代的参照元素的大小一样,通过平移原始的 pathData 使目标 `` 元素的起点坐标与 [`ref`](#ref) 指代的参照元素的起点坐标对齐。 同时,提供的 pathData 的偏移量将被移除,这意味着如果提供的 pathData 的左上角不在坐标原点 `0, 0`,将同时通过平移使其位于原点,当 `` 元素被渲染到画布后将于参照元素严格对齐。 ```ts import { Graph, Node } from '@antv/x6' const Path = Node.define({ markup: [{ tagName: 'path' }], attrs: { path: { refDResetOffset: 'M 10 10 30 10 30 30 z', // path offset is 10,10 fill: 'red', stroke: 'black', }, }, }) const container = document.getElementById('container')! const graph = new Graph({ container, width: 800, height: 80, grid: true, }) const path = new Path().resize(40, 40).addTo(graph) const view = graph.findView(path) console.log(view.findOne('path').getAttribute('d')) // 'M 0 0 40 0 40 40 z' ``` ### resetOffset 当 `resetOffset` 属性值为 `true` 时,将平移点阵,使点阵的左上角位于原点。 ```ts path.attr({ path: { d: 'M 10 10 20 20', resetOffset: true, // 平移后的 d 属性值为 "M 0 0 10 10" }, }) ``` ### refPointsKeepOffset 设置 `` 或 `` 元素的 [points 属性](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/points),通过缩放原始点阵使目标元素的大小与 [`ref`](#ref) 指代的参照元素的大小一样,通过平移原始点阵使目标元素的起点坐标与 [`ref`](#ref) 指代的参照元素的起点坐标对齐。 同时,点阵的偏移量将被保留,这意味着如果点阵的左上角不在坐标原点 `0, 0`,当元素被渲染到画布后这个偏移量将被保留。 ```ts import { Graph, Node } from '@antv/x6' const Polygon = Node.define({ markup: [{ tagName: 'polygon' }], attrs: { polygon: { refPointsKeepOffset: '10,10 30,10 30,30', // points offset is 10,10 fill: 'red', stroke: 'black', }, }, }) const container = document.getElementById('container')! const graph = new Graph({ container, width: 800, height: 80, grid: true, }) const polygon = new Polygon().resize(40, 40).addTo(graph) const view = graph.findView(polygon) console.log(view.findOne('polygon').getAttribute('points')) // '10,10 50,10 50,50' ``` ### refPointsResetOffset _简称_:**`refPoints`** 设置 `` 或 `` 元素的 [points 属性](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/points),通过缩放原始点阵使目标元素的大小与 [`ref`](#ref) 指代的参照元素的大小一样,通过平移原始点阵使目标元素的起点坐标与 [`ref`](#ref) 指代的参照元素的起点坐标对齐。 同时,点阵的偏移量将被移除,这意味着如果点阵的左上角不在坐标原点 `0, 0`,将同时通过平移使其位于原点,当 `` 元素被渲染到画布后将于参照元素严格对齐。 ```ts import { Graph, Node } from '@antv/x6' const Polygon = Node.define({ markup: [{ tagName: 'polygon' }], attrs: { polygon: { refPointsResetOffset: '10,10 30,10 30,30', // points offset is 10,10 fill: 'red', stroke: 'black', }, }, }) const container = document.getElementById('container')! const graph = new Graph({ container, width: 800, height: 80, grid: true, }) const polygon = new Polygon().resize(40, 40).addTo(graph) const view = graph.findView(polygon) console.log(view.findOne('polygon').getAttribute('points')) // '100,0 40,0 40,40' ``` ### xAlign 元素与其 `x` 坐标在水平方向的对齐方式。 - `'left'` 目标元素的左侧与 `x` 对齐。 - `'middle'` 目标元素的中心与 `x` 对齐。 - `'right'` 目标元素的右侧与 `x` 对齐。 ### yAlign 元素与 `y` 坐标在垂直方向的对齐方式。 - `'top'` 目标元素的顶部与 `y` 对齐。 - `'middle'` 目标元素的中心与 `y` 对齐。 - `'bottom'` 目标元素与的底部与 `y` 对齐。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Cell } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Cell componentDidMount() { const graph = new Graph({ container: this.container, width: 400, height: 250, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ x: 60, y: 60, width: 280, height: 120, markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'path', selector: 'hLine', }, { tagName: 'path', selector: 'vLine', }, { tagName: 'rect', selector: 'ref', }, { tagName: 'circle', selector: 'dot', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, }, dot: { r: 3, fill: '#8f8f8f', stroke: 'none', refX: 0.5, refY: 0.5, }, ref: { width: 70, height: 30, refX: 0.5, refY: 0.5, // xAlign: 'middle', // yAlign: 'middle', fill: '#8f8f8f', stroke: '#8f8f8f', }, label: { fontSize: 14, fill: '#333333', fontFamily: 'Arial, helvetica, sans-serif', }, hLine: { refY: 0.5, d: 'M -30 0 310 0', stroke: '#8f8f8f', strokeDasharray: '5 5', }, vLine: { refX: 0.5, d: 'M 0 -30 0 150', stroke: '#8f8f8f', strokeDasharray: '5 5', }, }, }) } onAttrsChanged = (attrs: State) => { this.node.attr({ ref: attrs, hLine: { refY: attrs.refY }, vLine: { refX: attrs.refX }, } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return (
) } } const root = createRoot(document.getElementById('container')!) root.render() window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### fill 当提供的 `fill` 属性值为对象时,表示使用渐变色填充,否则使用字符串颜色填充。 ```ts rect.attr('body/fill', { type: 'linearGradient', stops: [ { offset: '0%', color: '#E67E22' }, { offset: '20%', color: '#D35400' }, { offset: '40%', color: '#E74C3C' }, { offset: '60%', color: '#C0392B' }, { offset: '80%', color: '#F39C12' }, ], }) ``` ### filter 当提供的 `filter` 属性值为对象时,表示使用自定义滤镜填充,否则使用原生字符串形式(如 `"url(#myfilter)"`)。 ```ts rect.attr('body/filter', { name: 'dropShadow', args: { dx: 2, dy: 2, blur: 3, }, }) ``` ### stroke 当提供的 `stroke` 属性值为对象时,表示使用渐变色填充,否则使用字符串颜色填充。使用方式与 [fill](#fill) 属性一样。 ### style 使用 jQuery 的 [`css()`](https://api.jquery.com/css) 方法为指定的元素应用行内 CSS 样式。 ### html 使用 jQuery 的 [`html()`](https://api.jquery.com/html) 方法为指定的元素设置 innerHTML。 ### title 为指定的元素添加一个 `` 子元素,`<title>` 元素并不影响渲染结果,而只是添加一个描述性说明。 ```ts rect.attr('body/title', 'Description of the rectangle') ``` ### text 仅适用于 `<text>` 元素,用于设置文本内容。如果提供的文本是单行文本(不包含换行符 `'\n'`),那么文本被直接设置为 `<text>` 元素的内容;否则为每一行文本将创建一个 `<tspan>` 元素,然后将该元素添加到`<text>` 元素中, ### textWrap 仅适用于 `<text>` 元素,用于设置文本内容。与 [`text`](#text) 属性不同的是,该属性将自动为文本添加换行,使提供的文本完全被包围在参照元素的包围盒(bounding box)中。 其属性值为一个简单对象,通过 `text` 来指定文本内容。可以提供可以选的 `width` 和 `height` 选项来调整元素的大小,当为负数时,表示减少相应的宽度或高度(相当于为文本设置了对应的 padding 边距);反之为正数时,增加相应的宽度或高度;当为百分比时,表示高度或宽度是参照元素宽度或高度的多少百分比。 当提供的文本超出显示范围时,文本将被自动截断,如果 `ellipsis` 选项被设置为 `true` ,则在截断文本的末尾添加一个省略号 `...`。 默认情况下,英文单词会被截断显示,如果不想一个完整的单词被截断可以设置 `breakWord: false`,此时为了显示完整单词,文本可能超出宽度范围。 ```ts textWrap: { text: 'lorem ipsum dolor sit amet consectetur adipiscing elit', width: -10, // 宽度减少 10px height: '50%', // 高度为参照元素高度的一半 ellipsis: true, // 文本超出显示范围时,自动添加省略号 breakWord: true, // 是否截断单词 } ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import * as React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addNode({ x: 40, y: 40, width: 160, height: 48, attrs: { body: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, rx: 5, ry: 5, }, label: { textAnchor: 'left', refX: 8, text: 'hello test foo bar lint css jsvascript typescript', textWrap: { width: 144, height: 40, ellipsis: true, }, }, }, }) graph.addNode({ x: 240, y: 40, width: 160, height: 48, attrs: { body: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, rx: 5, ry: 5, }, label: { textAnchor: 'left', refX: 8, text: 'hello test foo bar 中文字符 lint css jsvascript typescript', textWrap: { width: 144, height: 40, ellipsis: true, }, }, }, }) graph.addNode({ x: 440, y: 40, width: 160, height: 48, attrs: { body: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, rx: 5, ry: 5, }, label: { textAnchor: 'left', refX: 8, text: 'hello-test-foo-bar-lint-css-jsvascript-typescript', textWrap: { width: 144, height: 40, ellipsis: true, }, }, }, }) graph.addNode({ x: 40, y: 128, width: 160, height: 48, attrs: { body: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, rx: 5, ry: 5, }, label: { textAnchor: 'left', refX: 8, text: 'Thisissomeveryveryverylong word. Words will break according to usual rules.', textWrap: { width: 144, height: 40, ellipsis: true, }, }, }, }) graph.addNode({ x: 240, y: 128, width: 160, height: 48, attrs: { body: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, rx: 5, ry: 5, }, label: { textAnchor: 'left', refX: 8, text: 'Thisissomeveryveryverylong word. Words will break according to usual rules.', textWrap: { width: 144, height: 40, ellipsis: true, breakWord: false, }, }, }, }) graph.addNode({ x: 440, y: 128, width: 160, height: 48, attrs: { body: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, rx: 5, ry: 5, }, label: { textAnchor: 'left', refX: 8, text: 'hello test foo bar lint css jsvascript typescript', textWrap: { width: 144, height: 20, ellipsis: true, }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="attrs-text-wrap-app"> <div ref={this.refContainer} className="app-content" /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### textPath 仅适用于 `<text>` 元素,用于渲染沿路径的文本。 当提供的属性值为字符串时,表示文本沿字符串(pathData)代表的路径渲染。 当提供的属性值为对象时,可以通过 `d` 选项来指定文本的渲染路径,或者通过 `selector` 来指定一个节点/边中的 SVGPathElement 元素,支持 CSS 选择器和 [Markup]() 中定义的选择器。同时可以通过 `startOffset` 选项来指定文本在路径中的位置,例如 `50%` 表示文本在沿路径 `50%` 处,`20` 表示文本在偏离路径起点 `20px` 处。 ### lineHeight 仅适用于 `<text>` 元素,用于指定文本的[行高](https://www.w3.org/TR/SVG/text.html#LineHeightProperty)。 ### textVerticalAnchor 仅适用于 `<text>` 元素,元素与 `y` 坐标在垂直方向的对齐方式。。 - `'top'` 目标元素的顶部与 `y` 对齐。 - `'middle'` 目标元素的中心与 `y` 对齐。 - `'bottom'` 目标元素与的底部与 `y` 对齐。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import * as React from 'react' import { Graph, Cell } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Cell componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ x: 120, y: 80, width: 200, height: 80, markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'path', selector: 'hLine', }, { tagName: 'path', selector: 'vLine', }, { tagName: 'rect', selector: 'bg', }, { tagName: 'text', selector: 'label', }, { tagName: 'circle', selector: 'dot', }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, }, dot: { r: 2, fill: '#8f8f8f', stroke: 'none', refX: 0.5, refY: 0.5, }, bg: { ref: 'label', fill: '#8f8f8f', stroke: '#8f8f8f', rx: 2, ry: 2, refWidth: 1, refHeight: 1, refX: 0, refY: 0, }, label: { fontSize: 14, refX: 0.5, refY: 0.5, textAnchor: 'start', textVerticalAnchor: 'top', fill: '#fff', stroke: '#fff', fontFamily: 'Arial, helvetica, sans-serif', text: 'Hello World', }, hLine: { refY: 0.5, d: 'M -40 0 240 0', stroke: '#8f8f8f', strokeDasharray: '5 5', }, vLine: { refX: 0.5, d: 'M 0 -40 0 120', stroke: '#8f8f8f', strokeDasharray: '5 5', }, }, }) } onGridChanged = (attrs: State) => { this.node.attr({ label: attrs, hLine: { refY: attrs.refY }, vLine: { refX: attrs.refX }, } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="attrs-text-anchor-app"> <div className="app-left"> <Settings onChange={this.onGridChanged} /> </div> <div ref={this.refContainer} className="app-content" /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### connection 仅适用于边的 `<path>` 元素,当该属性为 `true` 时,表示将在该元素上渲染边,即设置该元素的 [`d`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) 为边的 pathData。 ```ts edge.attr('pathSelector', { connection: true, stroke: 'red', fill: 'none', }) ``` 也支持包含 `stubs` 和 `reserve` 选项的对象。 - 当 `reverse` 为 `false` 时: - 当 `stubs` 为正数时,表示渲染的起始和终止部分长度。例如 `connection: { stubs: 20 }` 表示连线只渲染起始的 `20px` 和终止的 `20px`,其余部分不渲染。 - 当 `stubs` 为负数时,表示中间缺失(不渲染)部分的长度。例如 `connection: { stubs: -20 }` 表示连线中间有 `20px` 不渲染。 - 当 `reverse` 为 `true` 时: - 当 `stubs` 为正数时,起始和终止部分不渲染的长度。例如 `connection: { stubs: 20 }` 表示起始的 `20px` 和终止的 `20px` 不渲染。 - 当 `stubs` 为负数时,表示中间部分的长度。例如 `connection: { stubs: -20 }` 表示只渲染中间 `20px`。 ```ts edge.attr('pathSelector', { connection: { stubs: -20, reverse: true }, }) ``` ### atConnectionLengthKeepGradient _简称_:**`atConnectionLength`** 将边中的指定元素移动到指定偏移量的位置处,并自动旋转元素,使其方向与所在位置边的斜率保持一致。 - 为正数时,表示距离边起点的偏移量。 - 为负数时,表示距离边终点的偏移量。 ```ts edge.attr('rectSelector', { atConnectionLengthKeepGradient: 30, // atConnectionLength: 30, width: 10, height: 10, fill: 'red', }) ``` ### atConnectionLengthIgnoreGradient 将边中的指定元素移动到指定偏移量的位置处,忽略边的方向,即不会像 [`atConnectionLengthKeepGradient`](#atconnectionlengthkeepgradient) 属性那样自动旋转元素。 - 为正数时,表示距离边起点的偏移量。 - 为负数时,表示距离边终点的偏移量。 ```ts edge.attr('rectSelector', { atConnectionLengthIgnoreGradient: 30, width: 10, height: 10, fill: 'red', }) ``` ### atConnectionRatioKeepGradient _简称_:**`atConnectionRatio`** 将边中的指定元素移动到指定比例 `[0, 1]` 位置处,并自动旋转元素,使其方向与所在位置边的斜率保持一致。 ```ts edge.attr('rectSelector', { atConnectionRatioKeepGradient: 0.5, // atConnectionRatio: 0.5, width: 10, height: 10, fill: 'red', }) ``` ### atConnectionRatioIgnoreGradient 将边中的指定元素移动到指定比例 `[0, 1]` 位置处,忽略边的方向,即不会像 [`atConnectionRatioKeepGradient`](#atconnectionratiokeepgradient) 属性那样自动旋转元素。 ```ts edge.attr('rectSelector', { atConnectionRatioIgnoreGradient: 0.5, width: 10, height: 10, fill: 'red', }) ``` ### sourceMarker 适用于所有 `<path>` 元素,在路径的起点添加一个 SVG 元素(如起始箭头),并自动旋转该元素,使其与根据路径方向保持一致。了解更多详情请参考[这篇教程](/zh/api/model/marker)。 ```ts edge.attr('connection/sourceMarker', { tagName: 'circle', fill: '#666', stroke: '#333', r: 5, cx: 5, }) ``` ### targetMarker 适用于所有 `<path>` 元素,在路径的终点添加一个 SVG 元素(如终点箭头),并自动旋转该元素,使其与根据路径方向保持一致。了解更多详情请参考[这篇教程](/zh/api/model/marker)。 > **注意** 该元素初始时就被旋转了 `180` 度,在此基础上再自动调整旋转角度,并与路径的方向保持一致。例如,对于一个水平的直线,我们为其起点指定了一个向左的箭头,我们也可以为其重点指定相同的箭头,这个箭头会自动指向右侧(自动旋转了 `180` 度)。 ### vertexMarker 适用于所有 `<path>` 元素,使用方法与 [`sourceMarker`](#sourcemarker) 一致,将在 `<path>` 元素所有顶点位置添加额外元素。 ### magnet 当 `magnet` 属性为 `true` 时,表示该元素可以被链接,即在连线过程中可以被当做连线的起点或终点,与连接桩类似。 ### port 为标记为 [`magnet`](#magnet) 的元素指定连接桩 ID,当边链接到该元素时,该 ID 将保存到边的 `source` 或 `target` 中。 - 为字符串时,该字符串作为连接桩 ID。 - 为对象时,对象的 `id` 属性值将作为连接桩 ID。 ### event 在指定的元素上自定义一个点击事件,然后可以在 Graph 上添加该事件的回调。 ```ts node.attr({ // 表示一个删除按钮,点击时删除该节点 image: { event: 'node:delete', xlinkHref: 'trash.png', width: 20, height: 20, }, }) // 绑定事件回调,触发时删除节点 graph.on('node:delete', ({ view, e }) => { e.stopPropagation() view.cell.remove() }) ``` ### xlinkHref 属性 [`xlink:href`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href) 的别名,例如: ```ts node.attr({ image: { xlinkHref: 'xxx.png', }, }) ``` ### xlinkShow 属性 [`xlink:show`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:show) 的别名。 ### xlinkType 属性 [`xlink:type`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:type) 的别名。 ### xlinkTitle 属性 [`xlink:title`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:title) 的别名。 ### xlinkArcrole 属性 [`xlink:arcrole`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:arcrole) 的别名。 ### xmlSpace 属性 [`xml:space`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xml:space) 的别名。 ### xmlBase 属性 [`xml:base`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xml:base) 的别名。 ### xmlLang 属性 [`xml:lang`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xml:lang) 的别名。 ## registry 我们在 `registry` 对象上提供了 [`register`](#register) 和 [`unregister`](#unregister) 两个方法来注册和删除特殊属性。 ### register ```ts register(entities: { [name: string]: Definition }, force?: boolean): void register(name: string, entity: Definition, force?: boolean): Definition ``` 注册自定义属性。 ### unregister ```ts unregister(name: string): Definition | null ``` 删除注册的属性。 同时,我们将 [`register`](#register) 和 [`unregister`](#unregister) 两个方法挂载为 `Graph` 的两个静态方法 `Graph.registerAttr` 和 `Graph.unregisterAttr`,推荐使用这两个静态方法来注册和删除特殊属性。 ### Definition 在内部实现中,我们将特殊属性转换为浏览器可以识别的属性,所以理论上来讲特殊属性的值可以是任何类型,同时特殊属性的定义也支持多种形态。 #### 字符串 为原生属性定义别名,例如特殊属性 `xlinkHref` 的定义,实际上是为 [`xlink:href`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href) 属性定义了一个更易书写的别名。 ```ts // 定义 Graph.registerAttr('xlinkHref', 'xlink:href') // 使用 node.attr({ image: { xlinkHref: 'xxx.png', }, }) ``` 继续介绍属性定义前,我们先了解一下属性限定 `qualify` 函数,只有通过限定函数判定的属性值才会被特殊属性处理。例如,仅当 [`stroke`](#stroke) 属性值为 `Object` 类型时,才会被当做特殊属性处理(使用渐变色填充边框)。看下面限定函数的定义。 ```ts type QualifyFunction = ( this: CellView, // 节点/边的视图 val: ComplexAttrValue, // 当前属性的属性值 options: { elem: Element // 当前属性应用的元素 attrs: ComplexAttrs // 应用到该元素的属性键值对 cell: Cell // 节点/边 view: CellView // 节点/边的视图 }, ) => boolean // 返回 true 时表示通过限定函数的判定 ``` 例如 [`stroke`](#stroke) 属性的定义如下: ```ts export const stroke: Attr.Definition = { qualify(val) { // 仅当属性值为对象时,才触发特殊属性加工逻辑。 return ObjectExt.isPlainObject(val) }, set(stroke, { view }) { return `url(#${view.graph.defineGradient(stroke as any)})` }, } ``` #### set 设置属性定义,适用于大部分场景。 ```ts export interface SetDefinition { qualify?: QualifyFunction // 限定函数 set: ( this: CellView, // 节点/边的视图 val: ComplexAttrValue, // 当前属性的属性值 options: { refBBox: Rectangle // 参照元素的包围盒,没有参照元素时使用节点的位置和大小指代的矩形 elem: Element // 当前属性应用的元素 attrs: ComplexAttrs // 应用到该元素的属性键值对 cell: Cell // 节点/边 view: CellView // 节点/边的视图 }, ) => SimpleAttrValue | SimpleAttrs | void } ``` 当 `set` 方法返回 `string` 或 `number` 时,表示将返回值作为特殊属性的值,这种方式通常用于扩展原生属性的定义,如支持渐变色填充的 [`stroke`](#stroke) 和 [`fill`](#fill) 属性。 ```ts // stroke 属性定义 export const stroke: Attr.SetDefinition = { qualify: ObjectExt.isPlainObject, set(stroke, { view }) { // 返回字符串,返回值作为 stroke 属性的属性值。 return `url(#${view.graph.defineGradient(stroke as any)})` }, } // 使用 stroke 属性 node.attr({ rect: { stroke: {...}, }, }) ``` 当 `set` 方法返回一个简单对象时,则将该对象作为属性键值对应用到对应的元素上,如 [`sourceMarker`](#sourcemarker) 和 [`targetMarker`](#targetmarker) 属性。 ```ts export const sourceMarker: Attr.SetDefinition = { qualify: ObjectExt.isPlainObject, set(marker: string | JSONObject, { view, attrs }) { // 返回一个简单对象,返回值作为属性键值对被应用到对应的元素上。 return { 'marker-start': createMarker(marker, view, attrs), }, }, } ``` 当 `set` 方法没有返回值时,表示在在该方法内部完成了属性赋值,如 [`html`](#html) 属性。 ```ts export const html: Attr.Definition = { set(html, { view, elem }) { // 没有返回值,在方法内部完成属性赋值 view.$(elem).html(`${html}`) }, } ``` #### offset 偏移量属性定义。 ```ts export interface OffsetDefinition { qualify?: QualifyFunction // 限定函数 offset: ( this: CellView, // 节点/边的视图 val: ComplexAttrValue, // 当前属性的属性值 options: { refBBox: Rectangle // 参照元素的包围盒,没有参照元素时使用节点的位置和大小指代的矩形 elem: Element // 当前属性应用的元素 attrs: ComplexAttrs // 应用到该元素的属性键值对 cell: Cell // 节点/边 view: CellView // 节点/边的视图 }, ) => Point.PointLike // 返回绝对偏移量 } ``` 返回代表 `x` 和 `y` 方向绝对偏移量的 `Point.PointLike` 对象,如 [`xAlign`](#xalign) 属性。 ```ts export const xAlign: Attr.OffsetDefinition = { offset(alignment, { refBBox }) { ... // 返回 x 轴的绝对偏移量 return { x, y: 0 } }, } ``` #### position 定位属性定义。 ```ts export interface PositionDefinition { qualify?: QualifyFunction // 限定函数 offset: ( this: CellView, // 节点/边的视图 val: ComplexAttrValue, // 当前属性的属性值 options: { refBBox: Rectangle // 参照元素的包围盒,没有参照元素时使用节点的位置和大小指代的矩形 elem: Element // 当前属性应用的元素 attrs: ComplexAttrs // 应用到该元素的属性键值对 cell: Cell // 节点/边 view: CellView // 节点/边的视图 }, ) => Point.PointLike // 返回绝对定位坐标 } ``` 返回相对于节点的绝对定位坐标,如 [`refX`](#refx) 和 [`refDx`](#refdx) 属性。 ```ts export const refX: Attr.PositionDefinition = { position(val, { refBBox }) { ... // 返回 x 轴的绝对定位 return { x, y: 0 } }, } ``` ### docs/demos/api/attrs/x-align/index.less ```less .attrs-x-align-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/attrs/x-align/settings.tsx ```tsx import React from 'react' import { Radio, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { refX: number refY: number xAlign?: string yAlign?: string } export class Settings extends React.Component<Props, State> { state: State = { refX: 0.5, refY: 0.5, xAlign: 'left', yAlign: 'top', } notifyChange() { this.props.onChange(this.state) } onRefXChanged = (refX: number) => { this.setState({ refX }, () => { this.notifyChange() }) } onRefYChanged = (refY: number) => { this.setState({ refY }, () => { this.notifyChange() }) } onXAlignChange = (e: any) => { this.setState( { xAlign: e.target.value, }, () => { this.notifyChange() }, ) } onYAlignChange = (e: any) => { this.setState( { yAlign: e.target.value, }, () => { this.notifyChange() }, ) } render() { return ( <Card title="Attrs" size="small" bordered={false} style={{ width: 320 }}> <Row align="middle"> <Col span={5}>refX</Col> <Col span={14}> <Slider min={0} max={0.99} step={0.01} value={this.state.refX} onChange={this.onRefXChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value"> {(this.state.refX * 100).toFixed(0)}% </div> </Col> </Row> <Row align="middle"> <Col span={5}>refY</Col> <Col span={14}> <Slider min={0} max={0.99} step={0.01} value={this.state.refY} onChange={this.onRefYChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value"> {(this.state.refY * 100).toFixed(0)}% </div> </Col> </Row> <Row align="middle"> <Col span={5}>xAlign</Col> <Col span={19}> <Radio.Group onChange={this.onXAlignChange} value={this.state.xAlign} > <Radio value={'left'}>Left</Radio> <Radio value={'middle'}>Middle</Radio> <Radio value={'right'}>Right</Radio> </Radio.Group> </Col> </Row> <Row align="middle"> <Col span={5}>yAlign</Col> <Col span={19}> <Radio.Group onChange={this.onYAlignChange} value={this.state.yAlign} > <Radio value={'top'}>Top</Radio> <Radio value={'middle'}>Middle</Radio> <Radio value={'bottom'}>Bottom</Radio> </Radio.Group> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/attrs/text-wrap/index.less ```less .attrs-text-wrap-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 216px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/attrs/text-anchor/index.less ```less .attrs-text-anchor-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/attrs/text-anchor/settings.tsx ```tsx import React from 'react' import { Radio, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { refX: number refY: number textAnchor?: string textVerticalAnchor?: string } export class Settings extends React.Component<Props, State> { state: State = { refX: 0.5, refY: 0.5, textAnchor: 'start', textVerticalAnchor: 'top', } notifyChange() { this.props.onChange(this.state) } onRefXChanged = (refX: number) => { this.setState({ refX }, () => { this.notifyChange() }) } onRefYChanged = (refY: number) => { this.setState({ refY }, () => { this.notifyChange() }) } onTextAnchorAlignChange = (e: any) => { this.setState( { textAnchor: e.target.value, }, () => { this.notifyChange() }, ) } onTextVerticalAnchorAlignChange = (e: any) => { this.setState( { textVerticalAnchor: e.target.value, }, () => { this.notifyChange() }, ) } render() { return ( <Card title="Attrs" size="small" bordered={false} style={{ width: 320 }}> <Row align="middle"> <Col span={5}>refX</Col> <Col span={14}> <Slider min={0} max={0.99} step={0.01} value={this.state.refX} onChange={this.onRefXChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value"> {(this.state.refX * 100).toFixed(0)}% </div> </Col> </Row> <Row align="middle"> <Col span={5}>refY</Col> <Col span={14}> <Slider min={0} max={0.99} step={0.01} value={this.state.refY} onChange={this.onRefYChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value"> {(this.state.refY * 100).toFixed(0)}% </div> </Col> </Row> <Row align="middle"> <Col>textAnchor</Col> </Row> <Row align="middle"> <Col span={19} offset={5}> <Radio.Group onChange={this.onTextAnchorAlignChange} value={this.state.textAnchor} > <Radio value={'start'}>Start</Radio> <Radio value={'middle'}>Middle</Radio> <Radio value={'end'}>end</Radio> </Radio.Group> </Col> </Row> <Row align="middle"> <Col>textVerticalAnchor</Col> </Row> <Row align="middle"> <Col span={19} offset={5}> <Radio.Group onChange={this.onTextVerticalAnchorAlignChange} value={this.state.textVerticalAnchor} > <Radio value={'top'}>Top</Radio> <Radio value={'middle'}>Middle</Radio> <Radio value={'bottom'}>Bottom</Radio> </Radio.Group> </Col> </Row> </Card> ) } } ``` #### 高亮器 - Canonical page: https://x6.antv.antgroup.com/zh/api/registry/highlighter/ - Markdown source: https://x6.antv.antgroup.com/zh/api/registry/highlighter.md - Description: 高亮器 --- title: "高亮器" description: "高亮器" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/registry/highlighter/" version: "3.x" --- 节点/边的高亮器,用于高亮指定的元素。X6 内置了以下几种高亮器。 | 名称 | 说明 | |-----------|----------------------------------------------------------| | stroke | [边框高亮器](#stroke),沿元素的包围盒渲染一个高亮的边框。 | | className | [样式名高亮器](#classname),通过添加额外的样式名来高亮元素。 | 创建 Graph 时,可以通过 `highlighting` 选项来指定触发某种交互时的高亮样式,如: ```ts new Graph({ highlighting: { // 当连接桩可以被链接时,在连接桩外围渲染一个 2px 宽的红色矩形框 magnetAvailable: { name: 'stroke', args: { padding: 4, attrs: { 'stroke-width': 2, stroke: 'red', }, }, }, }, }) ``` 支持的 `highlighting` 配置项有: - `'default'` 默认高亮选项,当以下几种高亮配置缺省时被使用。 - `'embedding'` 拖动节点进行嵌入操作过程中,节点可以被嵌入时被使用。 - `'nodeAvailable'` 连线过程中,节点可以被链接时被使用。 - `'magnetAvailable'` 连线过程中,连接桩可以被链接时被使用。 - `'magnetAdsorbed'` 连线过程中,自动吸附到连接桩时被使用。 ## 内置高亮器 ### stroke 边框高亮器,沿元素的包围盒渲染一个高亮的边框。 | 参数名 | 类型 | 默认值 | 说明 | |---------|--------|--------------------------------------------|-------------| | rx | number | `0` | 边框倒角。 | | ry | number | `0` | 边框倒角。 | | padding | number | `3` | 边距。 | | attrs | object | `{ 'stroke-width': 3, stroke: '#FEB663' }` | 边框元素属性。 | ### className 样式名高亮器,通过添加额外的样式名来高亮元素。 | 参数名 | 类型 | 默认值 | 说明 | |-----------|--------|------------------|-------| | className | string | `x6-highlighted` | 样式名。 | ## 自定义高亮器 高亮器是一个具有如下签名的对象,该对象中包含 `highlight` 和 `unhighlight` 两个方法,分别用于高亮和取消高亮元素。 ```ts export interface Definition<T> { highlight: (cellView: CellView, magnet: Element, options: T) => void unhighlight: (cellView: CellView, magnet: Element, options: T) => void } ``` | 参数名 | 类型 | 默认值 | 说明 | |----------|----------|--------|-------------| | cellView | CellView | | 视图。 | | magnet | Element | | 被高亮的元素。 | | options | T | | 高亮选项。 | 下面我们来定义一个名为 `opacity` 的高亮器,该高亮器为元素添加一个 `'highlight-opacity'` 样式名。 ```ts export interface OpacityHighlighterOptions {} const className = 'highlight-opacity' export const opacity: Highlighter.Definition<OpacityHighlighterOptions> = { highlight(cellView, magnet) { Dom.addClass(magnet, className) }, unhighlight(cellView, magnetEl) { Dom.removeClass(magnetEl, className) }, } ``` 完成定义后就可以注册我们的高亮器: ```ts Graph.registerHighlighter('opacity', opacity, true) ``` 然后我们就可以通过 `opacity` 字符串来使用该高亮器了: ```ts new Graph({ highlighting: { magnetAvailable: { name: 'opacity', }, }, }) ``` #### 滤镜 - Canonical page: https://x6.antv.antgroup.com/zh/api/registry/filter/ - Markdown source: https://x6.antv.antgroup.com/zh/api/registry/filter.md - Description: 滤镜 --- title: "滤镜" description: "滤镜" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/api/registry/filter/" version: "3.x" --- 我们可以使用 [filter](/zh/api/registry/attr#filter) 这个特殊属性来为元素指定 [SVG 滤镜](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Filter_effects),像下面这样,为该元素的 `filter` 属性指定一个预定义的对象,其中 `name` 和 `args` 分别指定了滤镜名称和滤镜参数。 ```ts // 创建节点是通过 attrs 选项指定滤镜 const rect = graph.addNode({ x: 40, y: 40, width: 80, height: 30, attrs: { body: { filter: { name: 'dropShadow', args: { dx: 2, dy: 2, blur: 3, }, }, }, }, }) // 创建节点后可以通过 `attr()` 方法修改或指定元素滤镜 rect.attr('body/filter', { name: 'dropShadow', args: { dx: 2, dy: 2, blur: 3, }, }) ``` 另外,我们也可以调用 `graph.defineFilter(...)` 方法来得到一个滤镜的 ID,然后将 `filter` 属性指定为这个滤镜的 ID。 ```ts const filterId = graph.defineFilter({ name: 'dropShadow', args: { dx: 2, dy: 2, blur: 3, }, }) rect.attr('body/filter', `#${filterId}`) ``` 通过上面的简单介绍,我们了解了如何使用滤镜,下面我们就分别来看看在 X6 中预定义了哪些滤镜。 ## 内置滤镜 ### dropShadow 阴影滤镜。参考 [CSS drop-shadow()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/drop-shadow) 滤镜。 | 参数名 | 类型 | 默认值 | 说明 | |---------|--------|--------|--------------------| | dx | number | `0` | 阴影在 X 轴的偏移量。 | | dy | number | `0` | 阴影在 Y 轴的偏移量。 | | blur | number | `0` | 阴影的模糊半径。 | | opacity | number | `1` | 阴影的透明度。 | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 80, width: 400, height: 160, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `dropShadow\n\ncolor: ${args.color}\ndx: ${args.dx} \ndy: ${args.dy} \nblur: ${args.blur} \nopacity: ${args.opacity} \n` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'dropShadow', args: { ...args }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-drop-shadow-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### blur 高斯模糊滤镜。参考 [CSS blur()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/blur) 滤镜。 | 参数名 | 类型 | 默认值 | 说明 | |--------|--------|--------|-----------------------------------------| | x | number | `2` | X 轴方向的模糊程度。 | | y | number | - | Y 轴方向的模糊程度,缺省时与 X 轴保持一致。 | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 80, y: 80, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `blur(${args.x}${args.y > 0 ? `,${args.y}` : ''})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'blur', args: { x: args.x, y: args.y }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-blur-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### grayScale 灰阶滤镜。参考 [CSS grayscale()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/grayscale) 滤镜。 | 参数名 | 类型 | 默认值 | 说明 | |--------|--------|--------|------------------------------------------------------| | amount | number | `1` | 灰阶程度。取值从 `[0-1]`,`0` 表示没有灰度,`1` 表示全灰。 | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 40, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `grayScale(${args.amount})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'grayScale', args: { amount: args.amount }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-gray-scale-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### sepia 褐色滤镜。参考 [CSS sepia()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/sepia) 滤镜。 | 参数名 | 类型 | 默认值 | 说明 | |--------|--------|--------|--------------------------------------------------------------| | amount | number | `1` | 褐色程度。取值从 `[0-1]`,`0` 表示褐色程度为 `0`,`1` 表示全褐色。 | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 40, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `sepia(${args.amount})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'sepia', args: { amount: args.amount }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-sepia-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### saturate 饱和度滤镜。参考 [CSS saturate()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/saturate) 滤镜。 | 参数名 | 类型 | 默认值 | 说明 | |--------|--------|--------|----------------------| | amount | number | `1` | 饱和度。取值从 `[0-1]`。 | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 40, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `saturate(${args.amount})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'saturate', args: { amount: args.amount }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-saturate-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### hueRotate 色相旋转滤镜。参考 [CSS hue-rotate()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/hue-rotate) 滤镜。 | 参数名 | 类型 | 默认值 | 说明 | |--------|--------|--------|-------------| | angle | number | `0` | 色相旋转角度。 | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 40, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `hueRotate(${args.angle})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'hueRotate', args: { angle: args.angle }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-hue-rotate-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### invert 反色滤镜。参考 [CSS invert()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/invert) 滤镜。 | 参数名 | 类型 | 默认值 | 说明 | |--------|--------|--------|--------------------------------------------------------| | amount | number | `1` | 反色度。取值从 `[0-1]`,`0` 表示没有反色,`1` 表示完全反色。 | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 40, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `invert(${args.amount})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'invert', args: { amount: args.amount }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-invert-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### brightness 明亮度滤镜。参考 [CSS brightness()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/brightness) 滤镜。 | 参数名 | 类型 | 默认值 | 说明 | |--------|--------|--------|------------------------------------------------| | amount | number | `1` | 明亮度。取值从 `[0-1]`,`0` 表示全暗,`1` 表示全亮。 | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 40, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `brightness(${args.amount})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'brightness', args: { amount: args.amount }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-brightness-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### contrast 对比度滤镜。参考 [CSS contrast()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/contrast) 滤镜。 | 参数名 | 类型 | 默认值 | 说明 | |--------|--------|--------|------------------------------------------------| | amount | number | `1` | 对比度。取值从 `[0-1]`,`0` 表示全暗,`1` 表示全亮。 | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 40, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `contrast(${args.amount})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'contrast', args: { amount: args.amount }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-contrast-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### highlight 高亮滤镜。 | 参数名 | 类型 | 默认值 | 说明 | |---------|--------|--------|---------------| | color | string | `red` | 高亮颜色。 | | width | number | `1` | 高亮外框的宽度。 | | blur | number | `0` | 模糊半径。 | | opacity | number | `1` | 透明度。 | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 55, width: 400, height: 160, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `highlight\n\ncolor: ${args.color}\ndx: ${args.width} \ndy: ${args.blur} \nopacity: ${args.opacity} \n` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'highlight', args: { ...args }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-highlight-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### outline 边框滤镜。 | 参数名 | 类型 | 默认值 | 说明 | |---------|--------|--------|---------| | color | string | `blue` | 边框颜色。 | | width | number | `1` | 边框宽度。 | | margin | number | `2` | 边距。 | | opacity | number | `1` | 透明度。 | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 55, width: 400, height: 160, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `outline\n\ncolor: ${args.color}\nwidth: ${args.width} \nmargin: ${args.margin} \nopacity: ${args.opacity} \n` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'outline', args: { ...args }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-outline-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 自定义滤镜 滤镜定义是一个具有如下签名的函数,返回 `<filter>` 标签字符串。 ```ts export type Definition<T> = (args: T) => string ``` 例如,高斯模糊滤镜的定义为: ```ts export interface BlurArgs { x?: number y?: number } export function blur(args: BlurArgs = {}) { const x = getNumber(args.x, 2) const stdDeviation = args.y != null && isFinite(args.y) ? [x, args.y] : x return ` <filter> <feGaussianBlur stdDeviation="${stdDeviation}"/> </filter> `.trim() } ``` 我们可以调用 `Graph.registerFilter(...)` 方法来注册滤镜。 ```ts Graph.registerFilter('blur', blur) ``` ### docs/demos/api/filter/drop-shadow/index.less ```less .filter-drop-shadow-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/drop-shadow/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string dx: number dy: number blur: number opacity: number } export const defaults: State = { color: '#4943a3', dx: 10, dy: 10, blur: 10, opacity: 0.5, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onDxChanged = (dx: number) => { this.setState({ dx }, () => { this.notifyChange() }) } onDyChanged = (dy: number) => { this.setState({ dy }, () => { this.notifyChange() }) } onBlurChanged = (blur: number) => { this.setState({ blur }, () => { this.notifyChange() }) } onOpacityChanged = (opacity: number) => { this.setState({ opacity }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>color</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>dx</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.dx} onChange={this.onDxChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.dx.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>dy</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.dy} onChange={this.onDyChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.dy.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>blur</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.blur} onChange={this.onBlurChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.blur.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>opacity</Col> <Col span={14}> <Slider min={0.05} max={1} step={0.05} value={this.state.opacity} onChange={this.onOpacityChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.opacity.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/blur/index.less ```less .filter-blur-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/blur/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string x: number y: number } export const defaults: State = { color: '#4943a3', x: 10, y: 10, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onXChanged = (dx: number) => { this.setState({ x: dx }, () => { this.notifyChange() }) } onYChanged = (dy: number) => { this.setState({ y: dy }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>fill</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>x</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.x} onChange={this.onXChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.x.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>y</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.y} onChange={this.onYChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.y.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/gray-scale/index.less ```less .filter-gray-scale-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/gray-scale/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string amount: number } export const defaults: State = { color: '#4943a3', amount: 0.3, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onAmountChanged = (dx: number) => { this.setState({ amount: dx }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>fill</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>amount</Col> <Col span={14}> <Slider min={0} max={1} step={0.01} value={this.state.amount} onChange={this.onAmountChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.amount.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/sepia/index.less ```less .filter-sepia-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/sepia/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string amount: number } export const defaults: State = { color: '#4943a3', amount: 0.3, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onAmountChanged = (dx: number) => { this.setState({ amount: dx }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>fill</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>amount</Col> <Col span={14}> <Slider min={0} max={1} step={0.01} value={this.state.amount} onChange={this.onAmountChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.amount.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/saturate/index.less ```less .filter-saturate-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/saturate/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string amount: number } export const defaults: State = { color: '#4943a3', amount: 0.3, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onAmountChanged = (dx: number) => { this.setState({ amount: dx }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>color</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>amount</Col> <Col span={14}> <Slider min={0} max={1} step={0.01} value={this.state.amount} onChange={this.onAmountChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.amount.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/hue-rotate/index.less ```less .filter-hue-rotate-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/hue-rotate/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string angle: number } export const defaults: State = { color: '#4943a3', angle: 0, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onAngleChanged = (dx: number) => { this.setState({ angle: dx }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>fill</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>angle</Col> <Col span={14}> <Slider min={0} max={360} step={1} value={this.state.angle} onChange={this.onAngleChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.angle.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/invert/index.less ```less .filter-invert-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/invert/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string amount: number } export const defaults: State = { color: '#4943a3', amount: 0.3, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onAmountChanged = (dx: number) => { this.setState({ amount: dx }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>fill</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>amount</Col> <Col span={14}> <Slider min={0} max={1} step={0.01} value={this.state.amount} onChange={this.onAmountChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.amount.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/brightness/index.less ```less .filter-brightness-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/brightness/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string amount: number } export const defaults: State = { color: '#4943a3', amount: 0.3, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onAmountChanged = (dx: number) => { this.setState({ amount: dx }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>fill</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>amount</Col> <Col span={14}> <Slider min={0} max={1} step={0.01} value={this.state.amount} onChange={this.onAmountChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.amount.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/contrast/index.less ```less .filter-contrast-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/contrast/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string amount: number } export const defaults: State = { color: '#4943a3', amount: 0.3, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onAmountChanged = (dx: number) => { this.setState({ amount: dx }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>fill</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>amount</Col> <Col span={14}> <Slider min={0} max={1} step={0.01} value={this.state.amount} onChange={this.onAmountChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.amount.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/highlight/index.less ```less .filter-highlight-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/highlight/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string width: number blur: number opacity: number } export const defaults: State = { color: '#4943a3', width: 10, blur: 1, opacity: 0.5, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onWidthChanged = (width: number) => { this.setState({ width }, () => { this.notifyChange() }) } onBlurChanged = (blur: number) => { this.setState({ blur }, () => { this.notifyChange() }) } onOpacityChanged = (opacity: number) => { this.setState({ opacity }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>color</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>width</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.width} onChange={this.onWidthChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.width.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>blur</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.blur} onChange={this.onBlurChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.blur.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>opacity</Col> <Col span={14}> <Slider min={0.05} max={1} step={0.05} value={this.state.opacity} onChange={this.onOpacityChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.opacity.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/outline/index.less ```less .filter-outline-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/outline/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string width: number margin: number opacity: number } export const defaults: State = { color: '#4943a3', width: 2, margin: 2, opacity: 0.5, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onWidthChanged = (width: number) => { this.setState({ width }, () => { this.notifyChange() }) } onMarginChanged = (margin: number) => { this.setState({ margin }, () => { this.notifyChange() }) } onOpacityChanged = (opacity: number) => { this.setState({ opacity }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>color</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>width</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.width} onChange={this.onWidthChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.width.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>margin</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.margin} onChange={this.onMarginChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.margin.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>opacity</Col> <Col span={14}> <Slider min={0.05} max={1} step={0.05} value={this.state.opacity} onChange={this.onOpacityChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.opacity.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### tutorial #### 简介 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/about/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/about.md - Description: 简介 --- title: "简介" description: "简介" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/about/" version: "3.x" --- X6 是基于 HTML 和 SVG 的图编辑引擎,提供低成本的定制能力和开箱即用的内置扩展,方便我们快速搭建 DAG 图、ER 图、流程图、血缘图等应用。 如果你还没有使用过 X6, 建议通过 [快速上手](/zh/tutorial/getting-started) 抢先体验 X6 的魅力。 <p align="left"> <a href="https://github.com/antvis/X6/actions/workflows/ci.yml"><img alt="build" src="https://img.shields.io/github/actions/workflow/status/antvis/x6/ci.yml?branch=master&logo=github&style=flat-square" /></a> <a href="https://www.npmjs.com/package/@antv/x6"><img alt="NPM Package" src="https://img.shields.io/npm/v/@antv/x6.svg?style=flat-square" /></a> <a href="https://www.npmjs.com/package/@antv/x6"><img alt="NPM Downloads" src="https://img.shields.io/npm/dm/@antv/x6?logo=npm&style=flat-square" /></a> </p> <p align="left"> <a href="https://github.com/antvis/X6/blob/master/LICENSE"><img src="https://img.shields.io/github/license/antvis/x6?style=flat-square" alt="MIT License" /></a> <a href="https://www.typescriptlang.org"><img alt="Language" src="https://img.shields.io/badge/language-TypeScript-blue.svg?style=flat-square" /></a> <a href="https://github.com/antvis/x6/pulls"><img alt="PRs Welcome" src="https://img.shields.io/badge/PRs-Welcome-brightgreen.svg?style=flat-square" /></a> <a href="https://x6.antv.antgroup.com"><img alt="website" src="https://img.shields.io/static/v1?label=&labelColor=505050&message=website&color=0076D6&style=flat-square&logo=google-chrome&logoColor=0076D6" /></a> </p> ## ✨ 特性 - 🌱  极易定制:支持使用 SVG/HTML/React/Vue/Angular 定制节点样式和交互; - 🚀  开箱即用:内置 10+ 图编辑配套扩展,如框选、对齐线、小地图等; - 🧲  数据驱动:基于 MVC 架构,用户更加专注于数据逻辑和业务逻辑; - 💯  事件驱动:可以监听图表内发生的任何事件。 ## 🍉 使用文档 - [快速上手](/zh/tutorial/getting-started) - [基础](/zh/tutorial/basic/graph) - [进阶](/zh/tutorial/intermediate/connection-point) - [插件](/zh/tutorial/plugins/transform) - [API](/zh/api/graph/graph) X6 文档整体分为文档和 API 两部分,建议仔细阅读文档内容,会帮助你更好的上手开发,API 文档内容比较多,可以等遇到问题时,通过搜索定位到相关的文档。 ## ❤️ 如何交流 如果您有任何的问题、建议、反馈或者交流意愿,可以通过如下方式联系我们: - 官方推荐: [GitHub issues](https://github.com/antvis/X6/issues/new/choose) - 语雀专栏:[https://www.yuque.com/antv/blog](https://www.yuque.com/antv/blog) <img src="https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*-s9pTaHn1GcAAAAASFAAAAgAemJ7AQ/original" alt="X6 图编辑交流群" width="375" /> ## 🤝 参与贡献 ### Bugs 如果您在使用的过程中碰到问题,可以先通过 [issues](https://github.com/antvis/x6/issues) 看看有没有类似的 bug 或者建议。在你报告一个 bug 之前,请先确保已经搜索过已有的 issue 和阅读了我们的[常见问题](https://www.yuque.com/antv/x6/tox1ukbz5cw57qfy)。 ### 行为准则 我们有一份[行为准则](https://github.com/antvis/X6/blob/master/CONTRIBUTING.zh-CN.md),希望所有的贡献者都能遵守,请花时间阅读一遍全文以确保你能明白哪些是可以做的,哪些是不可以做的。 ### 加入社区 还可以参考下面的贡献指南,一步一步成为 X6 的贡献者吧: - [如何参与到 X6 开源开发](https://www.yuque.com/antv/x6/gcinvi) - [如何优雅地在 github 上贡献代码](https://segmentfault.com/a/1190000000736629?u_atoken=b71f69b7-7d74-4e6c-a373-76e0a36e2c87&u_asession=01aGvG2P10Vrjamv5BFM7yX0X2_OcJ_XmHlitgQC_BVnNLlRLdwpnHYH8ma1b1UKRaX0KNBwm7Lovlpxjd_P_q4JsKWYrT3W_NKPr8w6oU7K93NVUbout2zcDySUWFprtJUe3R9QHfzEvknA4dzJmVTGBkFo3NEHBv0PZUm6pbxQU&u_asig=05FBplinh079EhmRTHTDgrLXp5aawipV_A-9VAsAs841tY8QeTTaaTvFKcH6odRhI4VX2pBdH5ae6FY2MiL2X_4yTqZp2jK-_nBOl2nesFZDM2RmF5JkBT_JWpU60Z6lY1hzgqVxFxj_uE1HnffLBmwa5Sl9NkdZ4_S8RH_A-AooP9JS7q8ZD7Xtz2Ly-b0kmuyAKRFSVJkkdwVUnyHAIJzZMNY1otqX6vcbPyd-A-Ld3WE-pEMt_G6ZtWjng8eWoZH_8T8uYGNepqxdb-gLe1IO3h9VXwMyh6PgyDIVSG1W-dzbV77H9pFSh5eWBVfcZZYGYDqHeX90h_yD6KfDquy8GWlAwW_v4wTa3IAdocwA0iaDksczFnALAG-4HaicdUmWspDxyAEEo4kbsryBKb9Q&u_aref=SU72jL%2FvYl46xrVouxNG%2FiEj5e0%3D) <a href="https://github.com/antvis/x6/graphs/contributors"> <img src="https://openomy.app/svg?repo=antvis/X6&chart=bubble&latestMonth=12" target="_blank" alt="Contribution Leaderboard" style="display: block; height: 400px" /> </a> #### 画布 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/basic/graph/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/basic/graph.md - Description: 画布 --- title: "画布" description: "画布" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/basic/graph/" version: "3.x" --- ## 简介 X6 中的**画布(Graph)**是一个**容器**,用来渲染和管理节点(Node)、边(Edge)以及其他图形元素等等内容,画布本身也包含很多功能(缩放、网格、平移等等)。 在 X6 中,画布是通过 `Graph` 类来实例化的,同时可以指定[一系列选项](/zh/api/graph/graph)来配置画布: ```ts import { Graph } from '@antv/x6' const graph = new Graph({ // 指定画布挂载的容器 container: document.getElementById('container'), // ...其他配置 }) ``` ## 画布大小 在实例化 `Graph` 对象的时候,X6 支持多种方式来设置画布大小: 1. **固定画布大小**:设置 `width` 和 `height` 的固定数值方式来设置画布大小,例如 `width: 800`、`height: 500`,来将画布大小设置为 `800 x 500`。 ```js const graph = new Graph({ container: document.getElementById('container'), width: 800, height: 500, }) ``` 2. **容器大小**:不设置 `width` 和 `height`,默认就会以画布容器大小初始化画布,例如容器大小为 `800 x 500`,画布大小也会是 `800 x 500`。 ```js const graph = new Graph({ container: document.getElementById('container'), // 不设置 width 和 height,默认以容器大小初始化画布 // width: 800, // height: 500, }) ``` 3. **自适应容器大小**:通过设置 `autoResize: true` 来开启自适应容器大小,以容器大小作为画布大小,当画布容器大小改变时,画布大小也会自动重新计算。 ```js const graph = new Graph({ container: document.getElementById('container'), autoResize: true, }) ``` 使用 `autoResize` 还可以解决以下两个问题: - 画布容器还没有渲染完成(此时尺寸为 0),并且没有通过 `width` 和 `height` 配置画布大小,就实例化画布对象,导致渲染不出画布。 - 设置了画布大小,但是页面的 `resize`(例如用户改变浏览器窗口大小)导致画布容器大小改变,但是画布大小并没有跟着一起改变。 > **提示** 使用 `autoResize` 配置时,需要在画布容器外面再套一层宽高都是 100% 的外层容器,在外层容器上监听尺寸改变,当外层容器大小改变时,画布自动重新计算宽高以及元素位置 ```html <div style="width:100%; height:100%"> <div id="container"></div> </div> ``` ## 背景与网格 一个常见的需求是设置画布的背景颜色以及给画布添加网格,在 X6 画布中可以直接通过 `background` 以及 `grid` 配置来实现效果: ```js import { Graph } from '@antv/x6' const graph = new Graph({ // ...其他配置 background: { color: "#F2F7FA" }, grid: 10 }) ``` 这是一个完整的例子,注意背景颜色以及背景上的网格: ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', // 背景颜色 }, grid: { visible: true, type: 'doubleMesh', args: [ { color: '#eee', // 主网格线颜色 thickness: 1, // 主网格线宽度 }, { color: '#ddd', // 次网格线颜色 thickness: 1, // 次网格线宽度 factor: 4, // 主次网格线间隔 }, ], }, }) graph.addNode({ shape: 'rect', label: "hello X6", width: 100, height: 50, attrs: { body: { strokeWidth: 1, rx: 6, ry: 6, }, }, }) graph.centerContent() // 居中显示 ``` > **提示** 在 X6 中,网格是渲染/移动节点的最小单位,默认是 10px ,也就是说位置为 `{ x: 24, y: 38 }` 的节点渲染到画布后的实际位置为 `{ x: 20, y: 40 }`,最小移动单位可以通过 grid 配置中的 `size` [属性](/zh/api/graph/grid#配置)来设置。 背景不仅支持颜色,还支持背景图片,网格也支持四种不同类型,并且能配置网格线的颜色以及宽度,详细的配置与方法可见: - [背景配置](/zh/api/graph/background) - [网格配置](/zh/api/graph/grid) ## 缩放与平移 画布的拖拽、缩放也是常用操作,Graph 中通过 `panning` 和 `mousewheel` 配置来实现这两个功能,鼠标按下画布后移动时会拖拽画布,滚动鼠标滚轮会缩放画布。 ```ts import { Graph } from '@antv/x6' const graph = new Graph({ // ...其他配置 panning: true, mousewheel: true }) ``` 这是一个完整的例子,按住鼠标可以拖动画布,滚动鼠标滚轮可以缩放画布: ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, panning: true, mousewheel: true, }) graph.addNode({ shape: 'rect', label: "hello X6", width: 100, height: 50, attrs: { body: { strokeWidth: 1, rx: 6, ry: 6, }, }, }) graph.centerContent() // 居中显示 ``` 当然,`panning` 和 `mousewheel` 也支持很多其他的配置,比如修饰键(按下修饰键才能触发相应的行为)、缩放因子(速率)等等,详细的配置与方法可见: - [画布平移](/zh/api/graph/panning) - [画布缩放](/zh/api/graph/mousewheel) ## 画布插件 画布除了支持 网格、背景、缩放等功能之外,还通过插件支持了很多其他实用的功能,例如 滚动画布、对齐线等,可以按需来引入和使用: ```ts import { Graph, Scroller, Snapline } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), height: 300, width:800, }) // Snapline 插件可以使节点在拖动过程中显示与其他节点的对齐线 graph.use( new Snapline({ enabled: true, }), ) // Scroller 插件可以使画布支持滚动 graph.use( new Scroller({ enabled: true, pannable: true, }), ) ``` 这是一个完整的例子,使用 `Scroller` 插件后画布可以进行滚动了,使用 `Snapline` 插件后拖动节点靠近其他节点会出现对齐线: ```ts import { Graph, Scroller, Snapline } from '@antv/x6' const data = { // 表示节点 nodes: [ { id: 'node1', shape: 'rect', x: 40, y: 160, width: 100, height: 40, label: 'hello', attrs: { // body 是选择器名称,选中的是 rect 元素 body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, { id: 'node2', shape: 'rect', x: 160, y: 180, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, ], } const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', // 背景颜色 }, }) graph.use( new Snapline({ enabled: true, }), ) graph.use( new Scroller({ enabled: true, pannable: true, }), ) graph.fromJSON(data) // 渲染元素 graph.centerContent() // 居中显示 ``` 更多使用插件和详细功能可见: - [对齐线](/zh/tutorial/plugins/snapline) - [滚动画布](/zh/tutorial/plugins/scroller) ## 常用 API 除了上述的一些配置,X6 还有丰富的 API 对画布尺寸、位置进行操作,下面列举一些常用的 API: ```ts graph.resize(800, 600) // resize 画布大小 graph.translate(20, 20) // 在 x、y 方向上平移画布 graph.zoom(0.2) // 将画布缩放级别增加 0.2(默认为1) graph.zoom(-0.2) // 将画布缩放级别减少 0.2 graph.zoomTo(1.2) // 将画布缩放级别设置为 1.2 // 将画布中元素缩小或者放大一定级别,让画布正好容纳所有元素,可以通过 maxScale 配置最大缩放级别 graph.zoomToFit({ maxScale: 1 }) graph.centerContent() // 将画布中元素居中展示 ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Button } from 'antd' const data = { nodes: [ { id: 'node1', shape: 'rect', x: 40, y: 40, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, { id: 'node2', shape: 'rect', x: 160, y: 180, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, ], edges: [ { shape: 'edge', source: 'node1', target: 'node2', label: 'x6', attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }, ], } const commands = [ { key: 'zoomIn', label: 'ZoomIn(0.2)', }, { key: 'zoomOut', label: 'ZoomOut(-0.2)', }, { key: 'zoomTo', label: 'ZoomTo(1)', }, { key: 'zoomToFit', label: 'ZoomToFit', }, { key: 'centerContent', label: 'CenterContent', }, ] export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, grid: { visible: true, type: 'doubleMesh', args: [ { color: '#eee', // 主网格线颜色 thickness: 1, // 主网格线宽度 }, { color: '#ddd', // 次网格线颜色 thickness: 1, // 次网格线宽度 factor: 4, // 主次网格线间隔 }, ], }, panning: true, mousewheel: { enabled: true, modifiers: 'Ctrl', maxScale: 4, minScale: 0.2, }, }) graph.fromJSON(data) graph.centerContent() this.graph = graph } transform = (command: string) => { switch (command) { case 'translate': this.graph.translate(20, 20) break case 'zoomIn': this.graph.zoom(0.2) break case 'zoomOut': this.graph.zoom(-0.2) break case 'zoomTo': this.graph.zoomTo(1) break case 'zoomToFit': this.graph.zoomToFit() break case 'centerContent': this.graph.centerContent() break default: break } } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="transform-app"> <div className="app-btns"> <Button.Group> {commands.map((item) => ( <Button onClick={() => this.transform(item.key)} key={item.key}> {item.label} </Button> ))} </Button.Group> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 以上是一些常用的 API,更多 API 可见: - [配置项](/zh/api/graph/graph) - [网格](/zh/api/graph/grid) - [背景](/zh/api/graph/background) - [画布平移](/zh/api/graph/panning) - [画布缩放](/zh/api/graph/mousewheel) - [视口变换](/zh/api/graph/transform) - [坐标系](/zh/api/graph/coordinate) ### docs/demos/tutorial/basic/graph/transform/index.less ```less .transform-app { width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-btns { bottom: 0; padding: 0 8px; } .app-content { width: 100%; height: 240px; margin-top: 16px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` #### 连接点 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/intermediate/connection-point/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/intermediate/connection-point.md - Description: 连接点 --- title: "连接点" description: "连接点" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/intermediate/connection-point/" version: "3.x" --- > **在本章节中主要介绍连接点相关的知识,通过阅读,你可以了解到** - 锚点和连接点的概念 - 如何使用锚点和连接点定制特殊连线 先看一个例子: ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, 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-node', x: 40, y: 200, label: 'hello', }) const target = graph.addNode({ shape: 'custom-node', x: 300, y: 80, label: 'world', }) graph.addEdge({ source: { cell: source, anchor: { name: 'right', args: { dy: -10, }, }, connectionPoint: 'anchor', }, target: { cell: target, anchor: { name: 'left', args: { dy: -10, }, }, connectionPoint: 'anchor', }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.addEdge({ source: { cell: source, anchor: { name: 'right', }, connectionPoint: 'anchor', }, target: { cell: target, anchor: { name: 'left', }, connectionPoint: 'anchor', }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.addEdge({ source: { cell: source, anchor: { name: 'right', args: { dy: 10, }, }, connectionPoint: 'anchor', }, target: { cell: target, anchor: { name: 'left', args: { dy: 10, }, }, connectionPoint: 'anchor', }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="multi-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 当我们移动上面的节点时,会发现边与节点的连接位置保持不变,且多条边之间存在间隔。这与之前示例的现象完全不同。这是怎么实现的?下面通过一张图理解 `锚点` 和 `连接点`。 ## 介绍 默认情况下,锚点为 `center`(元素中心)。连接点用于计算交点,默认值为 `boundary`(与元素边框的交点)。因此,边会以起点元素的锚点到终点元素的锚点连一条参考线;参考线与元素的交点由 `connectionPoint` 指定的计算方法得到,该交点即为边的起点/终点。 <img src="https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*RhX1SYh1K-QAAAAAAAAAAAAAARQnAQ" alt="连接点" width="500" /> ## 使用方式 锚点 `anchor` 和连接点 `connectionPoint` 都有两种使用方式,第一种是在 `connecting` 中配置,全局生效。第二种是在创建边时在 `source`、`target` 中指定。 ```ts // 在 connecting 中配置 const graph = new Graph({ connecting: { sourceAnchor: { name: 'right', // 锚点会在节点右侧中心向上偏移 10px args: { dy: -10, }, }, targetAnchor: { name: 'right', // 锚点会在节点右侧中心向上偏移 10px args: { dy: -10, }, }, connectionPoint: 'anchor', }, }) // 也可以在创建连线时配置,且其优先级更高 graph.addEdge({ source: { cell: source, anchor: { name: 'right', args: { dy: -10, }, }, connectionPoint: 'anchor', }, target: { cell: target, anchor: { name: 'left', args: { dy: -10, }, }, connectionPoint: 'anchor', }, }) ``` 当然,X6 还支持丰富的锚点和连接点类型,如需定制特殊连线,可参考 [NodeAnchor](/zh/api/registry/node-anchor) 和 [ConnectionPoint](/zh/api/registry/connection-point)。 ### docs/demos/tutorial/intermediate/connection-point/multi/index.less ```less .multi-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` #### 图形变换 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/plugins/transform/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/plugins/transform.md - Description: 图形变换 --- title: "图形变换" description: "图形变换" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/plugins/transform/" version: "3.x" --- > **在本章节中主要介绍图形变换插件相关的知识,通过阅读,你可以了解到** - 如何通过交互插件调整节点大小 - 如何通过交互插件调整节点旋转角度 ## 使用 使用 `UI` 组件来调整节点尺寸和角度是常见需求。我们提供插件 `transform` 来实现该功能,代码示例如下: ```ts import { Graph, Transform } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use( new Transform({ resizing: resizingOptions, rotating: rotatingOptions, }), ) ``` ## 演示 首先我们体验使用交互修改节点尺寸(点击节点出现操作组件): ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Transform } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.initGraph({ enabled: true, minWidth: 1, maxWidth: 200, minHeight: 1, maxHeight: 150, orthogonal: false, restrict: false, preserveAspectRatio: false, }) } initGraph = (options: State) => { if (this.graph) { this.graph.dispose() } this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.graph.use( new Transform({ resizing: options, }), ) const source = this.graph.addNode({ shape: 'rect', x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 160, y: 180, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph.centerContent() } onSettingChanged = (options: State) => { this.initGraph(options) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="resizing-app"> <div className="app-side"> <Settings onChange={this.onSettingChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 接下来体验使用交互修改节点旋转角度(点击节点出现操作组件): ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Transform } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.initGraph({ enabled: true, grid: 15, }) } initGraph = (options: State) => { if (this.graph) { this.graph.dispose() } this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.graph.use( new Transform({ rotating: options, }), ) const source = this.graph.addNode({ x: 60, y: 130, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 260, y: 120, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph.centerContent() } onSettingChanged = (options: State) => { this.initGraph(options) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="rotating-app"> <div className="app-side"> <Settings onChange={this.onSettingChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 配置 ### 调整尺寸 | 属性名 | 类型 | 默认值 | 必选 | 描述 | |---------------------|---------|------------|------|--------------------------------------------| | enabled | boolean | `false` | | 是否支持调整节点大小 | | minWidth | number | 0 | | 最小的调整宽度 | | minHeight | number | 0 | | 最小的调整高度 | | maxWidth | number | `Infinity` | | 最大的调整宽度 | | maxHeight | number | `Infinity` | | 最大的调整高度 | | orthogonal | boolean | `true` | | 是否显示中间调整点 | | restrict | boolean | `false` | | 调整大小边界是否可以超出画布边缘 | | autoScroll | boolean | `true` | | 拖动位置超过画布时是否自动滚动画布 | | preserveAspectRatio | boolean | `false` | | 调整大小过程中是否保持节点的宽高比例 | | allowReverse | boolean | `true` | | 到达最小宽度或者高度时是否允许控制点反向拖动 | 上面的配置除了支持默认类型外,还支持使用函数的方式来动态修改: ```ts new Transform({ resizing: { enabled: true, orthogonal(node: Node) { const { enableOrthogonal } = node.getData() return enableOrthogonal }, }, }) ``` ### 调整角度 | 属性名 | 类型 | 默认值 | 必选 | 描述 | |---------|---------|---------|------|--------------------| | enabled | boolean | `false` | | 是否支持调整节点角度 | | grid | number | 15 | | 每次旋转的角度 | 上面的配置除了支持默认类型外,还支持使用函数的方式来动态修改: ```ts new Transform({ rotating: { enabled: true, grid() { return 30 }, }, }) ``` ## API ### graph.createTransformWidget(node: Node) 为节点创建 transform 控件。 ### graph.clearTransformWidgets() 清除所有 transform 控件。 ## 事件 | 事件名称 | 参数类型 | 描述 | |-----------------|-------------------------------------------------------------------------------|----------------------| | `node:resize` | `{ e: Dom.MouseDownEvent; x: number; y: number; node: Node; view: NodeView }` | 开始调整节点大小时触发 | | `node:resizing` | `{ e: Dom.MouseMoveEvent; x: number; y: number; node: Node; view: NodeView }` | 调整节点大小时触发 | | `node:resized` | `{ e: Dom.MouseUpEvent; x: number; y: number; node: Node; view: NodeView }` | 调整节点大小后触发 | | `node:rotate` | `{ e: Dom.MouseDownEvent; x: number; y: number; node: Node; view: NodeView }` | 开始旋转节点时触发 | | `node:rotating` | `{ e: Dom.MouseMoveEvent; x: number; y: number; node: Node; view: NodeView }` | 旋转节点时触发 | | `node:rotated` | `{ e: Dom.MouseUpEvent; x: number; y: number; node: Node; view: NodeView }` | 旋转节点后触发 | ```ts graph.on('node:rotated', ({ node }) => { console.log(node.angle()) }) // 我们也可以在插件实例上监听事件 transform.on('node:rotated', ({ node }) => { console.log(node.angle()) }) ``` ### docs/demos/tutorial/plugins/transform/resizing/index.less ```less .resizing-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .x6-widget-transform { margin: -1px 0 0 -1px; padding: 0; border: 1px solid #239edd; border-radius: 6px; } .x6-widget-transform > div { border: 1px solid #239edd; } .x6-widget-transform > div:hover { background-color: #3dafe4; } .x6-widget-transform-active-handle { background-color: #3dafe4; } .x6-widget-transform-resize { border-radius: 0; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } } ``` ### docs/demos/tutorial/plugins/transform/resizing/settings.tsx ```tsx import React from 'react' import { Checkbox, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { enabled: true minWidth?: number maxWidth?: number minHeight?: number maxHeight?: number orthogonal?: boolean restrict?: boolean preserveAspectRatio?: boolean } export class Settings extends React.Component<Props, State> { state: State = { enabled: true, minWidth: 1, maxWidth: 200, minHeight: 1, maxHeight: 150, orthogonal: false, restrict: false, preserveAspectRatio: false, } notifyChange() { this.props.onChange({ ...this.state, }) } onEnableChanged = (e: any) => { this.setState({ enabled: e.target.checked }, () => { this.notifyChange() }) } onMinWidthChanged = (minWidth: number) => { this.setState({ minWidth }, () => { this.notifyChange() }) } onMaxWidthChanged = (maxWidth: number) => { this.setState({ maxWidth }, () => { this.notifyChange() }) } onMinHeightChanged = (minHeight: number) => { this.setState({ minHeight }, () => { this.notifyChange() }) } onMaxHeightChanged = (maxHeight: number) => { this.setState({ maxHeight }, () => { this.notifyChange() }) } onOrthogonalChanged = (e: any) => { this.setState({ orthogonal: e.target.checked }, () => { this.notifyChange() }) } onRestrictedChanged = (e: any) => { this.setState({ restrict: e.target.checked }, () => { this.notifyChange() }) } onPreserveAspectRatioChanged = (e: any) => { this.setState({ preserveAspectRatio: e.target.checked }, () => { this.notifyChange() }) } render() { return ( <Card title="Resizing Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.enabled} onChange={this.onEnableChanged} > Enabled </Checkbox> </Col> </Row> <Row align="middle"> <Col span={6}>minWidth</Col> <Col span={14}> <Slider min={1} max={100} step={1} value={this.state.minWidth} onChange={this.onMinWidthChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.minWidth}</div> </Col> </Row> <Row align="middle"> <Col span={6}>maxWidth</Col> <Col span={14}> <Slider min={100} max={200} step={1} value={this.state.maxWidth} onChange={this.onMaxWidthChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.maxWidth}</div> </Col> </Row> <Row align="middle"> <Col span={6}>minHeight</Col> <Col span={14}> <Slider min={1} max={75} step={1} value={this.state.minHeight} onChange={this.onMinHeightChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.minHeight}</div> </Col> </Row> <Row align="middle"> <Col span={6}>maxHeight</Col> <Col span={14}> <Slider min={75} max={150} step={1} value={this.state.maxHeight} onChange={this.onMaxHeightChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.maxHeight}</div> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.orthogonal} onChange={this.onOrthogonalChanged} > Orthogonal </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.restrict} onChange={this.onRestrictedChanged} > Restrict </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.preserveAspectRatio} onChange={this.onPreserveAspectRatioChanged} > PreserveAspectRatio </Checkbox> </Col> </Row> </Card> ) } } ``` ### docs/demos/tutorial/plugins/transform/rotating/index.less ```less .rotating-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .x6-widget-transform { margin: -1px 0 0 -1px; padding: 0; border: 1px solid #239edd; border-radius: 6px; } .x6-widget-transform > div { border: 1px solid #239edd; } .x6-widget-transform > div:hover { background-color: #3dafe4; } .x6-widget-transform-active-handle { background-color: #3dafe4; } .ant-card { min-height: 250px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } } ``` ### docs/demos/tutorial/plugins/transform/rotating/settings.tsx ```tsx import React from 'react' import { Checkbox, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { enabled: true grid?: number } export class Settings extends React.Component<Props, State> { state: State = { enabled: true, grid: 15, } notifyChange() { this.props.onChange({ ...this.state, }) } onEnableChanged = (e: any) => { this.setState({ enabled: e.target.checked }, () => { this.notifyChange() }) } onGridChanged = (grid: number) => { this.setState({ grid }, () => { this.notifyChange() }) } render() { return ( <Card title="Rotating Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.enabled} onChange={this.onEnableChanged} > Enabled </Checkbox> </Col> </Row> <Row align="middle"> <Col span={6}>grid</Col> <Col span={14}> <Slider min={1} max={180} step={1} value={this.state.grid} onChange={this.onGridChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.grid}</div> </Col> </Row> </Card> ) } } ``` #### 节点 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/basic/node/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/basic/node.md - Description: 节点 --- title: "节点" description: "节点" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/basic/node/" version: "3.x" --- > **在本章节中主要介绍节点相关的知识,通过阅读,你可以了解到** - X6 支持的节点渲染方式 - 添加节点方法 - X6 内置的节点类型 - 如何自定义节点 - 如何通过 API 修改节点 ## 节点渲染方式 X6 是基于 `SVG` 的渲染引擎,可以使用不同的 SVG 元素渲染节点和边,非常适合节点内容比较简单的场景。面对复杂的节点, `SVG` 中有一个特殊的 `foreignObject` 元素,在该元素中可以内嵌任何 XHTML 元素,可以借助该元素来渲染 HTML、React/Vue/Angular 组件到需要位置,这会给项目开发带来非常大的便利。 在选择渲染方式时我们推荐: - 如果节点内容比较简单,而且需求比较固定,使用 `SVG` 节点 - 其他场景,都推荐使用当前项目所使用的框架来渲染节点 > **注意** React/Vue/HTML 渲染方式也存在一些限制,因为浏览器的兼容性问题,有时会出现一些异常的渲染行为。主要表现形式为节点内容展示不全或者节点内容闪烁。可以通过一些方法规避,比如在节点内部元素的 css 样式中不要使用 `position:absolute`、`position:relative`、`transform`、`opacity`。 下面的介绍都是基于 `SVG` 节点,但是其他渲染形式的使用方式与其非常类似,在进阶教程中我们会再次介绍。 ## 添加节点 节点和边都有共同的基类 [Cell](/zh/api/model/cell),除了从 `Cell` 继承属性外,还支持以下选项。 | 属性名 | 类型 | 默认值 | 描述 | |--------|--------|--------|--------------------------| | x | number | 0 | 节点位置 x 坐标,单位为 px。 | | y | number | 0 | 节点位置 y 坐标,单位为 px。 | | width | number | 1 | 节点宽度,单位为 px。 | | height | number | 1 | 节点高度,单位为 px。 | | angle | number | 0 | 节点旋转角度。 | ```ts graph.addNode({ shape: 'rect', x: 100, y: 40, width: 100, height: 40, }) ``` ## 内置节点 上面使用 `shape` 来指定了节点的图形,`shape` 的默认值为 `rect`。X6 内置节点与 `shape` 名称对应关系如下表。 | 构造函数 | shape 名称 | 描述 | |----------------|------------|----------------------------------------------| | Shape.Rect | rect | 矩形。 | | Shape.Circle | circle | 圆形。 | | Shape.Ellipse | ellipse | 椭圆。 | | Shape.Polygon | polygon | 多边形。 | | Shape.Polyline | polyline | 折线。 | | Shape.Path | path | 路径。 | | Shape.Image | image | 图片。 | | Shape.HTML | html | HTML 节点,使用 `foreignObject` 渲染 HTML 片段。 | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' const commonAttrs = { body: { fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, label: { refX: 0.5, refY: '100%', refY2: 4, textAnchor: 'middle', textVerticalAnchor: 'top', }, } export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addNode({ shape: 'rect', x: 40, y: 40, width: 80, height: 40, label: 'rect', attrs: commonAttrs, }) graph.addNode({ shape: 'circle', x: 180, y: 40, width: 40, height: 40, label: 'circle', attrs: commonAttrs, }) graph.addNode({ shape: 'ellipse', x: 280, y: 40, width: 80, height: 40, label: 'ellipse', attrs: commonAttrs, }) graph.addNode({ shape: 'path', x: 420, y: 40, width: 40, height: 40, // https://www.svgrepo.com/svg/13653/like path: 'M24.85,10.126c2.018-4.783,6.628-8.125,11.99-8.125c7.223,0,12.425,6.179,13.079,13.543c0,0,0.353,1.828-0.424,5.119c-1.058,4.482-3.545,8.464-6.898,11.503L24.85,48L7.402,32.165c-3.353-3.038-5.84-7.021-6.898-11.503c-0.777-3.291-0.424-5.119-0.424-5.119C0.734,8.179,5.936,2,13.159,2C18.522,2,22.832,5.343,24.85,10.126z', attrs: commonAttrs, label: 'path', }) graph.addNode({ shape: 'polygon', x: 60, y: 150, width: 40, height: 40, points: '100,10 40,198 190,78 10,78 160,198', attrs: commonAttrs, label: 'polygon', }) graph.addNode({ shape: 'polyline', x: 180, y: 150, width: 40, height: 40, label: 'polyline', attrs: { body: { ...commonAttrs.body, refPoints: '0,0 0,10 10,10 10,0', }, label: commonAttrs.label, }, }) graph.addNode({ shape: 'image', x: 290, y: 150, width: 60, height: 40, imageUrl: 'https://gw.alipayobjects.com/os/s/prod/antv/assets/image/logo-with-text-73b8a.svg', label: 'image', attrs: commonAttrs, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="shapes-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 定制节点 我们可以通过 `markup` 和 `attrs` 来定制节点的形状和样式,`markup` 可以类比 `HTML`,`attrs` 类比 `CSS`。强烈建议仔细阅读 [markup](/zh/api/model/cell#markup) 和 [attrs](/zh/api/model/cell#attrs) 文档。 接下来我们会遇到一个问题,定制的内容要被多个节点使用,是不是需要每个节点都重新定义一次呢?答案是否定的,X6 提供了便捷的方式,可以让不同的节点复用配置。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', // 继承于 rect 节点 width: 100, height: 40, markup: [ { tagName: 'rect', // 标签名称 selector: 'body', // 选择器 }, { tagName: 'image', selector: 'img', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, img: { 'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 16, height: 16, x: 12, y: 12, }, }, }, 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-node', // 可以直接使用上面注册过的 shape x: 40, y: 40, label: 'hello', }) const target = graph.addNode({ shape: 'custom-node', x: 160, y: 180, label: 'world', }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="registry-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 修改节点 在渲染完成之后,我们还可以通过 API 修改节点的所有属性。我们会常用到下面两个方法: - node.prop(path, value),详细使用见 [prop](/zh/api/model/cell#节点和边的属性-properties)。 - node.attr(path, value),详细使用见 [attr](/zh/api/model/cell#元素属性-attrs)。 首先来看 `prop`,我们直接打印 X6 默认 rect 节点的 `prop` 的值。 ```ts const node = graph.addNode({ shape: 'rect', width: 100, height: 40, x: 100, y: 100, label: 'node', }) console.log(node.prop()) // 结果 { "angle": 0, "position": { "x": 100, "y": 100 }, "size": { "width": 100, "height": 40 }, "attrs": { "text": { "fontSize": 14, "fill": "#000000", "refX": 0.5, "refY": 0.5, "textAnchor": "middle", "textVerticalAnchor": "middle", "fontFamily": "Arial, helvetica, sans-serif", "text": "node" }, "rect": { "fill": "#ffffff", "stroke": "#333333", "strokeWidth": 2 }, "body": { "refWidth": "100%", "refHeight": "100%" } }, "visible": true, "shape": "rect", "id": "ab47cadc-4104-457c-971f-50fbb077508a", "zIndex": 1 } ``` 从上面结果可以看到,`prop` 是配置处理后的一份新的配置,它的值可以通过方法进行更新,更新之后,节点会立即刷新到最新状态。为了更快捷的修改节点的 `attrs`,X6 提供了 `attr` 方法。 ```ts source.prop('size', { width: 120, height: 50 }) // 修改尺寸 source.attr('rect/fill', '#ccc') // 修改填充色,等价于 source.prop('attrs/rect/fill', '#ccc') ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Color } from '@antv/x6' import { Button } from 'antd' Graph.registerNode( 'custom-node', { inherit: 'rect', // 继承于 rect 节点 width: 100, height: 40, markup: [ { tagName: 'rect', // 标签名称 selector: 'body', // 选择器 }, { tagName: 'image', selector: 'img', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, img: { 'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 16, height: 16, x: 12, y: 12, }, }, }, true, ) const commands = [ { key: 'prop', label: 'prop', }, { key: 'attr', label: 'attr', }, ] export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const source = graph.addNode({ shape: 'custom-node', x: 40, y: 40, label: 'hello', }) const target = graph.addNode({ shape: 'custom-node', x: 160, y: 180, label: 'world', }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.centerContent() this.graph = graph } change = (command: string) => { const nodes = this.graph.getNodes() switch (command) { case 'prop': nodes.forEach((node) => { const width = 100 + Math.floor(Math.random() * 50) const height = 40 + Math.floor(Math.random() * 10) node.prop('size', { width, height }) }) break case 'attr': nodes.forEach((node) => { const color = Color.randomHex() node.attr('body/fill', color) }) break default: break } } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="node-prop-app "> <div className="app-btns"> <Button.Group> {commands.map((item) => ( <Button onClick={() => this.change(item.key)} key={item.key}> {item.label} </Button> ))} </Button.Group> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 在上面的 JSON 数据中,我们可以看到有一些属性 `refWidth`、`refHeight` 并不是 SVG 的原生属性,它们其实是 X6 内置的特殊属性,比如 `refWidth` 就是相对宽度。更多详细的特殊属性参考 [attrs](/zh/api/model/attrs)。 ### docs/demos/tutorial/basic/node/shapes/index.less ```less .shapes-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` ### docs/demos/tutorial/basic/node/registry/index.less ```less .registry-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` ### docs/demos/tutorial/basic/node/prop/index.less ```less .node-prop-app { border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-btns { bottom: 0; margin: 0; padding: 0 8px; } .app-content { width: 100%; height: 240px; margin: 0; margin-top: 16px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` #### 快速上手 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/getting-started/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/getting-started.md - Description: 快速上手 --- title: "快速上手" description: "快速上手" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/getting-started/" version: "3.x" --- ## 安装 可以通过任意你使用的包管理工具来安装 X6。 ```shell [npm] npm install @antv/x6 --save ``` ```shell [yarn] yarn add @antv/x6 ``` ```bash [pnpm] pnpm add @antv/x6 ``` 如果使用 `umd` 包,可以使用下面三个 CDN 中的任何一个,默认使用 X6 的最新版: - [https://unpkg.com/@antv/x6/dist/index.js](https://unpkg.com/@antv/x6/dist/index.js) - [https://cdn.jsdelivr.net/npm/@antv/x6/dist/index.js](https://cdn.jsdelivr.net/npm/@antv/x6/dist/index.js) - [https://cdnjs.cloudflare.com/ajax/libs/antv-x6/2.18.1/index.js](https://cdnjs.cloudflare.com/ajax/libs/antv-x6/2.18.1/index.js) ## 开始使用 在项目中安装或者引入完成 X6 之后就可以开始使用了,我们将以一个简单的例子开始体验 X6,再开始之前也强烈推荐先学习一些 [SVG 基础知识](https://codepen.io/HunorMarton/full/PoGbgqj),不过没有 SVG 相关知识也可以轻松上手~ ### 1. 初始化画布 在创建画布之前,首先需要在页面中创建一个 **画布容器** 用于挂载画布: ```html <div id="container"></div> ``` 然后初始化画布对象,可以通过配置设置画布的样式,比如背景颜色: ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), // 设置画布大小 width: 800, height: 600, // 设置画布背景颜色 background: { color: '#F2F7FA', }, }) ``` ### 2. 渲染节点和边 创建画布后,可在其中添加**节点**和**边**。X6 支持 JSON 格式数据,该对象中 `nodes` 代表节点数据,`edges` 代表边数据,可以使用 `attrs` 属性来定制节点和边的样式(可以类比 CSS)。 这样我们就得到了一个基础的 X6 画布: ```ts import { Graph } from '@antv/x6' const data = { // 表示节点 nodes: [ { id: 'node1', shape: 'rect', x: 40, y: 40, width: 100, height: 40, label: 'hello', attrs: { // body 是选择器名称,选中的是 rect 元素 body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, { id: 'node2', shape: 'rect', x: 160, y: 180, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, ], // 表示边 edges: [ { shape: 'edge', source: 'node1', target: 'node2', label: 'x6', attrs: { // line 是选择器名称,选中的边的 path 元素 line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }, ], } const graph = new Graph({ container: document.getElementById('container'), height: 300, // 设置画布背景颜色 background: { color: '#F2F7FA', }, }) graph.fromJSON(data) // 渲染元素 graph.centerContent() // 居中显示 ``` ### 3. 前端框架集成 X6 是基于 js 开发的包,不依赖任何的前端框架,可以在`html`中使用,也可以任意 js 框架中使用,但是实际开发中大多数业务都是在使用`React`、`Vue` 等前端框架进行开发,X6 也可以很简单地在这些框架中进行使用,下面的例子实现了上面 [步骤2渲染节点和边章节](#2-渲染节点和边) 相同的效果: ```jsx [React] import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const data = { nodes: [ { id: 'node1', shape: 'rect', x: 40, y: 40, width: 100, height: 40, label: 'hello', attrs: { // body 是选择器名称,选中的是 rect 元素 body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, { id: 'node2', shape: 'rect', x: 160, y: 180, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, ], edges: [ { shape: 'edge', source: 'node1', target: 'node2', label: 'x6', attrs: { // line 是选择器名称,选中的边的 path 元素 line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }, ], } const graph = new Graph({ container: this.container, width: 500, height: 300, // 设置画布背景颜色 background: { color: '#F2F7FA', }, }) graph.fromJSON(data) // 渲染元素 graph.centerContent() // 居中显示 } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="app-content" ref={this.refContainer} /> ) } } ``` ```vue [Vue] <template> <div class="app-content"> <div id="container"></div> </div> </template> <script lang="ts"> import { defineComponent } from 'vue' import { Graph } from '@antv/x6' export default defineComponent({ name: 'App', mounted() { const data = { nodes: [ { id: 'node1', shape: 'rect', x: 40, y: 40, width: 100, height: 40, label: 'hello', attrs: { // body 是选择器名称,选中的是 rect 元素 body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, { id: 'node2', shape: 'rect', x: 160, y: 180, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, ], edges: [ { shape: 'edge', source: 'node1', target: 'node2', label: 'x6', attrs: { // line 是选择器名称,选中的边的 path 元素 line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }, ], } const graph = new Graph({ container: document.getElementById('container'), width: 500, height: 300, // 设置画布背景颜色 background: { color: '#F2F7FA', }, }) graph.fromJSON(data) // 渲染元素 graph.centerContent() // 居中显示 } }); </script> ``` ```ts [Angular] // app.component.html <div> <h1>{{ title }}</h1> <div #container></div> </div> // app.component.ts import { Component, ViewChild, ElementRef } from '@angular/core'; import { Graph } from '@antv/x6' @Component({ selector: 'app-root', templateUrl: './app.component.html', }) export class AppComponent { @ViewChild('container') container: ElementRef; ngAfterViewInit() { const data = { nodes: [ { id: 'node1', shape: 'rect', x: 40, y: 40, width: 100, height: 40, label: 'hello', attrs: { // body 是选择器名称,选中的是 rect 元素 body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, { id: 'node2', shape: 'rect', x: 160, y: 180, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, ], edges: [ { shape: 'edge', source: 'node1', target: 'node2', label: 'x6', attrs: { // line 是选择器名称,选中的边的 path 元素 line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }, ], } const graph = new Graph({ container: this.container.nativeElement, height: 300, // 设置画布背景颜色 background: { color: '#F2F7FA', }, }) graph.fromJSON(data) // 渲染元素 graph.centerContent() // 居中显示 } } ``` 在上面的例子中,都使用了内置的`rect`节点,除此之外 X6 还支持使用框架组件来自定义节点,例如使用 `React` 组件、`Vue` 组件来渲染节点,这些进阶内容我们会在后续的章节中详细介绍,你也可以提前了解: - [React 节点](/zh/tutorial/intermediate/react) - [Vue 节点](/zh/tutorial/intermediate/vue) - [Angular 节点](/zh/tutorial/intermediate/angular) - [HTML 节点](/zh/tutorial/intermediate/html) ### 4. 使用插件 除了基本的元素渲染能力,X6 还内置了大量的图编辑配套插件,使用这些成熟的插件,能很大程度上降低开发成本。例如下面为画布增加对齐线功能,**当移动的节点与其他节点对齐时,会自动出现对齐线**,可以方便用户进行位置排版。 ```ts import { Snapline } from '@antv/x6' graph.use( new Snapline({ enabled: true, }), ) ``` ```ts import { Graph, Snapline } from '@antv/x6' const data = { nodes: [ { id: 'node1', shape: 'rect', x: 40, y: 40, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, { id: 'node2', shape: 'rect', x: 160, y: 180, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, ], edges: [ { shape: 'edge', source: 'node1', target: 'node2', label: 'x6', attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }, ], } const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) graph.use( new Snapline({ enabled: true, }), ) graph.fromJSON(data) // 渲染元素 graph.centerContent() // 居中显示 ``` 更多插件会在后续章节中详细介绍。 ### 5. 数据导出 在上面 [步骤2渲染节点和边章节](#2-渲染节点和边) 中可以看到,可以使用 `fromJSON` 将 `JSON` 数据渲染到画布中,当然,也支持将画布中的数据导出成 `JSON`,这样我们就可以将画布数据序列化后存储到服务端。 ```ts graph.toJSON() ``` 我们的演示 demo 就到这里了,想继续了解 X6 的一些能力,可以从[基础教程](/zh/tutorial/basic/graph)开始阅读。 #### 对齐线 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/plugins/snapline/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/plugins/snapline.md - Description: 对齐线 --- title: "对齐线" description: "对齐线" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/plugins/snapline/" version: "3.x" --- > **在本章节中主要介绍对齐线插件相关的知识,通过阅读,你可以了解到** - 如何在画布中使用对齐线 ## 使用 对齐线是用于移动节点排版的辅助工具。你可以通过插件 `Snapline` 启用该功能,示例: ```ts import { Graph, Snapline } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use( new Snapline({ enabled: true, }), ) ``` ## 演示 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Snapline } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.graph.use( new Snapline({ enabled: true, clean: false, }), ) this.graph.addNode({ x: 200, y: 100, width: 100, height: 40, label: 'Drag Me', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const source = this.graph.addNode({ x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 160, y: 180, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } onSnaplineChanged = (options: State) => { this.graph.setSnaplineTolerance(options.tolerance) this.graph.setSnaplineFilter(options.filter) if (options.sharp) { this.graph.enableSharpSnapline() } else { this.graph.disableSharpSnapline() } if (options.resizing) { this.graph.enableSnaplineOnResizing() } else { this.graph.disableSnaplineOnResizing() } } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="snapline-app"> <div className="app-side"> <Settings onChange={this.onSnaplineChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 配置 | 属性名 | 类型 | 默认值 | 必选 | 描述 | |-----------|---------|---------|------|----------------------------------------------------------------------------------------------| | className | string | - | | 附加样式名,用于定制对齐线样式 | | tolerance | number | 10 | | 对齐精度,即移动节点时与目标位置的距离小于 `tolerance` 时触发显示对齐线 | | sharp | boolean | `false` | | 是否使用短款对齐线 | | resizing | boolean | `false` | | 改变节点大小时是否触发对齐线 | | clean | boolean | `true` | | 如果为 `true`,则在 3 秒后清除对齐线;为 `false` 则不会清除;如果为数字(ms),则在指定时间后清除对齐线 | | filter | Filter | - | | 节点过滤器 | 上面的 Filter 类型比较复杂,支持以下三种类型: - `string[]`: 节点 `shape` 数组,指定的节点 `shape` 才会参与对齐计算 - `({ id: string })[]`: 节点 ID 数组,指定的节点才会参与对齐计算 - `(this: Graph, node: Node) => boolean`: 返回 `true` 的节点才参与对齐计算 ## API ### graph.isSnaplineEnabled() ```ts isSnaplineEnabled(): boolean ``` 返回是否启用对齐线。 ### graph.enableSnapline() ```ts enableSnapline(): this ``` 启用对齐线。 ### graph.disableSnapline() ```ts disableSnapline(): this ``` 禁用对齐线。 ### graph.toggleSnapline(...) ```ts toggleSnapline(enabled?: boolean): this ``` 切换对齐线的启用状态。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |---------|---------|:----:|--------|------------------------------------------| | enabled | boolean | | - | 是否启用对齐线,缺省时切换对齐线的启用状态。 | ### graph.hideSnapline() ```ts hideSnapline(): this ``` 隐藏对齐线。 ### graph.isSnaplineOnResizingEnabled() ```ts isSnaplineOnResizingEnabled(): boolean ``` 调整节点大小时,是否触发对齐线。 ### graph.enableSnaplineOnResizing() ```ts enableSnaplineOnResizing(): this ``` 启用调整节点大小过程中触发对齐线。 ### graph.disableSnaplineOnResizing() ```ts disableSnaplineOnResizing(): this ``` 禁用调整节点大小过程中触发对齐线。 ### graph.toggleSnaplineOnResizing(...) ```ts toggleSnaplineOnResizing(enabled?: boolean): this ``` 切换调整节点大小过程中是否触发对齐线。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |---------|---------|:----:|--------|------------------------------------------| | enabled | boolean | | - | 是否启用对齐线,缺省时切换对齐线的启用状态。 | ### graph.isSharpSnapline() ```ts isSharpSnapline(): boolean ``` 是否使用短款对齐线。 ### graph.enableSharpSnapline() ```ts enableSharpSnapline(): this ``` 启用短款对齐线,启用后对齐线只显示到相关节点位置处,否则显示贯穿画布的对齐线。 ### graph.disableSharpSnapline() ```ts disableSharpSnapline(): this ``` 禁用短款对齐线,对齐线将贯穿整个画布。 ### graph.toggleSharpSnapline() ```ts toggleSharpSnapline(enabled?: boolean): this ``` 切换短款对齐线的启用状态。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |---------|---------|:----:|--------|--------------------------------------------------| | enabled | boolean | | - | 是否启用短款对齐线,缺省时切换短款对齐线的启用状态。 | ### graph.getSnaplineTolerance() ```ts getSnaplineTolerance(): number ``` 获取对齐线精度。 ### graph.setSnaplineTolerance(...) ```ts setSnaplineTolerance(tolerance: number): this ``` 设置对齐线精度。 ### graph.setSnaplineFilter(...) ```ts setSnaplineFilter( filter?: | null | (string | { id: string })[] | ((this: Graph, node: Node) => boolean) ): this ``` 设置过滤条件,满足过滤条件的节点才会参与对齐线计算。 ### docs/demos/tutorial/plugins/snapline/index.less ```less .snapline-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } } ``` ### docs/demos/tutorial/plugins/snapline/settings.tsx ```tsx import React from 'react' import { Checkbox, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { tolerance: number sharp: boolean resizing: boolean filter?: string[] } export class Settings extends React.Component<Props, State> { state: State = { tolerance: 10, sharp: false, resizing: false, } tryToJSON(val: string) { try { return JSON.parse(val) } catch (error) { return val } } notifyChange() { this.props.onChange({ ...this.state, }) } onSharpChanged = (e: any) => { this.setState({ sharp: e.target.checked }, () => { this.notifyChange() }) } onResingChanged = (e: any) => { this.setState({ resizing: e.target.checked }, () => { this.notifyChange() }) } onToleranceChanged = (tolerance: number) => { this.setState({ tolerance }, () => { this.notifyChange() }) } onFilterChanged = (e: any) => { this.setState( { filter: e.target.checked ? ['circle'] : undefined, }, () => { this.notifyChange() }, ) } render() { return ( <Card title="Snapline Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>Tolerance</Col> <Col span={2} offset={1}> <div className="slider-value">{this.state.tolerance}</div> </Col> </Row> <Row align="middle"> <Col span={24}> <Slider min={1} max={30} step={1} value={this.state.tolerance} onChange={this.onToleranceChanged} /> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.sharp} onChange={this.onSharpChanged}> Sharp Line </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.resizing} onChange={this.onResingChanged} > Snap on Resizing </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.filter != null} onChange={this.onFilterChanged} > Add Filter(only circle) </Checkbox> </Col> </Row> </Card> ) } } ``` #### 边 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/basic/edge/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/basic/edge.md - Description: 边 --- title: "边" description: "边" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/basic/edge/" version: "3.x" --- > **在本章节中主要介绍边相关的知识,通过阅读,你可以了解到** - 添加边的方法 - 如何配置边的形状 - 如何在边上增加箭头 - 如何自定义边 - 如何通过 API 修改边 ## 添加边 节点和边都有共同的基类 [Cell](/zh/api/model/cell),除了从 `Cell` 继承属性外,还支持以下选项。 | 属性名 | 类型 | 默认值 | 描述 | |--------------|-------------------|------------------------------------------------|-----------------| | source | TerminalData | - | 源节点或起始点。 | | target | TerminalData | - | 目标节点或目标点。 | | vertices | Point.PointLike[] | - | 路径点。 | | router | RouterData | - | 路由。 | | connector | ConnectorData | - | 连接器。 | | labels | Label[] | - | 标签。 | | defaultLabel | Label | [默认标签](/zh/api/model/edge#labels-和-defaultlabel) | 默认标签。 | ```ts graph.addEdge({ shape: 'edge', source: 'node1', target: 'node2', }) ``` ## 配置边 下面分别看下上面的配置如何使用。 ### source/target 边的源和目标节点(点)。 ```ts 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 }, // 目标点 }) ``` ### vertices 路径点。边从起始点开始,按顺序经过路径点,最后到达终止点。 ```ts graph.addEdge({ source: rect1, target: rect2, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', // 继承于 rect 节点 width: 100, height: 40, markup: [ { tagName: 'rect', // 标签名称 selector: 'body', // 选择器 }, { tagName: 'image', selector: 'img', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, img: { 'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 16, height: 16, x: 12, y: 12, }, }, }, 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-node', // 可以直接使用上面注册过的 shape x: 40, y: 40, label: 'hello', }) const target = graph.addNode({ shape: 'custom-node', x: 300, y: 220, label: 'world', }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="vertices-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### router 路由 `router` 将对 `vertices` 进一步处理,并在必要时添加额外的点,然后返回处理后的点。例如,经过 [orth 路由](/zh/api/registry/router#orth)处理后,边的每一条链接线段都是水平或垂直的。 ```ts graph.addEdge({ source: rect1, target: rect2, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], // 如果没有 args 参数,可以简写为 router: 'orth' router: { name: 'orth', args: {}, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', // 继承于 rect 节点 width: 100, height: 40, markup: [ { tagName: 'rect', // 标签名称 selector: 'body', // 选择器 }, { tagName: 'image', selector: 'img', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, img: { 'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 16, height: 16, x: 12, y: 12, }, }, }, 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-node', // 可以直接使用上面注册过的 shape x: 40, y: 40, label: 'hello', }) const target = graph.addNode({ shape: 'custom-node', x: 300, y: 220, label: 'world', }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], router: 'orth', }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="router-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` X6 默认提供了以下几种路由,点击下面的链接查看每种路由的使用方式。 - [normal](/zh/api/registry/router#normal) - [orth](/zh/api/registry/router#orth) - [oneSide](/zh/api/registry/router#oneside) - [manhattan](/zh/api/registry/router#manhattan) - [metro](/zh/api/registry/router#metro) - [er](/zh/api/registry/router#er) 另外,我们也可以注册自定义路由,详情请参考[自定义路由](/zh/api/registry/router#registry)教程。 ### connector 连接器 `connector` 将路由 `router` 返回的点加工成渲染边所需要的 [pathData](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d)。例如,`rounded` 连接器将连线之间的倒角处理为圆弧倒角。 ```ts graph.addEdge({ source: rect1, target: rect2, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], router: 'orth', // 如果没有 args 参数,可以简写为 connector: 'rounded' connector: { name: 'rounded', args: {}, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', // 继承于 rect 节点 width: 100, height: 40, markup: [ { tagName: 'rect', // 标签名称 selector: 'body', // 选择器 }, { tagName: 'image', selector: 'img', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, img: { 'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 16, height: 16, x: 12, y: 12, }, }, }, 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-node', // 可以直接使用上面注册过的 shape x: 40, y: 40, label: 'hello', }) const target = graph.addNode({ shape: 'custom-node', x: 300, y: 220, label: 'world', }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], router: 'orth', connector: 'rounded', }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="connector-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` X6 默认提供了以下几种连接器,点击下面的链接查看每种连接器的使用方式。 - [normal](/zh/api/registry/connector#normal) - [rounded](/zh/api/registry/connector#rounded) - [smooth](/zh/api/registry/connector#smooth) - [jumpover](/zh/api/registry/connector#jumpover) 另外,我们也可以注册自定义连接器,详情请参考[自定义连接器](/zh/api/registry/connector)。 ### labels 用于设置标签文本、位置、样式等。通过数组形式支持多标签,`labels` 指定的每一项都将与 [defaultLabel](/zh/api/model/edge#labels-和-defaultlabel) 进行 [merge](https://lodash.com/docs/4.17.15#merge) 后使用。 ```ts const edge = graph.addEdge({ source: rect1, target: rect2, labels: [ { attrs: { label: { text: 'edge', }, }, }, ], }) // 或 const edge = graph.addEdge({ source: rect1, target: rect2, labels: ['edge'], // 通过 labels 可以设置多个标签,当只设置标签文本是可以简化为此写法 }) // 或 const edge = graph.addEdge({ source: rect1, target: rect2, label: 'edge', // 通过 label 设置单个标签,当只设置标签文本时可以简化为此写法 }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', // 继承于 rect 节点 width: 100, height: 40, markup: [ { tagName: 'rect', // 标签名称 selector: 'body', // 选择器 }, { tagName: 'image', selector: 'img', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, img: { 'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 16, height: 16, x: 12, y: 12, }, }, }, 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-node', x: 40, y: 40, label: 'hello', }) const target = graph.addNode({ shape: 'custom-node', x: 300, y: 220, label: 'world', }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, labels: [ { attrs: { label: { text: '40%', stroke: '#aaa', }, }, position: 0.4, }, { attrs: { label: { text: '60%', stroke: '#aaa', }, }, position: 0.6, }, ], }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="labels-app "> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 除了设置文本,还可以使用 Label 在边上创建一些复杂图形,我们在 [API](/zh/api/model/labels) 中会详细介绍。 ### defaultLabel 默认标签。默认标签可以简化标签配置项,`labels` 指定的每一项都将与 `defaultLabel` 进行 merge 后使用。 ## 使用箭头 我们定义了 `sourceMarker` 和 `targetMarker` 两个特殊属性来为边定制起始和终止箭头。例如,对 `Shape.Edge` 我们可以通过 `line` 选择器来指定起始和终止箭头。 ### 内置箭头 X6 提供了以下几种内置箭头,使用时只需要指定箭头名和参数(可省略)即可。 - [block](/zh/api/model/marker#block) - [classic](/zh/api/model/marker#classic) - [diamond](/zh/api/model/marker#diamond) - [cross](/zh/api/model/marker#cross) - [async](/zh/api/model/marker#async) - [path](/zh/api/model/marker#path) - [circle](/zh/api/model/marker#circle) - [circlePlus](/zh/api/model/marker#circleplus) - [ellipse](/zh/api/model/marker#ellipse) ```ts graph.addEdge({ shape: 'edge', source: [100, 100], target: [500, 500], attrs: { line: { sourceMarker: 'block', // 实心箭头 targetMarker: { name: 'ellipse', // 椭圆 rx: 10, // 椭圆箭头的 x 半径 ry: 6, // 椭圆箭头的 y 半径 }, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' const markers = [ 'block', 'classic', 'diamond', 'circle', 'circlePlus', 'ellipse', 'cross', 'async', ] export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) markers.forEach((marker, i) => { graph.addEdge({ sourcePoint: [120, 20 + i * 40], targetPoint: [400, 20 + i * 40], label: marker, attrs: { line: { sourceMarker: marker, targetMarker: marker, stroke: '#8f8f8f', strokeWidth: 1, }, }, }) }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="native-marker-app "> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` > **提示** X6 中边默认自带 `classic` 箭头,如果要去掉,可以将 `targetMarker` 设置为 `null`。 ### 自定义箭头 我们也可以通过 `tagName` 指定的 SVG 元素来渲染箭头,例如下面我们使用 `<path>` 元素来渲染箭头,箭头默认继承边的填充色 `fill` 和边框色 `stroke`。 ```ts graph.addEdge({ shape: 'edge', source: [100, 100], target: [500, 500], attrs: { line: { sourceMarker: { tagName: 'path', d: 'M 20 -10 0 0 20 10 Z', }, targetMarker: { tagName: 'path', fill: 'yellow', // 使用自定义填充色 stroke: 'green', // 使用自定义边框色 strokeWidth: 2, d: 'M 20 -10 0 0 20 10 Z', }, }, }, }) ``` > **提示** 我们的起始箭头和终止箭头使用了相同的 `d` 属性,这是因为我们会自动计算箭头方向,简单来说,我们在定义箭头时,只需要定义一个**向左指向坐标原点**的箭头即可。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: [100, 140], target: [400, 140], label: 'custom-marker', attrs: { line: { sourceMarker: { tagName: 'path', d: 'M 20 -10 0 0 20 10 Z', }, targetMarker: { tagName: 'path', stroke: '#D94111', strokeWidth: 2, fill: '#90C54C', d: 'M 20 -10 0 0 20 10 Z', }, stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="custom-marker-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 更多箭头的案例和定制技巧请参考 [API](/zh/api/model/marker)。 ## 定制边 和节点一样,我们可以通过 `markup` 和 `attrs` 来定制边的形状和样式,也可以注册自定义边来达到复用效果。X6 默认的边 `Shape.Edge` 中定义了 `line`(代表 path 元素)和 `wrap`(代表透明的 path 元素,用于响应交互)两个选择器。我们在创建边时可以像下面这样定义边的样式。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerEdge('double-edge', { inherit: 'edge', markup: [ { tagName: 'path', selector: 'outline', attrs: { fill: 'none', }, }, { tagName: 'path', selector: 'line', attrs: { fill: 'none', cursor: 'pointer', }, }, ], attrs: { line: { connection: true, stroke: '#dddddd', strokeWidth: 4, strokeLinejoin: 'round', targetMarker: { tagName: 'path', stroke: '#000000', d: 'M 10 -3 10 -10 -2 0 10 10 10 3', }, }, outline: { connection: true, stroke: '#000000', strokeWidth: 6, strokeLinejoin: 'round', }, }, }) Graph.registerEdge('shadow-edge', { inherit: 'edge', markup: [ { tagName: 'path', selector: 'shadow', attrs: { fill: 'none', }, }, { tagName: 'path', selector: 'line', attrs: { fill: 'none', cursor: 'pointer', }, }, ], attrs: { line: { connection: true, stroke: '#dddddd', strokeWidth: 20, strokeLinejoin: 'round', targetMarker: { name: 'path', stroke: 'none', d: 'M 0 -10 -10 0 0 10 z', offsetX: -5, }, sourceMarker: { name: 'path', stroke: 'none', d: 'M -10 -10 0 0 -10 10 0 10 0 -10 z', offsetX: -5, }, }, shadow: { connection: true, refX: 3, refY: 6, stroke: '#000000', strokeOpacity: 0.2, strokeWidth: 20, strokeLinejoin: 'round', targetMarker: { name: 'path', d: 'M 0 -10 -10 0 0 10 z', stroke: 'none', offsetX: -5, }, sourceMarker: { name: 'path', stroke: 'none', d: 'M -10 -10 0 0 -10 10 0 10 0 -10 z', offsetX: -5, }, }, }, }) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ shape: 'double-edge', source: [100, 100], target: [400, 100], }) graph.addEdge({ shape: 'shadow-edge', source: [100, 180], target: [400, 180], }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="registry-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 修改边 和节点类似,在渲染完成之后,我们还可以通过 API 修改边的所有属性。我们会常用到下面两个方法: - edge.prop(path, value),详细使用见 [prop](/zh/api/model/cell#节点和边的属性-properties)。 - edge.attr(path, value),详细使用见 [attr](/zh/api/model/cell#元素属性-attrs)。 下面我们看一下 X6 默认提供的边的 `prop`。 ```ts const edge = graph.addEdge({ source: [200, 140], target: [500, 140], label: 'edge', }) console.log(edge.prop()) // 输出结果 { "shape": "edge", "attrs": { "lines": { "connection": true, "strokeLinejoin": "round" }, "wrap": { "strokeWidth": 10 }, "line": { "stroke": "#333", "strokeWidth": 2, "targetMarker": "classic" } }, "id": "9d5e4f54-1ed3-429e-8d8c-a1526cff2cd8", "source": { "x": 200, "y": 140 }, "target": { "x": 500, "y": 140 }, "labels": [{ "attrs": { "label": { "text": "edge" } } }], "zIndex": 1 } ``` 从上面结果可以看到,`prop` 是处理后的一份新的配置,它的值可以通过方法进行更新,更新之后,边会立即刷新到最新状态。为了更快捷的修改边的 `attrs`,X6 提供了 `attr` 方法。 ```ts edge.prop('target', { x: 300, y: 300 }) // 修改终点 edge.attr('line/stroke', '#ccc') // 修改边颜色,等价于 edge.prop('attrs/line/stroke', '#ccc') ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Color } from '@antv/x6' import { Button } from 'antd' const commands = [ { key: 'prop', label: 'prop', }, { key: 'attr', label: 'attr', }, ] export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const source = graph.addNode({ shape: 'rect', x: 40, y: 100, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode({ shape: 'rect', x: 340, y: 100, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph = graph } change = (command: string) => { const edges = this.graph.getEdges() switch (command) { case 'prop': edges.forEach((edge) => { const x = Math.floor(Math.random() * 600) const y = Math.floor(Math.random() * 200) edge.prop('vertices', [[x, y]]) }) break case 'attr': edges.forEach((edge) => { const color = Color.randomHex() edge.attr('line/stroke', color) }) break default: break } } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="prop-app"> <div className="app-btns"> <Button.Group> {commands.map((item) => ( <Button onClick={() => this.change(item.key)} key={item.key}> {item.label} </Button> ))} </Button.Group> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/tutorial/basic/edge/vertices/index.less ```less .vertices-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/edge/router/index.less ```less .router-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/edge/connector/index.less ```less .connector-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/edge/labels/index.less ```less .labels-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/edge/native-marker/index.less ```less .native-marker-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/edge/custom-marker/index.less ```less .custom-marker-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/edge/registry/index.less ```less .registry-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` ### docs/demos/tutorial/basic/edge/prop/index.less ```less .prop-app { border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-btns { bottom: 0; margin: 0; padding: 0 8px; } .app-content { width: 100%; height: 240px; margin: 0; margin-top: 16px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` #### 工具 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/intermediate/tools/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/intermediate/tools.md - Description: 工具 --- title: "工具" description: "工具" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/intermediate/tools/" version: "3.x" --- > **在本章节中主要介绍工具相关的知识,通过阅读,你可以了解到** - 如何为节点或边添加工具 - X6 默认内置哪些常用工具 ## 使用工具 创建节点/边时可以通过 [`tools`](/zh/api/model/cell#工具集-tools) 选项来添加工具: ```ts graph.addNode({ tools: [ { name: 'button-remove', // 工具名称 args: { // 工具参数 x: 10, y: 10, }, }, ], }) // 如果无需参数,可以简写为: graph.addNode({ tools: ['button-remove'], }) graph.addEdge({ source, target, vertices: [ { x: 90, y: 160, }, { x: 210, y: 160, }, ], tools: ['vertices', 'segments'], }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-tools-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, 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-tools-node', x: 40, y: 40, label: 'Source', tools: ['button-remove'], }) const target = graph.addNode({ shape: 'custom-tools-node', x: 160, y: 240, label: 'Target', tools: ['button-remove'], }) graph.addEdge({ source, target, vertices: [ { x: 90, y: 160 }, { x: 210, y: 160 }, ], tools: ['vertices', 'segments'], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="tools-basic-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 节点/边创建后可以调用 [hasTool(name)](/zh/api/model/cell#hastool)、[addTools(...)](/zh/api/model/cell#addtools)、[removeTools()](/zh/api/model/cell#removetools) 等方法以添加或移除工具。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-tools-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, 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-tools-node', x: 40, y: 40, label: 'Source', }) const target = graph.addNode({ shape: 'custom-tools-node', x: 160, y: 240, label: 'Target', }) graph.addEdge({ source, target, vertices: [ { x: 90, y: 160 }, { x: 210, y: 160 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.on('cell:mouseenter', ({ cell }) => { if (cell.isNode()) { cell.addTools([ { name: 'boundary', args: { attrs: { fill: '#7c68fc', stroke: '#333', 'stroke-width': 1, 'fill-opacity': 0.2, }, }, }, { name: 'button-remove', args: { x: 0, y: 0, offset: { x: 10, y: 10 }, }, }, ]) } else { cell.addTools(['vertices', 'segments']) } }) graph.on('cell:mouseleave', ({ cell }) => { cell.removeTools() }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="tools-onhover-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 内置工具 工具是渲染在节点/边上的小部件,用于增强交互能力。我们分别为节点和边提供以下内置工具: ### 节点 - [button](/zh/api/registry/node-tool#button) 在指定位置处渲染一个按钮,支持自定义按钮的点击交互。 - [button-remove](/zh/api/registry/node-tool#button-remove) 在指定的位置处,渲染一个删除按钮,点击时删除对应的节点。 - [boundary](/zh/api/registry/node-tool#boundary) 根据节点的包围盒渲染一个包围节点的矩形。注意:该工具仅用于可视化,不带任何交互。 ### 边 - [vertices](/zh/api/registry/edge-tool#vertices) 在路径点位置渲染小圆点;拖动小圆点可修改位置,双击可删除;在边上单击可添加路径点。 - [segments](/zh/api/registry/edge-tool#segments) 在线段中心渲染一个工具条;拖动工具条可调整线段两端路径点的位置。 - [boundary](/zh/api/registry/edge-tool#boundary) 根据边的包围盒渲染一个包围边的矩形。注意,该工具仅仅渲染一个矩形,不带任何交互。 - [button](/zh/api/registry/edge-tool#button) 在指定位置处渲染一个按钮,支持自定义按钮的点击交互。 - [button-remove](/zh/api/registry/edge-tool#button-remove) 在指定的位置处,渲染一个删除按钮,点击时删除对应的边。 - [source-arrowhead 和 target-arrowhead](/zh/api/registry/edge-tool#source-arrowhead-和-target-arrowhead) 在边的起点或终点渲染一个图形(默认箭头);可拖动该图形修改边的起点/终点。 ### docs/demos/tutorial/intermediate/tools/basic/index.less ```less .tools-basic-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/intermediate/tools/onhover/index.less ```less .tools-onhover-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### 复制粘贴 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/plugins/clipboard/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/plugins/clipboard.md - Description: 复制粘贴 --- title: "复制粘贴" description: "复制粘贴" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/plugins/clipboard/" version: "3.x" --- > **在本章节中主要介绍剪切板插件相关的知识,通过阅读,你可以了解到** - 如何使用复制粘贴功能 ## 使用 剪切板用于复制/粘贴节点和边。你可以通过插件 `Clipboard` 启用该功能,示例: ```ts import { Graph, Clipboard } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use( new Clipboard({ enabled: true, }), ) ``` ## 演示 - 选中节点后,点击复制按钮复制节点。 - 设置不同的 `offset`,观察粘贴时对节点位置的影响。 - 启用 `localStorage` 后复制节点,刷新页面或重新打开浏览器后,点击粘贴按钮。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Button, message } from 'antd' import { Graph, Clipboard, Selection } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private options: State componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.graph.use( new Selection({ enabled: true, showNodeSelectionBox: true, }), ) this.graph.use( new Clipboard({ enabled: true, useLocalStorage: true, }), ) this.graph.addNode({ x: 280, y: 80, width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const source = this.graph.addNode({ x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 180, y: 160, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } onSettingsChanged = (options: State) => { this.options = { ...options } } refContainer = (container: HTMLDivElement) => { this.container = container } onCopy = () => { const cells = this.graph.getSelectedCells() if (cells && cells.length) { this.graph.copy(cells, this.options) message.success('复制成功') } else { message.info('请先选中节点再复制') } } onPaste = () => { if (this.graph.isClipboardEmpty()) { message.info('剪切板为空,不可粘贴') } else { const cells = this.graph.paste(this.options) this.graph.cleanSelection() this.graph.select(cells) message.success('粘贴成功') } } render() { return ( <div className="clipboard-app"> <div className="app-side"> <Settings onChange={this.onSettingsChanged} /> <div className="app-btns"> <Button onClick={this.onCopy} type="primary"> Copy Selected Cells </Button> <Button onClick={this.onPaste} type="ghost"> Paste </Button> </div> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 配置 | 属性名 | 类型 | 默认值 | 必选 | 描述 | |-----------------|---------|---------|------|------------------------------------------------------------------------------------------------------| | useLocalStorage | boolean | `false` | | 开启后被复制的节点/边同时被保存到 `localStorage` 中,浏览器刷新或者关闭后重新打开,复制/粘贴也能正常工作 | ## API ### graph.copy(...) ```ts copy(cells: Cell[], options: CopyOptions = {}): this ``` 复制节点/边。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |-------------------------|---------|:----:|--------|--------------------------------------------| | cells | Cell[] | ✓ | | 被复制的节点/边。 | | options.deep | boolean | | - | 是否递归复制所有子节点/边。 | | options.useLocalStorage | boolean | | - | 是否将复制的节点/边保存在 `localStorage` 中。 | ### graph.cut(...) ```ts cut(cells: Cell[], options: CopyOptions = {}): this ``` 剪切节点/边。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |-------------------------|---------|:----:|--------|--------------------------------------------| | cells | Cell[] | ✓ | | 被剪切的节点/边。 | | options.deep | boolean | | - | 是否递归复制所有子节点/边。 | | options.useLocalStorage | boolean | | - | 是否将复制的节点/边保存在 `localStorage` 中。 | ### graph.paste(...) ```ts paste(options?: PasteOptions, graph?: Graph): Cell[] ``` 粘贴,返回粘贴到画布的节点/边。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |-------------------------|----------------------------------------|:----:|--------|------------------------------------| | options.useLocalStorage | boolean | | - | 是否使用 `localStorage` 中的节点/边。 | | options.offset | number \| `{ dx: number; dy: number }` | | `20` | 粘贴到画布的节点/边的偏移量。 | | options.nodeProps | Node.Properties | | - | 粘贴到画布的节点的额外属性。 | | options.edgeProps | Edge.Properties | | - | 粘贴到画布的边的额外属性。 | | graph | Graph | | `this` | 粘贴的目标画布,默认粘贴到当前画布。 | ### graph.getCellsInClipboard() ```ts getCellsInClipboard(): Cell[] ``` 获取剪切板中的节点/边。 ### graph.cleanClipboard() ```ts cleanClipboard(): this ``` 清空剪切板。 ### graph.isClipboardEmpty() ```ts isClipboardEmpty(): boolean ``` 返回剪切板是否为空。 ### graph.isClipboardEnabled() ```ts isClipboardEnabled(): boolean ``` 返回是否启用了剪切板。 ### graph.enableClipboard() ```ts enableClipboard(): this ``` 启用剪切板。 ### graph.disableClipboard() ```ts disableClipboard(): this ``` 禁用剪切板。 ### graph.toggleClipboard(...) ```ts toggleClipboard(enabled?: boolean): this ``` 切换剪切板的启用状态。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |---------|---------|:----:|--------|------------------------------------------| | enabled | boolean | | - | 是否启用剪切板,缺省时切换剪切板的启用状态。 | ## 事件 | 事件名称 | 参数类型 | 描述 | |---------------------|---------------------|--------------------------| | `clipboard:changed` | `{ cells: Cell[] }` | 复制、剪切、清空剪切板时触发 | ```ts graph.on('clipboard:changed', ({ cells }) => { console.log(cells) }) // 我们也可以在插件实例上监听事件 clipboard.on('clipboard:changed', ({ cells }) => { console.log(cells) }) ``` ### docs/demos/tutorial/plugins/clipboard/index.less ```less .clipboard-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } .app-btns { display: flex; margin-top: 16px; } } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-btn { flex: 1; margin-right: 8px; } .ant-btn:last-child { margin-right: 0; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .x6-widget-selection-box { border: 2px dashed #239edd; } .x6-widget-selection-inner { border: 1px solid #239edd; } } ``` ### docs/demos/tutorial/plugins/clipboard/settings.tsx ```tsx import React from 'react' import { Checkbox, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { offset: number useLocalStorage: boolean } export class Settings extends React.Component<Props, State> { state: State = { offset: 30, useLocalStorage: true, } componentDidMount() { this.notifyChange() } notifyChange() { this.props.onChange({ ...this.state, }) } onUseLocalStorageChanged = (e: any) => { this.setState({ useLocalStorage: e.target.checked }, () => { this.notifyChange() }) } onOffsetChanged = (offset: number) => { this.setState({ offset }, () => { this.notifyChange() }) } render() { return ( <Card title="Clipboard Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={8}>Paste Offset</Col> <Col span={2} offset={1}> <div className="slider-value">{this.state.offset}</div> </Col> </Row> <Row align="middle"> <Col span={24}> <Slider min={10} max={80} step={1} value={this.state.offset} onChange={this.onOffsetChanged} /> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.useLocalStorage} onChange={this.onUseLocalStorageChanged} > Use LocalStorage </Checkbox> </Col> </Row> </Card> ) } } ``` #### 连接桩 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/basic/port/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/basic/port.md - Description: 连接桩 --- title: "连接桩" description: "连接桩" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/basic/port/" version: "3.x" --- > **在本章节中主要介绍连接桩相关的知识,通过阅读,你可以了解到** - 如何在节点中配置连接桩 - 连接桩的增、删、改 - 如何配置连接桩的位置 - 如何配置连接桩上标签的位置 ## 配置连接桩 首先我们将具有相同行为和外观的连接桩归为同一组,并通过 `groups` 选项来设置分组,该选项是一个对象 `{ [groupName: string]: PortGroupMetadata }`,组名为键,值为每组连接桩的默认选项,支持的选项如下: ```ts 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[]`,数组的每一项表示一个连接桩,连接桩支持的选项如下: ```ts 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 // 标签位置计算方法的参数 } } } ``` 下方示例展示了连接桩的定义方式。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node-with-port', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { top: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, bottom: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, }, }, 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-node-with-port', x: 40, y: 40, label: 'hello', ports: { items: [ { id: 'port_1', group: 'bottom', }, { id: 'port_2', group: 'bottom', }, ], }, }) const target = graph.addNode({ shape: 'custom-node-with-port', x: 160, y: 180, label: 'world', ports: { items: [ { id: 'port_3', group: 'top', }, { id: 'port_4', group: 'top', }, ], }, }) graph.addEdge({ source: { cell: source, port: 'port_2' }, target: { cell: target, port: 'port_3' }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="config-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 修改连接桩 节点上有丰富的 [API](/zh/api/model/node#连接桩-ports) 对连接桩进行增、删、改操作。 ```ts // 添加连接桩 node.addPort({ group: 'top', attrs: { text: { text: 'xx', }, }, }) // 删除连接桩 node.removePort(portId) // 更新连接桩 node.portProp(portId, 'attrs/circle/stroke', color) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Button } from 'antd' Graph.registerNode( 'custom-node-with-port', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { top: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, label: { position: 'top', }, }, bottom: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, label: { position: 'top', }, }, }, }, }, true, ) const commands = [ { key: 'addPort', label: 'addPort', }, { key: 'removePort', label: 'removePort', }, { key: 'updatePort', label: 'updatePort', }, ] export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addNode({ id: '1', shape: 'custom-node-with-port', x: 150, y: 100, label: 'hello', }) graph.centerContent() this.graph = graph } change = (command: string) => { const node = this.graph.getCellById('1') as Node const ports = node.getPorts() let color: string switch (command) { case 'addPort': node.addPort({ group: 'top', attrs: { text: { text: `${ports.length + 1}`, }, }, }) break case 'removePort': if (ports.length) { node.removePortAt(ports.length - 1) } break case 'updatePort': color = Color.randomHex() ports.forEach((port) => { node.portProp(port.id!, 'attrs/circle/stroke', color) }) break default: break } } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="dynamic-app"> <div className="app-btns"> <Button.Group> {commands.map((item) => ( <Button onClick={() => this.change(item.key)} key={item.key}> {item.label} </Button> ))} </Button.Group> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 连接桩位置 连接桩布局算法只能通过 `groups` 中的 `position` 选项来指定,因为布局算法在计算连接桩位置时需要考虑到群组中的所有连接桩,我们在单个连接桩中可以通过 `args` 选项来影响该连接桩的布局结果。 我们默认提供了下面几种连接桩布局算法,同时支持[自定义连接桩布局算法并注册使用](/zh/api/registry/port-layout),点击下面的链接可以了解每种布局算法的使用方法。 - [`absolute`](/zh/api/registry/port-layout#absolute) 绝对定位。 - [`left`](/zh/api/registry/port-layout#left-right-top-bottom) 矩形节点左侧均匀分布。 - [`right`](/zh/api/registry/port-layout#left-right-top-bottom) 矩形节点右侧均匀分布。 - [`top`](/zh/api/registry/port-layout#left-right-top-bottom) 矩形节点顶部均匀分布。 - [`bottom`](/zh/api/registry/port-layout#left-right-top-bottom) 矩形节点底部均匀分布。 - [`line`](/zh/api/registry/port-layout#line) 沿指定的线均匀分布。 - [`ellipse`](/zh/api/registry/port-layout#ellipse) 沿椭圆圆弧分布。 - [`ellipseSpread`](/zh/api/registry/port-layout#ellipsespread) 沿椭圆均匀分布。 ## 连接桩标签位置 在 `groups.label.position` 选项和节点的 `ports.items.label.position` 选项中都可以指定标签的位置。 我们默认提供了下面几种标签位置,也支持[自定义标签位置并注册使用](/zh/api/registry/port-label-layout),点击下面的链接了解每种标签位置的使用方法。 - [`left`](/zh/api/registry/port-label-layout#side) 标签位于连接桩左侧。 - [`right`](/zh/api/registry/port-label-layout#side) 标签位于连接桩右侧。 - [`top`](/zh/api/registry/port-label-layout#side) 标签位于连接桩上方。 - [`bottom`](/zh/api/registry/port-label-layout#side) 标签位于连接桩下方。 - [`inside`](/zh/api/registry/port-label-layout#insideoutside) 标签位于节点内围(靠近边线的内侧)。 - [`outside`](/zh/api/registry/port-label-layout#insideoutside) 标签位于节点外围(靠近边线的外侧)。 - [`insideOriented`](/zh/api/registry/port-label-layout#insideoutside) 标签位于节点内围,而且根据所在方位自动调整文本的方向。 - [`outsideOriented`](/zh/api/registry/port-label-layout#insideoutside) 标签位于节点外围,而且根据所在方位自动调整文本的方向。 - [`radial`](/zh/api/registry/port-label-layout#radial) 标签位于圆形或椭圆形节点的外围。 - [`radialOriented`](/zh/api/registry/port-label-layout#radial) 标签位于圆形或椭圆形节点的外围,并使标签文本自动沿圆弧方向旋转。 ### docs/demos/tutorial/basic/ports/config/index.less ```less .config-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` ### docs/demos/tutorial/basic/ports/dynamic/index.less ```less .dynamic-app { border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-btns { bottom: 0; margin: 0; padding: 0 8px; } .app-content { width: 100%; height: 240px; margin: 0; margin-top: 16px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` #### 群组 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/intermediate/group/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/intermediate/group.md - Description: 群组 --- title: "群组" description: "群组" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/intermediate/group/" version: "3.x" --- > **在本章节中主要介绍组合相关的知识,通过阅读,你可以了解到** - 如何组合节点 - 如何通过交互方式组合节点 - 如何在群组内限制子节点移动 - 如何自动扩展父节点 - 如何实现父节点的展开/折叠 ## 组合节点 我们可以通过父子关系来实现群组,并提供了一系列用于获取与设置组合关系的[方法](/zh/api/model/cell#父子关系-parentchildren)。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-group-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, 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-group-node', x: 60, y: 100, label: 'Child\n(inner)', zIndex: 2, }) const target = graph.addNode({ shape: 'custom-group-node', x: 420, y: 80, label: 'Child\n(outer)', zIndex: 2, }) const parent = graph.addNode({ shape: 'custom-group-node', x: 40, y: 40, width: 360, height: 160, zIndex: 1, label: 'Parent\n(try to move me)', }) parent.addChild(source) parent.addChild(target) graph.addEdge({ source, target, vertices: [ { x: 120, y: 60 }, { x: 200, y: 100 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="embed-edge-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` 从上述示例可以看出: - 当移动父节点时子节点也会跟着移动,即便子节点位于父节点外部。 - 默认将边的起点和终点的共同父节点视为边的父节点。移动父节点时,边的路径点将跟随移动。 ## 通过交互组合节点 有时需要将一个节点拖入另一个节点,使其成为后者的子节点。此时可以开启 `embedding` 选项,并在节点移动时通过 `findParent` 指定的方法返回父节点。详细配置参见 [API](/zh/tutorial/basic/interacting#组合)。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, embedding: { enabled: true, findParent({ node }) { const bbox = node.getBBox() return this.getNodes().filter((node) => { const data = node.getData<{ parent: boolean }>() if (data && data.parent) { const targetBBox = node.getBBox() return bbox.isIntersectWithRect(targetBBox) } return false }) }, }, }) graph.addNode({ shape: 'custom-node', x: 40, y: 140, width: 120, height: 60, label: 'Child\n(unembed)', zIndex: 10, }) graph.addNode({ shape: 'custom-node', x: 500, y: 100, width: 120, height: 60, label: 'Child\n(unembed)', zIndex: 10, }) graph.addNode({ shape: 'custom-node', x: 200, y: 80, width: 240, height: 160, zIndex: 1, label: 'Parent', data: { parent: true, }, }) graph.on('node:change:parent', ({ node }) => { node.attr({ label: { text: 'Child\n(embed)', }, }) }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="embedding-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 限制子节点的移动 需要将子节点的移动范围限制在父节点内时,可在创建 `Graph` 实例时通过 `translating.restrict` 选项实现。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-group-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, translating: { restrict(view) { if (view) { const cell = view.cell if (cell.isNode()) { const parent = cell.getParent() if (parent) { return parent.getBBox() } } } return null }, }, }) const child = graph.addNode({ shape: 'custom-group-node', x: 100, y: 60, label: 'Child', zIndex: 2, }) const parent = graph.addNode({ shape: 'custom-group-node', x: 40, y: 40, width: 240, height: 160, zIndex: 1, label: 'Parent\n(try to move me)', }) parent.addChild(child) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="restrict-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 自动扩展父节点 监听 `node:change:position` 事件,在子节点移动时自动扩展/收缩父节点的尺寸,使父节点完全包围子节点。该示例代码略复杂,可通过下方 Demo 的 `CodeSandbox` 链接查看完整代码。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Dom } from '@antv/x6' import { Settings, State } from './settings.tsx' Graph.registerNode( 'custom-group-node', { inherit: 'rect', width: 80, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement private embedPadding = 20 componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, embedding: { enabled: true, }, }) const source = graph.addNode({ shape: 'custom-group-node', x: 80, y: 100, width: 80, height: 40, label: 'Child', zIndex: 2, }) const target = graph.addNode({ shape: 'custom-group-node', x: 280, y: 80, label: 'Child', zIndex: 2, }) const parent = graph.addNode({ shape: 'custom-group-node', x: 40, y: 40, width: 360, height: 160, zIndex: 1, label: 'Parent', }) parent.addChild(source) parent.addChild(target) let ctrlPressed = false graph.on('node:embedding', ({ e }: { e: Dom.MouseMoveEvent }) => { ctrlPressed = e.metaKey || e.ctrlKey }) graph.on('node:embedded', () => { ctrlPressed = false }) graph.on('node:change:size', ({ node, options }) => { if (options.skipParentHandler) { return } const children = node.getChildren() if (children && children.length) { node.prop('originSize', node.getSize()) } }) graph.on('node:change:position', ({ node, options }) => { if (options.skipParentHandler || ctrlPressed) { return } const children = node.getChildren() if (children && children.length) { node.prop('originPosition', node.getPosition()) } const parent = node.getParent() if (parent && parent.isNode()) { let originSize = parent.prop('originSize') if (originSize == null) { originSize = parent.getSize() parent.prop('originSize', originSize) } let originPosition = parent.prop('originPosition') if (originPosition == null) { originPosition = parent.getPosition() parent.prop('originPosition', originPosition) } let x = originPosition.x let y = originPosition.y let cornerX = originPosition.x + originSize.width let cornerY = originPosition.y + originSize.height let hasChange = false const children = parent.getChildren() if (children) { children.forEach((child) => { const bbox = child.getBBox().inflate(this.embedPadding) const corner = bbox.getCorner() if (bbox.x < x) { x = bbox.x hasChange = true } if (bbox.y < y) { y = bbox.y hasChange = true } if (corner.x > cornerX) { cornerX = corner.x hasChange = true } if (corner.y > cornerY) { cornerY = corner.y hasChange = true } }) } if (hasChange) { parent.prop( { position: { x, y }, size: { width: cornerX - x, height: cornerY - y }, }, // Note that we also pass a flag so that we know we shouldn't // adjust the `originPosition` and `originSize` in our handlers. { skipParentHandler: true }, ) } } }) } refContainer = (container: HTMLDivElement) => { this.container = container } onSettingsChanged = (state: State) => { this.embedPadding = state.padding } render() { return ( <div className="expand-shrink-app"> <div className="app-side"> <Settings onChange={this.onSettingsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 展开与折叠父节点 首先,我们自定义一个 `Group` 节点,在左上角渲染展开/折叠按钮,并为该按钮设置自定义事件 `node:collapse`: ```ts import { Node } from '@antv/x6' export class Group extends Node { private collapsed: boolean = false private expandSize: { width: number; height: number } protected postprocess() { this.toggleCollapse(false) } isCollapsed() { return this.collapsed } toggleCollapse(collapsed?: boolean) { const target = collapsed == null ? !this.collapsed : collapsed if (target) { this.attr('buttonSign', { d: 'M 1 5 9 5 M 5 1 5 9' }) this.expandSize = this.getSize() this.resize(100, 32) } else { this.attr('buttonSign', { d: 'M 2 5 8 5' }) if (this.expandSize) { this.resize(this.expandSize.width, this.expandSize.height) } } this.collapsed = target } } Group.config({ markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'text', selector: 'label', }, { tagName: 'g', selector: 'buttonGroup', children: [ { tagName: 'rect', selector: 'button', }, { tagName: 'path', selector: 'buttonSign', }, ], }, ], attrs: { body: { ... }, label: { ... }, buttonGroup: { ... }, button: { ... // 自定义事件 event: 'node:collapse', }, buttonSign: { ... }, }, }) ``` 随后,在 `graph` 上监听 `node:collapse` 事件,根据父节点的展开/折叠状态显示或隐藏对应的子节点: ```ts graph.on('node:collapse', ({ node }: { node: Group }) => { node.toggleCollapse() const collapsed = node.isCollapsed() const cells = node.getDescendants() cells.forEach((node) => { if (collapsed) { node.hide() } else { node.show() } }) }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Group } from './shape.ts' Graph.registerNode( 'custom-group-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const createGroup = ( id: string, x: number, y: number, width: number, height: number, fill: string, ) => { const group = new Group({ id, x, y, width, height, attrs: { body: { fill }, label: { text: id }, }, }) graph.addNode(group) return group } const createNode = ( id: string, x: number, y: number, width: number, height: number, ) => { return graph.addNode({ shape: 'custom-group-node', id, x, y, width, height, label: id, }) } const createEdge = ( id: string, source: string, target: string, vertices?: { x: number; y: number }[], ) => { return graph.addEdge({ id, source, target, vertices, label: id, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } const a = createGroup('a', 100, 40, 480, 280, '#91d5ff') const aa = createGroup('aa', 180, 100, 160, 140, '#47C769') const aaa = createGroup('aaa', 200, 160, 120, 40, '#0491e4') const b = createNode('b', 450, 200, 50, 50) a.addChild(aa) aa.addChild(aaa) a.addChild(b) createNode('c', 680, 80, 50, 50) createEdge('edge1', 'aa', 'b') createEdge('edge3', 'b', 'c') aa.addChild( createEdge('edge2', 'aa', 'aaa', [ { x: 60, y: 140 }, { x: 60, y: 220 }, ]), ) graph.on('node:collapse', ({ node }: { node: Group }) => { node.toggleCollapse() const collapsed = node.isCollapsed() const collapse = (parent: Group) => { const cells = parent.getChildren() if (cells) { cells.forEach((cell) => { if (collapsed) { cell.hide() } else { cell.show() } if (cell instanceof Group) { if (!cell.isCollapsed()) { collapse(cell) } } }) } } collapse(node) }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="collapsable-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/tutorial/intermediate/group/embed-edge/index.less ```less .embed-edge-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/interacting/embedding/index.less ```less .embedding-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/intermediate/group/restrict/index.less ```less .restrict-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/intermediate/group/expand-shrink/index.less ```less .expand-shrink-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } } ``` ### docs/demos/tutorial/intermediate/group/expand-shrink/settings.tsx ```tsx import React from 'react' import { Card, Row, Col, Slider } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { padding: number } export class Settings extends React.Component<Props, State> { state: State = { padding: 20, } notifyChange() { this.props.onChange({ ...this.state, }) } onPaddingChanged = (padding: number) => { this.setState({ padding }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>Padding</Col> <Col span={12}> <Slider min={0} max={40} step={1} value={this.state.padding} onChange={this.onPaddingChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.padding}</div> </Col> </Row> <Row align="middle" style={{ borderTop: '1px solid #f0f0f0' }}> <Col style={{ padding: '24px 0 8px 0', color: '#666' }}> Press and hold on{' '} <strong style={{ color: '#faad14' }}>Ctrl or Command</strong> key, then move the child node to remove it from parent node. </Col> </Row> </Card> ) } } ``` ### docs/demos/tutorial/intermediate/group/collapsable/index.less ```less .collapsable-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 360px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/intermediate/group/collapsable/shape.ts ```ts import { Node } from '@antv/x6' export class Group extends Node { private collapsed = false private expandSize: { width: number; height: number } protected postprocess() { this.toggleCollapse(false) } isCollapsed() { return this.collapsed } toggleCollapse(collapsed?: boolean) { const target = collapsed == null ? !this.collapsed : collapsed if (target) { this.attr('buttonSign', { d: 'M 1 5 9 5 M 5 1 5 9' }) this.expandSize = this.getSize() this.resize(100, 32) } else { this.attr('buttonSign', { d: 'M 2 5 8 5' }) if (this.expandSize) { this.resize(this.expandSize.width, this.expandSize.height) } } this.collapsed = target } } Group.config({ markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'text', selector: 'label', }, { tagName: 'g', selector: 'buttonGroup', children: [ { tagName: 'rect', selector: 'button', attrs: { 'pointer-events': 'visiblePainted', }, }, { tagName: 'path', selector: 'buttonSign', attrs: { fill: 'none', 'pointer-events': 'none', }, }, ], }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', stroke: 'none', fill: '#fff', }, buttonGroup: { refX: 8, refY: 8, }, button: { height: 14, width: 16, rx: 2, ry: 2, fill: '#f5f5f5', stroke: '#ccc', cursor: 'pointer', event: 'node:collapse', }, buttonSign: { refX: 3, refY: 2, stroke: '#808080', }, label: { fontSize: 12, fill: '#fff', refX: 32, refY: 10, }, }, }) ``` #### 快捷键 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/plugins/keyboard/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/plugins/keyboard.md - Description: 快捷键 --- title: "快捷键" description: "快捷键" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/plugins/keyboard/" version: "3.x" --- > **在本章节中主要介绍快捷键插件相关的知识,通过阅读,你可以了解到** - 如何为画布绑定快捷键 ## 使用 你可以通过插件 `Keyboard` 启用快捷键功能,示例: ```ts import { Graph, Keyboard } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use( new Keyboard({ enabled: true, }), ) ``` ## 演示 ```ts import './index.less' import { createRoot } from 'react-dom/client' import * as React from 'react' import { Graph, Clipboard, Keyboard, Selection } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.graph.use( new Clipboard({ enabled: true, }), ) this.graph.use( new Selection({ enabled: true, showNodeSelectionBox: true, }), ) this.graph.use( new Keyboard({ enabled: true, global: true, }), ) this.graph.addNode({ x: 280, y: 100, width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const source = this.graph.addNode({ x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 160, y: 180, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph.bindKey('ctrl+c', () => { const cells = this.graph.getSelectedCells() if (cells.length) { this.graph.copy(cells) } return false }) this.graph.bindKey('ctrl+v', () => { if (!this.graph.isClipboardEmpty()) { const cells = this.graph.paste({ offset: 32 }) this.graph.cleanSelection() this.graph.select(cells) } return false }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="keyboard-app"> <div className="app-info"> <p> <span>Ctrl + C</span>Copy Cell <span style={{ marginLeft: 24 }}>Ctrl + V</span>Paste Cell </p> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 配置 | 属性名 | 类型 | 默认值 | 必选 | 描述 | |--------|-------------------------------------------|---------|------|------------------------------------------------------------------------------------------------------------------------------------------| | global | boolean | `false` | | 是否为全局键盘事件,设置为 `true` 时键盘事件绑定在 `document` 上,否则绑定在画布容器上。当绑定在画布容器上时,需要容器获得焦点才能触发键盘事件 | | format | `(this: Graph, key: string) => string` | - | | 绑定或解绑键盘事件时,格式化按键字符串 | | guard | `(this: Graph, e: KeyboardEvent) => boolean` | - | | 判断一个键盘事件是否应该被处理,返回 `false` 时对应的键盘事件被忽略 | `format` 和 `guard` 配置使用如下: ```ts graph.use( new Keyboard({ enabled: true, format(key) { return key.replace(/\s/g, '').replace('cmd', 'command') }, }), ) // 下面语句被格式化后等同于 graph.bindKey('command', (e) => { }) graph.bindKey('cmd', (e) => {}) graph.use( new Keyboard({ enabled: true, guard(this: Graph, e: KeyboardEvent) { if (e.altKey) { // 当按下 alt 键时,忽略所有键盘事件 return false } return true }, }), ) ``` ## API ### graph.bindKey(...) ```ts bindKey( keys: string | string[], callback: (e: KeyboardEvent) => void, action?: 'keypress' | 'keydown' | 'keyup', ): this ``` 绑定快捷键。 ### graph.unbindKey(...) ```ts unbindKey( keys: string | string[], action?: 'keypress' | 'keydown' | 'keyup', ): this ``` 解绑快捷键。 ### graph.clearKeys() ```ts clearKeys(): this ``` 清除所有快捷键。 ### graph.triggerKey() ```ts triggerKey( keys: string, action?: 'keypress' | 'keydown' | 'keyup', ): this ``` 手动触发快捷键。 ### graph.isKeyboardEnabled() ```ts isKeyboardEnabled(): boolean ``` 获取是否启用了键盘事件。 ### graph.enableKeyboard() ```ts enableKeyboard(): this ``` 启用键盘事件。 ### graph.disableKeyboard() ```ts disableKeyboard(): this ``` 禁用键盘事件。 ### graph.toggleKeyboard(...) ```ts toggleKeyboard(enabled?: boolean): this ``` 切换键盘事件的启用状态。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |---------|---------|:----:|--------|----------------------------------------------| | enabled | boolean | | - | 是否启用键盘事件,缺省时切换键盘事件的启用状态。 | ### docs/demos/tutorial/plugins/keyboard/index.less ```less .keyboard-app { border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-info { bottom: 0; margin: 0; padding: 0 8px; } .app-info span { display: inline-block; margin-right: 8px; padding: 2px 8px; font-size: 12px; line-height: 18px; background: #e9e9e9; border-radius: 4px; } .app-content { width: 100%; height: 280px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .x6-widget-selection-box { border: 2px dashed #239edd; } .x6-widget-selection-inner { border: 1px solid #239edd; } } ``` #### 交互 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/basic/interacting/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/basic/interacting.md - Description: 交互 --- title: "交互" description: "交互" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/basic/interacting/" version: "3.x" --- > **本章节主要介绍元素交互相关的知识,通过阅读,你可以了解到** - 如何设置连线交互规则 - 节点之间怎么嵌入 - 怎么配置高亮样式 - 怎么禁止、启用一些交互动作 ## 连线 连线交互规则都是通过 `connecting` 配置来完成,完整的配置参考 [API](/zh/api/model/interaction#连线)。下面介绍一些常用的功能。 ### allowXXX 可以通过 `allowXXX` 配置来定义连线能否连接到对应的位置。默认支持以下项: - `allowBlank`:是否允许连接到画布空白处,默认为 `true`。 - `allowLoop`:是否允许创建循环连线,即边的起始节点和终止节点为同一节点,默认为 `true`。 - `allowNode`:是否允许边连接到节点(非节点上的连接桩),默认为 `true`。 - `allowEdge`:是否允许边连接到另一个边,默认为 `true`。 - `allowPort`:是否允许边连接到连接桩,默认为 `true`。 - `allowMulti`:是否允许在相同的起始节点和终止节点之间创建多条边,默认为 `true`。 它们的值都支持以下两种类型: ```ts new Graph({ connecting: { allowNode: true, // boolean }, }) // 函数形式,多用于动态控制连接限制 new Graph({ connecting: { allowNode(args) { return true }, }, }) ``` > **提示** `allowMulti` 支持设置为字符串 `withPort`,代表在起始和终止节点的相同连接桩之间只允许创建一条边(即起始和终止节点之间可以创建多条边,但必须要连接在不同的连接桩上)。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.initGraph({ allowBlank: true, allowMulti: true, allowLoop: true, allowNode: true, allowEdge: true, allowPort: true, }) } initGraph = (options: State) => { if (this.graph) { this.graph.dispose() } const graph = new Graph({ container: this.container, width: 800, height: 300, background: { color: '#F2F7FA', }, connecting: { ...options, createEdge() { return this.createEdge({ attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) }, }, }) graph.addNode({ x: 60, y: 50, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { in: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, out: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, items: [ { id: 'port1', group: 'in', }, { id: 'port2', group: 'in', }, { id: 'port3', group: 'in', }, { id: 'port4', group: 'out', }, { id: 'port5', group: 'out', }, ], }, }) graph.addNode({ x: 160, y: 200, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { in: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, out: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, items: [ { id: 'port1', group: 'in', }, { id: 'port2', group: 'in', }, { id: 'port3', group: 'in', }, { id: 'port4', group: 'out', }, { id: 'port5', group: 'out', }, ], }, }) graph.addEdge({ source: [320, 80], target: [500, 200], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph = graph } onSettingChanged = (options: State) => { this.initGraph(options) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="connecting-app"> <div className="app-side"> <Settings onChange={this.onSettingChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### router/connector 在[边教程](/zh/tutorial/basic/edge#router)中我们知道,可以在添加边的时候指定 `router` 和 `connector`,如果整个画布中大部分边的 `router` 或者 `connector` 是一样的,我们可以直接配置在 `connecting` 中,这样就可以避免在边中重复配置。 ```ts new Graph({ connecting: { router: 'orth', connector: 'rounded', }, }) ``` ### createEdge 在上面的 demo 中,我们可以从节点、连接桩拉出一条连线出来,那你可能会问,什么样的元素能拉出连线呢?这是 X6 设计非常巧妙的一个地方,只要具备 `magnet=true` 属性的元素,都可以拉出连线。而且在 `connecting` 中可以通过 `createEdge` 方法配置拉出连线的样式。 ```ts new Graph({ connecting: { createEdge() { return this.createEdge({ shape: 'edge', attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) }, }, }) ``` ### validateXXX 我们还可以通过 `validateXXX` 方法来定义是否能创建连线或者连线是否有效。相比 `allowXXX`,`validateXXX` 更具备灵活性。默认支持以下项: - `validateMagnet`:点击 `magnet=true` 的元素时 根据 `validateMagnet` 返回值来判断是否新增边,触发时机是元素被按下,如果返回 `false`,则没有任何反应,如果返回 `true`,会在当前元素创建一条新的边。 - `validateConnection`:在移动边的时候判断连接是否有效,如果返回 `false`,当鼠标放开的时候,不会连接到当前元素,常常配合[高亮](/zh/tutorial/basic/interacting#高亮)功能一起使用。 - `validateEdge`:当停止拖动边的时候根据 `validateEdge` 返回值来判断边是否生效,如果返回 `false`,该边会被清除。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node-with-port', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { top: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, bottom: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, connecting: { allowNode: false, allowBlank: false, highlight: true, createEdge() { return this.createEdge({ attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) }, validateMagnet({ magnet }) { // 节点上方的连接桩无法创建连线 return magnet.getAttribute('port-group') !== 'top' }, validateConnection({ sourceCell, targetCell, sourceMagnet, targetMagnet, }) { // 不能连接自身 if (sourceCell === targetCell) { return false } // 只能从 bottom 连接桩开始连接,连接到 top 连接桩 if ( !sourceMagnet || sourceMagnet.getAttribute('port-group') === 'top' ) { return false } if ( !targetMagnet || targetMagnet.getAttribute('port-group') !== 'top' ) { return false } // 不能重复连线 const edges = this.getEdges() const portId = targetMagnet.getAttribute('port') if (edges.find((edge) => edge.getTargetPortId() === portId)) { return false } return true }, }, highlighting: { // 连接桩可以被连接时在连接桩外围围渲染一个包围框 magnetAvailable: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#A4DEB1', strokeWidth: 4, }, }, }, // 连接桩吸附连线时在连接桩外围围渲染一个包围框 magnetAdsorbed: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#31d0c6', strokeWidth: 4, }, }, }, }, }) graph.addNode({ shape: 'custom-node-with-port', x: 40, y: 40, label: 'hello', ports: { items: [ { id: 'port_1', group: 'top', }, { id: 'port_2', group: 'top', }, { id: 'port_3', group: 'bottom', }, { id: 'port_4', group: 'bottom', }, ], }, }) graph.addNode({ shape: 'custom-node-with-port', x: 160, y: 180, label: 'world', ports: { items: [ { id: 'port_5', group: 'top', }, { id: 'port_6', group: 'top', }, { id: 'port_7', group: 'bottom', }, { id: 'port_8', group: 'bottom', }, ], }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="validate-app "> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 组合 有时候我们需要将一个节点拖动到另一个节点中,使其成为另一节点的子节点,这时我们可以通过 `embedding` 选项来开启,在节点被移动时通过 `findParent` 指定的方法返回父节点。更详细的配置参考 [API](/zh/api/model/interaction#组合)。 ```ts const graph = new Graph({ embedding: { enabled: true, findParent({ node }) { // 获取移动节点的包围盒 const bbox = node.getBBox() // 找到 data 中配置 { parent: true } 的节点,并且移动节点和找到的节点包围盒相交时,返回 true return this.getNodes().filter((node) => { const data = node.getData<{ parent: boolean }>() if (data && data.parent) { const targetBBox = node.getBBox() return bbox.isIntersectWithRect(targetBBox) } return false }) }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, embedding: { enabled: true, findParent({ node }) { const bbox = node.getBBox() return this.getNodes().filter((node) => { const data = node.getData<{ parent: boolean }>() if (data && data.parent) { const targetBBox = node.getBBox() return bbox.isIntersectWithRect(targetBBox) } return false }) }, }, }) graph.addNode({ shape: 'custom-node', x: 40, y: 140, width: 120, height: 60, label: 'Child\n(unembed)', zIndex: 10, }) graph.addNode({ shape: 'custom-node', x: 500, y: 100, width: 120, height: 60, label: 'Child\n(unembed)', zIndex: 10, }) graph.addNode({ shape: 'custom-node', x: 200, y: 80, width: 240, height: 160, zIndex: 1, label: 'Parent', data: { parent: true, }, }) graph.on('node:change:parent', ({ node }) => { node.attr({ label: { text: 'Child\n(embed)', }, }) }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="embedding-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 高亮 可以通过 `highlighting` 选项来指定触发某种交互时的高亮样式,如: ```ts new Graph({ highlighting: { // 连接桩可以被连接时在连接桩外围渲染一个包围框 magnetAvailable: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#A4DEB1', strokeWidth: 4, }, }, }, // 连接桩吸附连线时在连接桩外围渲染一个包围框 magnetAdsorbed: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#31d0c6', strokeWidth: 4, }, }, }, }, }) ``` 支持的 `highlighting` 配置项有: - `default` 默认高亮选项,当以下几种高亮配置缺省时被使用。 - `embedding` 拖动节点进行嵌入操作过程中,节点可以被嵌入时被使用。 - `nodeAvailable` 连线过程中,节点可以被链接时被使用。 - `magnetAvailable` 连线过程中,连接桩可以被链接时被使用。 - `magnetAdsorbed` 连线过程中,自动吸附到连接桩时被使用。 上面 `magnetAvailable.name` 其实是高亮器的名称,X6 内置了 `stroke` 和 `className` 两种高亮器,详细信息参考 [Highlighter](/zh/api/registry/highlighter)。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node-with-port', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { top: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, bottom: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, connecting: { allowNode: false, allowBlank: false, highlight: true, createEdge() { return this.createEdge({ attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) }, }, highlighting: { // 连接桩可以被连接时在连接桩外围围渲染一个包围框 magnetAvailable: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#A4DEB1', strokeWidth: 4, }, }, }, // 连接桩吸附连线时在连接桩外围围渲染一个包围框 magnetAdsorbed: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#31d0c6', strokeWidth: 4, }, }, }, }, }) graph.addNode({ shape: 'custom-node-with-port', x: 40, y: 40, label: 'hello', ports: { items: [ { id: 'port_1', group: 'bottom', }, { id: 'port_2', group: 'bottom', }, ], }, }) graph.addNode({ shape: 'custom-node-with-port', x: 160, y: 180, label: 'world', ports: { items: [ { id: 'port_3', group: 'top', }, { id: 'port_4', group: 'top', }, ], }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="highlighting-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 交互限制 可以通过配置 `interacting` 来启动、禁用一些元素的交互行为,如果画布上元素纯预览,不能进行任何交互,可以直接设置为 `false`。 ```ts new Graph({ interacting: false, }) ``` 如果需要更细节的定义允许哪些交互、禁用哪些交互,我们可以针对不同的属性值进行配置,支持的属性包括: - `nodeMovable` 节点是否可以被移动。 - `magnetConnectable` 当在具有 `magnet` 属性的元素上按下鼠标开始拖动时,是否触发连线交互。 - `edgeMovable` 边是否可以被移动。 - `edgeLabelMovable` 边的标签是否可以被移动。 - `arrowheadMovable` 边的起始/终止箭头(在使用 arrowhead 工具后)是否可以被移动。 - `vertexMovable` 边的路径点是否可以被移动。 - `vertexAddable` 是否可以添加边的路径点。 - `vertexDeletable` 边的路径点是否可以被删除。 它们的值都支持以下两种类型: ```ts // 直接设置为 boolean 值 new Graph({ interacting: { nodeMovable: false, edgeMovable: true, }, }) // 函数形式,多用于动态控制交互行为 new Graph({ interacting: { nodeMovable(view) { const node = view.cell const { enableMove } = node.getData() return enableMove }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.initGraph({ nodeMovable: false, magnetConnectable: false, edgeMovable: false, edgeLabelMovable: false, }) } initGraph = (options: State) => { if (this.graph) { this.graph.dispose() } const graph = new Graph({ container: this.container, width: 800, height: 300, background: { color: '#F2F7FA', }, interacting: { ...options, }, connecting: { createEdge() { return this.createEdge({ attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) }, }, }) graph.addNode({ x: 60, y: 50, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { in: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, out: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, items: [ { id: 'port1', group: 'in', }, { id: 'port2', group: 'in', }, { id: 'port3', group: 'in', }, { id: 'port4', group: 'out', }, { id: 'port5', group: 'out', }, ], }, }) graph.addNode({ x: 160, y: 200, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { in: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, out: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, items: [ { id: 'port1', group: 'in', }, { id: 'port2', group: 'in', }, { id: 'port3', group: 'in', }, { id: 'port4', group: 'out', }, { id: 'port5', group: 'out', }, ], }, }) graph.addEdge({ source: [320, 80], target: [500, 200], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, label: 'Move Me', }) this.graph = graph } onSettingChanged = (options: State) => { this.initGraph(options) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="interacting-app"> <div className="app-side"> <Settings onChange={this.onSettingChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/tutorial/basic/interacting/connecting/index.less ```less .connecting-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; height: 100%; padding: 0 8px; } .app-content { flex: 1; margin-top: 0; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/interacting/connecting/settings.tsx ```tsx import React from 'react' import { Checkbox, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { allowBlank: boolean allowMulti: boolean allowLoop: boolean allowNode: boolean allowEdge: boolean allowPort: boolean } export class Settings extends React.Component<Props, State> { state: State = { allowBlank: true, allowMulti: true, allowLoop: true, allowNode: true, allowEdge: true, allowPort: true, } notifyChange() { this.props.onChange({ ...this.state, }) } onAllowTypeChanged = (type: string, flag: boolean) => { const s = { [type]: flag } as any this.setState(s, () => { this.notifyChange() }) } render() { return ( <Card title="Connecting Settings" size="small" bordered={false} style={{ width: 240 }} > <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.allowBlank} onChange={(e) => this.onAllowTypeChanged('allowBlank', e.target.checked) } > AllowBlank </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.allowMulti} onChange={(e) => this.onAllowTypeChanged('allowMulti', e.target.checked) } > AllowMulti </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.allowLoop} onChange={(e) => this.onAllowTypeChanged('allowLoop', e.target.checked) } > AllowLoop </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.allowNode} onChange={(e) => this.onAllowTypeChanged('allowNode', e.target.checked) } > AllowNode </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.allowEdge} onChange={(e) => this.onAllowTypeChanged('allowEdge', e.target.checked) } > AllowEdge </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.allowPort} onChange={(e) => this.onAllowTypeChanged('allowPort', e.target.checked) } > AllowPort </Checkbox> </Col> </Row> </Card> ) } } ``` ### docs/demos/tutorial/basic/interacting/validate/index.less ```less .validate-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` ### docs/demos/tutorial/basic/interacting/embedding/index.less ```less .embedding-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/interacting/highlighting/index.less ```less .highlighting-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` ### docs/demos/tutorial/basic/interacting/interacting/index.less ```less .interacting-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; height: 100%; padding: 0 8px; } .app-content { flex: 1; margin-top: 0; box-shadow: 0 0 10px 1px #e9e9e9; } } .ant-card { min-height: 250px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } ``` ### docs/demos/tutorial/basic/interacting/interacting/settings.tsx ```tsx import React from 'react' import { Checkbox, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { nodeMovable: boolean magnetConnectable: boolean edgeMovable: boolean edgeLabelMovable: boolean } export class Settings extends React.Component<Props, State> { state: State = { nodeMovable: false, magnetConnectable: false, edgeMovable: false, edgeLabelMovable: false, } notifyChange() { this.props.onChange({ ...this.state, }) } onAllowTypeChanged = (type: string, flag: boolean) => { const s = { [type]: flag } as any this.setState(s, () => { this.notifyChange() }) } render() { return ( <Card title="Connecting Settings" size="small" bordered={false} style={{ width: 240 }} > <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.nodeMovable} onChange={(e) => this.onAllowTypeChanged('nodeMovable', e.target.checked) } > nodeMovable </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.magnetConnectable} onChange={(e) => this.onAllowTypeChanged('magnetConnectable', e.target.checked) } > magnetConnectable </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.edgeMovable} onChange={(e) => this.onAllowTypeChanged('edgeMovable', e.target.checked) } > edgeMovable </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.edgeLabelMovable} onChange={(e) => this.onAllowTypeChanged('edgeLabelMovable', e.target.checked) } > edgeLabelMovable </Checkbox> </Col> </Row> </Card> ) } } ``` #### React 节点 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/intermediate/react/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/intermediate/react.md - Description: React 节点 --- title: "React 节点" description: "React 节点" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/intermediate/react/" version: "3.x" --- > **在本章节中主要介绍 React 节点相关的知识,通过阅读,你可以了解到** - 如何使用 React 组件来渲染节点内容 - 如何更新节点内容 ## 渲染节点 我们提供了一个独立的包 `@antv/x6-react-shape`,用于通过 React 渲染节点。 > **注意** 版本兼容关系:X6 3.x 须使用 x6-react-shape 3.x 版本。同时 x6-react-shape 自 2.0.8 起仅支持 React 18 及以上;若项目低于 React 18,请将 x6-react-shape 锁定到 2.0.8 并使用 X6 2.x。 ```ts import { register } from '@antv/x6-react-shape' const NodeComponent = () => { return ( <div className="react-node"> <Progress type="circle" percent={30} width={80} /> </div> ) } register({ shape: 'custom-basic-react-node', width: 100, height: 100, component: NodeComponent, }) graph.addNode({ shape: 'custom-basic-react-node', x: 60, y: 100, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { register } from '@antv/x6-react-shape' import { Progress } from 'antd' const NodeComponent = () => { return ( <div className="react-node"> <Progress type="circle" percent={30} width={80} /> </div> ) } register({ shape: 'custom-basic-react-node', width: 100, height: 100, component: NodeComponent, }) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addNode({ shape: 'custom-basic-react-node', x: 60, y: 100, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="react-basic-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 更新节点 与 `HTML` 类似,注册节点时可提供 `effect` 字段(当前节点的 `props` 数组)。当 `effect` 中任一 `prop` 发生变化时,会重新渲染对应的 React 组件。 ```ts register({ shape: 'custom-update-react-node', width: 100, height: 100, effect: ['data'], component: NodeComponent, }) const node = graph.addNode({ shape: 'custom-update-react-node', x: 60, y: 100, data: { progress: 30, }, }) setInterval(() => { const { progress } = node.getData<{ progress: number }>() node.setData({ progress: (progress + 10) % 100, }) }, 1000) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { register } from '@antv/x6-react-shape' import { Progress } from 'antd' const NodeComponent = ({ node }: { node: Node }) => { const { progress } = node.getData() return ( <div className="react-node"> <Progress type="circle" percent={progress} width={80} /> </div> ) } register({ shape: 'custom-update-react-node', width: 100, height: 100, effect: ['data'], component: NodeComponent, }) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const node = graph.addNode({ shape: 'custom-update-react-node', x: 60, y: 100, data: { progress: 30, }, }) setInterval(() => { const { progress } = node.getData<{ progress: number }>() node.setData({ progress: (progress + 10) % 100, }) }, 1000) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="react-update-app "> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Portal 方式 上述渲染方式有一个缺点:组件会被直接渲染到节点的 DOM 中,方式如下。 ```ts import { createRoot, Root } from 'react-dom/client' const root = createRoot(container) // container 为节点容器 root.render(component) ``` 此时组件不在常规渲染树中,因而无法访问外部 `Context`。若有此需求,请使用 `Portal` 模式来渲染 React 组件。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { register, getProvider } from '@antv/x6-react-shape' import { Progress, Button } from 'antd' const X6ReactPortalProvider = getProvider() // 注意,一个 graph 只能申明一个 portal provider const ProgressContext = React.createContext(30) const NodeComponent = () => { const progress = React.useContext(ProgressContext) return ( <div className="react-node"> <Progress type="circle" percent={progress} width={80} /> </div> ) } register({ shape: 'custom-portal-react-node', width: 100, height: 100, component: NodeComponent, }) export default class Example extends React.Component { private container: HTMLDivElement state = { progress: 30, } componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addNode({ shape: 'custom-portal-react-node', x: 60, y: 100, }) graph.centerContent() } changeProgress = () => { this.setState({ progress: (this.state.progress + 10) % 100, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="react-portal-app"> <ProgressContext.Provider value={this.state.progress}> <X6ReactPortalProvider /> </ProgressContext.Provider> <div className="app-btns"> <Button onClick={this.changeProgress}>Add</Button> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/tutorial/intermediate/react/basic/index.less ```less .react-basic-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .react-node { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; border: 1px solid #8f8f8f; border-radius: 6px; } } ``` ### docs/demos/tutorial/intermediate/react/update/index.less ```less .react-update-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .react-node { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; border: 1px solid #8f8f8f; border-radius: 6px; } } ``` ### docs/demos/tutorial/intermediate/react/portal/index.less ```less .react-portal-app { border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-btns { bottom: 0; margin: 0; padding: 0 8px; } .app-content { width: 100%; height: 240px; margin: 0; margin-top: 16px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } .react-node { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; border: 1px solid #8f8f8f; border-radius: 6px; } } ``` #### 撤销重做 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/plugins/history/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/plugins/history.md - Description: 撤销重做 --- title: "撤销重做" description: "撤销重做" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/plugins/history/" version: "3.x" --- > **在本章节中主要介绍撤销重做插件相关的知识,通过阅读,你可以了解到** - 如何实现元素操作的撤销与重做 - 如何设置是否记录元素添加、删除和属性更改 - 如何将多个改变合并为一条历史记录 ## 使用 你可以通过插件 `History` 启用撤销重做功能,示例: ```ts import { Graph, History } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use( new History({ enabled: true, }), ) ``` ## 演示 - 随意移动节点后,Undo 按钮变得可用。 - 点击 Undo 按钮,节点位置被还原,然后 Redo 按钮变得可用。 - 点击 Redo 按钮,节点位置被更新。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Button } from 'antd' import { Graph, History } from '@antv/x6' interface Props {} interface State { canUndo: boolean canRedo: boolean } export default class App extends React.Component<Props, State> { private container: HTMLDivElement private graph: Graph state: State = { canRedo: false, canUndo: false, } componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.use( new History({ enabled: true, }), ) graph.addNode({ x: 100, y: 60, width: 100, height: 40, label: 'Drag Me', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.on('history:change', () => { this.setState({ canRedo: graph.canRedo(), canUndo: graph.canUndo(), }) }) this.graph = graph } onUndo = () => { this.graph.undo() } onRedo = () => { this.graph.redo() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="history-app"> <div className="app-btns"> <Button.Group> <Button onClick={this.onUndo} disabled={!this.state.canUndo}> Undo </Button> <Button onClick={this.onRedo} disabled={!this.state.canRedo}> Redo </Button> </Button.Group> </div> <div ref={this.refContainer} className="app-content" /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<App />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 配置 | 属性名 | 类型 | 默认值 | 必选 | 描述 | |------------------|---------------------------------|---------|------|--------------------------------------------------------------------------------------------------| | stackSize | number | `0` | | `stackSize` 为 0 表示不限制历史记录栈的长度,如果设置为其他数字表示最多只会记录该数字长度的历史记录 | | ignoreAdd | boolean | `false` | | `ignoreAdd` 如果为 `true`,添加元素不会被记录到历史记录 | | ignoreRemove | boolean | `false` | | `ignoreRemove` 如果为 `true`,删除元素不会被记录到历史记录 | | ignoreChange | boolean | `false` | | `ignoreChange` 如果为 `true`,元素属性变化不会被记录到历史记录 | | beforeAddCommand | `(event, args) => any` | - | | 当一个命令被添加到 Undo 队列前被调用,如果该方法返回 `false`,那么这个命令将不会被添加到 Undo 队列中 | | afterAddCommand | `(event, args, cmd) => any` | - | | 当一个命令被添加到 Undo 队列后被调用 | | executeCommand | `(cmd, revert, options) => any` | - | | 当命令被撤销或重做时被调用,`revert` 为 `true` 表示命令被撤销,否则表示命令被重做 | > **提示** 在实际项目中,我们经常会需要将多个改变一次性撤销或者重做,X6 中提供 `batch` 的概念,可以将多个改变合并成一个历史记录。使用方式如下: ```ts // 方式一 graph.startBatch('custom-batch-name') // 节点改变边框颜色以及修改位置会合并成一条记录,可以一次性撤销 node.attr('body/stroke', 'red') node.position(30, 30) graph.stopBatch('custom-batch-name') // 方式二 graph.batchUpdate(() => { node.prop('zIndex', 10) node.attr('label/text', 'hello') node.attr('label/fill', '#ff0000') }) ``` ## API ### graph.undo(...) ```ts undo(options?: KeyValue): this ``` 撤销。`options` 将被传递到事件回调中。 ### graph.undoAndCancel(...) ```ts undoAndCancel(options?: KeyValue): this ``` 撤销,并且不添加到重做队列中,因此该命令不能被重做。`options` 将被传递到事件回调中。 ### graph.redo(...) ```ts redo(options?: KeyValue): this ``` 重做。`options` 将被传递到事件回调中。 ### graph.canUndo() ```ts canUndo(): boolean ``` 是否可以撤销。 ### graph.canRedo() ```ts canRedo(): boolean ``` 是否可以重做。 ### graph.cleanHistory(...) ```ts cleanHistory(options?: KeyValue): this ``` 清空历史队列。`options` 将被传递到事件回调中。 ### graph.getHistoryStackSize(...) ```ts getHistoryStackSize(): number ``` 获取 history 栈的尺寸。 ### graph.getUndoRemainSize(...) ```ts getUndoRemainSize(): number ``` 获取 history undo 栈的剩余尺寸。 ### graph.getUndoStackSize(...) ```ts getUndoStackSize(): number ``` 获取 history undo 栈的尺寸。 ### graph.getRedoStackSize(...) ```ts getRedoStackSize(): number ``` 获取 history redo 栈的尺寸。 ### graph.isHistoryEnabled() ```ts isHistoryEnabled(): boolean ``` 返回是否启用了历史状态。 ### graph.enableHistory() ```ts enableHistory(): this ``` 启用历史状态。 ### graph.disableHistory() ```ts disableHistory(): this ``` 禁用历史状态。 ### graph.toggleHistory(...) ```ts toggleHistory(enabled?: boolean): this ``` 切换历史的启用状态。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |---------|---------|:----:|--------|------------------------------------------| | enabled | boolean | | - | 是否启用历史状态,缺省时切换历史的启用状态。 | ## 事件 | 事件名称 | 参数类型 | 描述 | |------------------|--------------------------------------------------|-------------------------| | `history:undo` | `{ cmds: Command[], options: KeyValue }` | 当命令被撤销时触发 | | `history:redo` | `{ cmds: Command[], options: KeyValue }` | 当命令被重做时触发 | | `history:cancel` | `{ cmds: Command[], options: KeyValue }` | 当命令被取消时触发 | | `history:add` | `{ cmds: Command[], options: KeyValue }` | 当命令被添加到队列时触发 | | `history:clean` | `{ cmds: Command[] \| null, options: KeyValue }` | 当历史队列被清空时触发 | | `history:change` | `{ cmds: Command[] \| null, options: KeyValue }` | 当历史队列改变时触发 | | `history:batch` | `{ cmds: Command, options: KeyValue }` | 当接收到 batch 命令时触发 | ```ts graph.on('history:undo', ({ cmds }) => { console.log(cmds) }) // 我们也可以在插件实例上监听事件 history.on('undo', ({ cmds }) => { console.log(cmds) }) ``` ### docs/demos/tutorial/plugins/history/index.less ```less .history-app { border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-btns { bottom: 0; margin: 0; padding: 0 8px; } .app-content { width: 100%; height: 240px; margin: 0; margin-top: 16px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### 事件 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/basic/events/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/basic/events.md - Description: 事件 --- title: "事件" description: "事件" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/basic/events/" version: "3.x" --- > **在本章节中主要介绍事件相关的知识,通过阅读,你可以了解到** - 可以监听哪些类别的事件 - 如何监听事件 ## 视图交互事件 通过鼠标、键盘或者各种可交互的组件与应用产生交互时触发的事件。 ### 鼠标事件 | 事件 | cell 节点/边 | node 节点 | port 连接桩 | edge 边 | blank 画布空白区域 | |--------|--------------------|--------------------|-------------------------|--------------------|---------------------| | 单击 | `cell:click` | `node:click` | `node:port:click` | `edge:click` | `blank:click` | | 双击 | `cell:dblclick` | `node:dblclick` | `node:port:dblclick` | `edge:dblclick` | `blank:dblclick` | | 右键 | `cell:contextmenu` | `node:contextmenu` | `node:port:contextmenu` | `edge:contextmenu` | `blank:contextmenu` | | 鼠标按下 | `cell:mousedown` | `node:mousedown` | `node:port:mousedown` | `edge:mousedown` | `blank:mousedown` | | 移动鼠标 | `cell:mousemove` | `node:mousemove` | `node:port:mousemove` | `edge:mousemove` | `blank:mousemove` | | 鼠标抬起 | `cell:mouseup` | `node:mouseup` | `node:port:mouseup` | `edge:mouseup` | `blank:mouseup` | | 鼠标滚轮 | `cell:mousewheel` | `node:mousewheel` | - | `edge:mousewheel` | `blank:mousewheel` | | 鼠标进入 | `cell:mouseenter` | `node:mouseenter` | `node:port:mouseenter` | `edge:mouseenter` | `graph:mouseenter` | | 鼠标离开 | `cell:mouseleave` | `node:mouseleave` | `node:port:mouseleave` | `edge:mouseleave` | `graph:mouseleave` | > **注意** 需要注意,这里的 `mousemove` 事件不同于常规的鼠标移动事件:仅在按下鼠标拖动时触发。 除了 `mouseenter` 和 `mouseleave` 外,事件回调函数的参数都包含鼠标相对于画布的位置 `x`、`y` 和鼠标事件对象 `e` 等参数。 ```ts 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` 来监听该元素的点击事件,例如: ```ts node.attr({ // 表示一个删除按钮,点击时删除该节点 image: { event: 'node:delete', xlinkHref: 'trash.png', width: 20, height: 20, }, }) ``` 可以通过绑定的事件名 `node:delete` 或通用的 `cell:customevent`、`node:customevent`、`edge:customevent` 事件名来监听。 ```ts 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() } }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' 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> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### 画布缩放/平移 | 事件名 | 回调参数 | 说明 | |-------------|------------------------------------------------------|---------------------------------------------------------------| | `scale` | `{ sx: number; sy: number; ox: number; oy: number }` | 缩放画布时触发,`sx` 和 `sy` 是缩放比例,`ox` 和 `oy` 是缩放中心。 | | `resize` | `{ width: number; height: number }` | 改变画布大小时触发,`width` 和 `height` 是画布大小。 | | `translate` | `{ tx: number; ty: number }` | 平移画布时触发,`tx` 和 `ty` 分别是 X 和 Y 轴的偏移量。 | ```ts graph.on('scale', ({ sx, sy, ox, oy }) => {}) graph.on('resize', ({ width, height }) => {}) graph.on('translate', ({ tx, ty }) => {}) ``` ### 节点或边移动 | 事件名 | 回调参数 | 说明 | |---------------|-------------------------------------------------------------------------------|-------------------| | `node:move` | `{ e: Dom.MouseDownEvent; x: number; y: number; node: Node; view: NodeView }` | 开始移动节点时触发。 | | `node:moving` | `{ e: Dom.MouseMoveEvent; x: number; y: number; node: Node; view: NodeView }` | 移动节点时触发。 | | `node:moved` | `{ e: Dom.MouseUpEvent; x: number; y: number; node: Node; view: NodeView }` | 移动节点后触发。 | | `edge:move` | `{ e: Dom.MouseDownEvent; x: number; y: number; node: Node; view: NodeView }` | 开始移动边时触发。 | | `edge:moving` | `{ e: Dom.MouseMoveEvent; x: number; y: number; node: Node; view: NodeView }` | 移动边时触发。 | | `edge:moved` | `{ e: Dom.MouseUpEvent; x: number; y: number; node: Node; view: NodeView }` | 移动边后触发。 | 参数中的 `x` 和 `y` 是鼠标相对于画布的坐标。 ```ts graph.on('node:moved', ({ e, x, y, node, view }) => {}) ``` ### 节点嵌入 | 事件名 | 回调参数 | 说明 | |------------------|---------------------------------------------------------------------------------------------------------------------------|------------------------------| | `node:embed` | `{ e: Dom.MouseDownEvent; x: number; y: number; node: Node; view: NodeView, currentParent: Node }` | 开启嵌入,在开始拖动节点时触发。 | | `node:embedding` | `{ e: Dom.MouseMoveEvent; x: number; y: number; node: Node; view: NodeView, currentParent: Node, candidateParent: Node }` | 寻找目标节点过程中触发。 | | `node:embedded` | `{ e: Dom.MouseUpEvent; x: number; y: number; node: Node; view: NodeView, previousParent: Node, currentParent: Node }` | 完成节点嵌入后触发。 | ### 边连接/取消连接 当拖动边的起始/终止箭头将边连接到节点/边,或将边从节点/边上分离后,会触发 `edge:connected`,回调函数的参数如下。 ```ts interface Args { e: Dom.MouseUpEvent // 鼠标事件对象 edge: Edge // 边 view: EdgeView // 边的视图 isNew: boolean // 是否是新创建的边 type: Edge.TerminalType // 操作的是起始箭头还是终止箭头('source' | 'target') previousCell?: Cell | null // 交互前连接到的节点/边 previousView?: CellView | null // 交互前连接到的节点/边的视图 previousPort?: string | null // 交互前连接到的连接桩 ID previousPoint?: Point.PointLike | null // 交互前连接到的点(将边的终端从空白处拖动到节点/边上时记录起始终端的位置) previousMagnet?: Element | null // 交互前连接到的元素 currentCell?: Cell | null // 交互后连接到的节点/边 currentView?: CellView | null // 交互后连接到的节点/边的视图 currentPort?: string | null // 交互后连接到的连接桩 ID currentPoint?: Point.PointLike | null // 交互后连接到的点(将边的终端从节点/边上拖动到空白处时记录拖动后终端的位置) currentMagnet?: Element | null // 交互后连接到的元素 } ``` 我们可以通过 `isNew` 来判断连线完成后,对应的边是否是新创建的边。比如从一个连接桩开始,创建了一条边并连接到另一个节点/连接桩,此时 `isNew` 就为 `true`。 ```ts graph.on('edge:connected', ({ isNew, edge }) => { if (isNew) { // 对新创建的边进行插入数据库等持久化操作 } }) ``` 特别注意的是,参数中的 `previous...` 是记录操作终端在连接/取消连接之前的状态,并不是指 `sourceCell`,在创建新边后获取 `sourceCell` 时不能使用 `previousCell`,正确的使用方式是: ```ts graph.on('edge:connected', ({ isNew, edge }) => { if (isNew) { const source = edge.getSourceCell() } }) ``` ## 节点/边 ### 添加/删除/修改 当节点/边被添加到画布时,触发以下事件: - `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 是边时才触发) 可以在节点/边上监听: ```ts cell.on('added', ({ cell, index, options }) => {}) cell.on('removed', ({ cell, index, options }) => {}) cell.on('changed', ({ cell, options }) => {}) ``` 或者在 Graph 上监听: ```ts 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` 事件,并触发节点/边重绘。例如: ```ts 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 是边时才触发) 可以在节点/边上监听: ```ts // 当 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 上监听: ```ts 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,例如 ```ts cell.on('change:custom', ({ cell, current, previous, options }) => { console.log(current) }) ``` 当通过 `cell.prop('custom', 'any data')` 方法修改 `custom` 属性的值时将触发 `change:custom` 事件。 ### 动画 动画系统中支持以下事件: - `cell:animation:finish` 动画结束时触发 - `cell:animation:cancel` 动画取消时触发 - `node:animation:finish` 动画结束时触发(仅当 cell 是节点时才触发) - `node:animation:cancel` 动画取消时触发(仅当 cell 是节点时才触发) - `edge:animation:finish` 动画结束时触发(仅当 cell 是边时才触发) - `edge:animation:cancel` 动画取消时触发(仅当 cell 是边时才触发) ## 视图 由于 X6 实现了异步的渲染调度算法,所以节点的添加不一定意味着挂载到画布上。节点在被挂载到画布时以及从画布上卸载时会分别触发单独的事件。 | 事件名 | 回调参数 | 说明 | |------------------|----------------------|-------------------------| | `view:mounted` | `{ view: CellView }` | 节点被挂载到画布上时触发。 | | `view:unmounted` | `{ view: CellView }` | 节点从画布上卸载时触发。 | ```ts graph.on('view:mounted', ({ view }) => {}) graph.on('view:unmounted', ({ view }) => {}) ``` 大家经常需要在调用 `fromJSON` 或 `resetCells` 后,监听画布完成渲染事件,此时可以使用 `render:done` 事件(2.15.1 版本新增)。 ```typescript graph.on('render:done', () => { // pass }) graph.fromJSON([...]) ``` ### docs/demos/tutorial/basic/event/custom-click/index.less ```less .custom-click-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 200px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### Vue 节点 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/intermediate/vue/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/intermediate/vue.md - Description: Vue 节点 --- title: "Vue 节点" description: "Vue 节点" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/intermediate/vue/" version: "3.x" --- > **在本章节中主要介绍 Vue 节点相关的知识,通过阅读,你可以了解到** - 如何使用 Vue 组件来渲染节点内容 - 如何更新节点内容 ## 渲染节点 我们提供一个独立的包 `@antv/x6-vue-shape`,用于通过 Vue 组件渲染节点。 > **注意** 版本兼容关系:X6 3.x 须使用 x6-vue-shape 3.x 版本。 ```html <template> <div class="app-content"> <div id="container"></div> <TeleportContainer /> </div> </template> <script lang="ts"> import { defineComponent } from 'vue' import ProgressNode from './components/ProgressNode.vue' import { Graph } from '@antv/x6' import { register, getTeleport } from '@antv/x6-vue-shape' register({ shape: 'custom-vue-node', width: 100, height: 100, component: ProgressNode, }) const TeleportContainer = getTeleport() export default defineComponent({ name: 'App', components: { TeleportContainer, }, mounted() { const graph = new Graph({ container: document.getElementById('container')!, background: { color: '#F2F7FA', }, autoResize: true, }) graph.addNode({ shape: 'custom-vue-node', x: 100, y: 60, }) }, }) </script> ``` 节点组件内容如下: ```html <template> <el-progress type="dashboard" :percentage="percentage" :width="80"> <template #default="{ percentage }"> <span class="percentage-value">{{ percentage }}%</span> </template> </el-progress> </template> <script lang="ts"> import { defineComponent } from 'vue' export default defineComponent({ name: 'ProgressNode', inject: ['getNode'], data() { return { percentage: 80, } }, mounted() { const node = (this as any).getNode() console.log(node) }, }) </script> ``` 效果如下: <iframe src="/demos/vue/basic/index.html" style="width: 100%; height: 260px; border: 0px; overflow: hidden;"></iframe> ## 更新节点 `Vue` 节点中更新节点内容,可以使用两种方式: - 在组件内部监听节点事件,在外部触发事件。 - 使用 `Vuex` 等状态管理器(这里不展开讲,和普通的 `Vue` 组件一样使用 `Vuex` 数据)。 下面介绍使用事件的方式来动态更新节点内容: ```ts const node = graph.addNode({ shape: 'custom-vue-node', x: 100, y: 60, data: { progress: 80, }, }) setInterval(() => { const { progress } = node.getData() node.setData({ progress: (progress + 10) % 100, }) }, 2000) ``` 在节点组件内部,我们可以监听节点 `data` 改变事件: ```ts export default defineComponent({ name: 'ProgressNode', inject: ['getNode'], data() { return { percentage: 80, } }, mounted() { const node = (this as any).getNode() as Node node.on('change:data', ({ current }) => { const { progress } = current this.percentage = progress }) }, }) ``` <iframe src="/demos/vue/update/index.html" style="width: 100%; height: 260px; border: 0px; overflow: hidden;"></iframe> ## 在 Vue 2 中使用 上述示例使用了 `Teleport`,它是 `Vue 3` 的特性;在 `Vue 2` 中如何使用? ```html <template> <div id="app"></div> </template> <script lang="ts"> import Vue from 'vue' import CustomNode from './components/CustomNode.vue' import { Graph } from '@antv/x6' import { register } from '@antv/x6-vue-shape' register({ shape: 'custom-vue-node', width: 100, height: 100, component: CustomNode, }) export default Vue.extend({ name: 'App', mounted() { const graph = new Graph({ container: document.getElementById('app'), width: 1000, height: 1000, }) graph.addNode({ shape: 'custom-vue-node', x: 100, y: 100, }) }, }) </script> ``` 节点组件写法和上面的一致。 > **注意** 在 Vue 2 中,节点组件内容存在一些限制,例如无法使用 Vuex、i18n、Element UI 等。 #### 框选 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/plugins/selection/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/plugins/selection.md - Description: 框选 --- title: "框选" description: "框选" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/plugins/selection/" version: "3.x" --- > **在本章节中主要介绍框选插件相关的知识,通过阅读,你可以了解到** - 如何启用选择与框选 - 如何配置多选与严格框选 - 如何设置修饰键与触发事件(eventTypes) ## 使用 你可以通过插件 `Selection` 启用选择与框选功能,示例: ```ts import { Graph, Selection } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use( new Selection({ enabled: true, }), ) ``` ## 演示 - 点击选中节点。 - 启用多选,按住 Ctrl/Command 后点击节点多选。 - 启用移动,拖动选框移动节点。 - 启用框选,在画布空白位置按下鼠标左键,拖动选框来框选节点。 - 启用严格框选(strict)模式,观察其对框选的影响。 - 选择与框选配合使用的修饰键,如 `alt`;按住 `alt` 键,在画布空白处按下鼠标左键并拖动选框来框选节点。 - 应用自定义过滤器(排除 circle 节点),圆形节点不能被选中。 - 应用自定义附加内容(显示选中节点个数),选择两个及以上的节点,触发显示自定义内容。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Selection } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.graph.use( new Selection({ enabled: true, multiple: true, rubberband: true, movable: true, showNodeSelectionBox: true, }), ) this.graph.addNode({ x: 320, y: 100, width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const source = this.graph.addNode({ x: 80, y: 50, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 240, y: 200, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } onSettingChanged = (options: State) => { this.graph.toggleMultipleSelection(options.multiple) this.graph.toggleSelectionMovable(options.movable) this.graph.toggleRubberband(options.rubberband) this.graph.toggleStrictRubberband(options.strict) this.graph.setSelectionFilter(options.filter) this.graph.setRubberbandModifiers(options.modifiers as any) this.graph.setSelectionDisplayContent( options.content ? (selection) => { return `${selection.length} node${ selection.length > 1 ? 's' : '' } selected.` } : null, ) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="selection-app"> <div className="app-side"> <Settings onChange={this.onSettingChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 配置 | 属性名 | 类型 | 默认值 | 必选 | 描述 | | -------------------------- | -------------------- | ------------------------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | enabled | boolean | `true` | | 是否启用 Selection 插件 | | className | string | - | | 附加样式名,用于定制样式 | | multiple | boolean | `true` | | 是否启用点击多选,启用后按住 `ctrl` 或 `command` 键点击节点实现多选 | | multipleSelectionModifiers | ModifierKey | `['ctrl', 'meta']` | | 用于设置上面点击多选配套的修饰键 | | rubberband | boolean | `false` | | 是否启用框选节点功能 | | rubberNode | boolean | `true` | | 框选时是否将节点纳入框选范围计算 | | rubberEdge | boolean | `false` | | 框选时是否将边纳入框选范围计算 | | modifiers | ModifierKey | - | | 用于设置上面框选配套的修饰键 | | strict | boolean | `false` | | 选框是否需要完全包围节点时才选中节点 | | movable | boolean | `true` | | 拖动选框时框选的节点是否一起移动 | | content | string | - | | 设置附加显示的内容 | | filter | Filter | - | | 节点过滤器 | | showNodeSelectionBox | boolean | `false` | | 是否显示节点的选择框 | | showEdgeSelectionBox | boolean | `false` | | 是否显示边的选择框 | | pointerEvents | `'none' \| 'auto'` | `auto` | | 如果打开 `showNodeSelectionBox` 时,会在节点上方盖一层元素,导致节点的事件无法响应,此时可以配置 `pointerEvents: none` 来解决,默认值是 `auto` | | eventTypes | SelectionEventType[] | `['leftMouseDown', 'mouseWheelDown']` | | 用于设置框选的触发事件类型 | | movingRouterFallback | string | - | | 批量拖拽框选节点过程中,支持临时将与选中节点相连边的路由降级为指定路由,拖拽停止并空闲后自动恢复为原始路由(推荐在拖拽大量节点且边的原始路由为 `manhattan` 时,将 `movingRouterFallback` 设置为 `orth`,能有效提升拖拽流畅性) | `Filter` 的类型定义如下: ```ts type Filter = string[] | { id: string }[] | (this: Graph, cell: Cell) => boolean ``` - `string[]`: 节点 shape 数组,指定的节点/边 shape 才能被选中 - `({ id: string })[]`: 节点 ID 数组,指定的节点/边才能被选中 - `(this: Graph, cell: Cell) => boolean`: 返回 true 的节点/边才能被选中 `ModifierKey` 的类型定义如下: ```ts type ModifierKey = string | ('alt' | 'ctrl' | 'meta' | 'shift' | 'space')[] | null ``` X6 中修饰键包括 `alt`、`ctrl`、`meta`、`shift`、`space` 五个,设置修饰键后点击鼠标并按下修饰键即可触发相应的行为。如果框选和画布拖拽平移的触发条件完全相同时,即相同事件类型(eventTypes)和修饰键(modifiers),框选的优先级更高(禁用默认的画布拖拽平移);如果触发条件不同,则互不影响。因此在同时开始框选和拖拽画布时,修饰键就非常有用,比如框选和拖拽画布的触发时机都是鼠标左键在画布空白位置按下(leftMouseDown),这时可以为框选和拖拽画布设置不一样的修饰键,达到同时开启又不冲突的效果。支持配置单个(如 `alt`)或多个(如 `['alt', 'ctrl']`)修饰键,通过数组形式配置的多个修饰键是或关系,比如刚刚配置的修饰键表示按下 `alt` 或 `ctrl`,如果需要更加灵活的配置,可以使用如下这些形式: - `alt` 表示按下 `alt`。 - `[alt, ctrl]`, 表示按下 `alt` 或 `ctrl`。 - `alt|ctrl` 表示按下 `alt` 或 `ctrl`。 - `alt&ctrl` 表示同时按下 `alt` 和 `ctrl`。 - `alt|ctrl&shift` 表示同时按下 `alt` 和 `shift` 或者同时按下 `ctrl` 和 `shift`。 `SelectionEventType` 的类型定义如下: ```ts type SelectionEventType = 'leftMouseDown' | 'mouseWheelDown'; ``` 触发框选的交互方式。支持2种形式或者他们之间的组合: - `leftMouseDown`: 按下鼠标左键移动进行拖拽 - `mouseWheelDown`: 按下鼠标滚轮进行拖拽 ## API ### graph.select(...) ```ts select(cells: Cell | string | (Cell | string)[]): this ``` 选中指定的节点/边。需要注意的是,该方法不会取消选中当前选中的节点/边,而是将指定的节点/边追加到选区中。如果同时需要取消选中当前选中的节点/边,请使用 [resetSelection(...)](#graphresetselection) 方法。 ### graph.unselect(...) ```ts unselect(cells: Cell | string | (Cell | string)[]): this ``` 取消选中指定的节点/边。 ### graph.isSelected(...) ```ts isSelected(cell: Cell | string): boolean ``` 返回指定的节点/边是否被选中。 ### graph.resetSelection(...) ```ts resetSelection(cells?: Cell | string | (Cell | string)[]): this ``` 先清空选区,然后选中提供的节点/边。 ### graph.getSelectedCells() ```ts getSelectedCells(): Cell[] ``` 获取选中的节点/边。 ### graph.cleanSelection() ```ts cleanSelection(): this ``` 清空选区。 ### graph.isSelectionEmpty() ```ts isSelectionEmpty(): boolean ``` 返回选区是否为空。 ### graph.isSelectionEnabled() ```ts isSelectionEnabled(): boolean ``` 返回是否启用选择能力。 ### graph.enableSelection() ```ts enableSelection(): this ``` 启用选择能力。 ### graph.disableSelection() ```ts disableSelection(): this ``` 禁用选择能力。 ### graph.toggleSelection(...) ```ts toggleSelection(enabled?: boolean): this ``` 切换选择的启用状态。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |---------|---------|:----:|--------|------------------------------------------| | enabled | boolean | | - | 是否启用选择能力,缺省时切换选择的启用状态。 | ### graph.isMultipleSelection() ```ts isMultipleSelection(): boolean ``` 返回是否启用了多选。 ### graph.enableMultipleSelection() ```ts enableMultipleSelection(): this ``` 启用多选。 ### graph.disableMultipleSelection() ```ts disableMultipleSelection(): this ``` 禁用多选。 ### graph.toggleMultipleSelection(...) ```ts toggleMultipleSelection(multiple?: boolean): this ``` 切换多选的启用状态。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |----------|---------|:----:|--------|--------------------------------------| | multiple | boolean | | - | 是否启用多选,缺省时切换多选的启用状态。 | ### graph.isSelectionMovable() ```ts isSelectionMovable(): boolean ``` 返回选中的节点/边是否可以被移动。 ### graph.enableSelectionMovable() ```ts enableSelectionMovable(): this ``` 启用选中的节点/边的移动。 ### graph.disableSelectionMovable() ```ts disableSelectionMovable(): this ``` 禁用选中节点/边的移动。 ### graph.toggleSelectionMovable(...) ```ts toggleSelectionMovable(enabled?: boolean): this ``` 切换选中节点/边是否可以被移动。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |---------|---------|:----:|--------|-----------------------------------------------| | enabled | boolean | | - | 是否启用选中的节点/边的移动,缺省时切换启用状态。 | ### graph.isRubberbandEnabled() ```ts isRubberbandEnabled(): boolean ``` 返回是否启用了框选。 ### graph.enableRubberband() ```ts enableRubberband(): this ``` 启用框选。 ### graph.disableRubberband() ```ts disableRubberband(): this ``` 禁用框选。 ### graph.toggleRubberband(...) ```ts toggleRubberband(enabled?: boolean): this ``` 切换框选的启用状态。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |---------|---------|:----:|--------|--------------------------------| | enabled | boolean | | - | 是否启用框选,缺省时切换启用状态。 | ### graph.isStrictRubberband() ```ts isStrictRubberband(): boolean ``` 返回是否启用了严格框选。启用严格框选后,只有节点/边被选框完全包围时才会选中节点/边。 ### graph.enableStrictRubberband() ```ts enableStrictRubberband(): this ``` 启用严格框选。启用严格框选后,只有节点/边被选框完全包围时才会选中节点/边。 ### graph.disableStrictRubberband() ```ts disableStrictRubberband(): this ``` 禁用严格框选。禁用严格框选后,只需要选框与节点/边的包围盒相交即可选中节点/边。 ### graph.toggleStrictRubberband(...) ```ts toggleStrictRubberband(enabled?: boolean): this ``` 切换严格框选的启用状态。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |---------|---------|:----:|--------|------------------------------------| | enabled | boolean | | - | 是否启用严格框选,缺省时切换启用状态。 | ### graph.setSelectionFilter(...) ```ts setSelectionFilter( filter?: | null | (string | { id: string })[] | ((this: Graph, cell: Cell) => boolean) ): this ``` 设置选择的过滤条件,满足过滤条件的节点/边才能被选中。 开启 `filter` 后,blank 上 rubberband 框选过程中 `box:mousemove` 和 `box:mouseup` 事件返回的 `nodes`、`edges` 也会应用相同的过滤规则,因此其预览结果会和最终 selection 保持一致。 ### graph.setRubberbandModifiers(...) ```ts setRubberbandModifiers(modifiers?: string | ModifierKey[] | null): this ``` 设置框选的修饰键,只有同时按下修饰键时才能触发框选。 ### graph.setSelectionDisplayContent(...) ```ts setSelectionDisplayContent( content?: | null | false | string | ((this: Graph, selection: Selection, contentElement: HTMLElement) => string) ): this ``` 设置选中节点/边的附加显示内容。 ## 事件 | 事件名称 | 参数类型 | 描述 | |---------------------|---------------------------------------------------------------------------------|---------------------------------| | `cell:selected` | `{ cell: Cell; options: Model.SetOptions }` | 节点/边被选中时触发 | | `node:selected` | `{ node: Node; options: Model.SetOptions }` | 节点被选中时触发 | | `edge:selected` | `{ edge: Edge; options: Model.SetOptions }` | 边被选中时触发 | | `cell:unselected` | `{ cell: Cell; options: Model.SetOptions }` | 节点/边被取消选中时触发 | | `node:unselected` | `{ node: Node; options: Model.SetOptions }` | 节点被取消选中时触发 | | `edge:unselected` | `{ edge: Edge; options: Model.SetOptions }` | 边被取消选中时触发 | | `box:mousedown` | `{ e: Dom.MouseDownEvent; view: CellView \| null; cell: Cell \| null; x: number; y: number; nodes: Node[]; edges: Edge[] }` | 按下 selection box,或在画布空白处开始 rubberband 框选时触发 | | `box:mousemove` | `{ e: Dom.MouseMoveEvent; view: CellView \| null; cell: Cell \| null; x: number; y: number; nodes: Node[]; edges: Edge[] }` | 拖动 selection box,或 rubberband 框选过程中实时触发 | | `box:mouseup` | `{ e: Dom.MouseUpEvent; view: CellView \| null; cell: Cell \| null; x: number; y: number; nodes: Node[]; edges: Edge[] }` | 结束拖动 selection box,或结束 rubberband 框选时触发 | | `selection:changed` | `{added: Cell[]; removed: Cell[]; selected: Cell[]; options: Model.SetOptions}` | 选中的节点/边发生改变(增删)时触发 | `box:*` 事件中的参数说明: - `x`、`y`:当前鼠标所在的图坐标。 - `nodes`、`edges`:当前 box 对应的节点和边集合。 - `view`、`cell`:当前关联的视图和单元,在 blank 上进行 rubberband 框选时可能为 `null`。 `nodes` 和 `edges` 在不同交互阶段表示的含义如下: - 在画布空白处开始或拖动 rubberband 框选时,`nodes` 和 `edges` 表示当前选框实时命中的节点和边; `strict` 和 `filter` 配置会影响该结果。 - 在拖动已有 selection box 时,`nodes` 和 `edges` 表示当前 selection 中的节点和边。 ```ts graph.on('node:selected', ({ node }) => { console.log(node) }) graph.on('box:mousemove', ({ nodes, edges, cell }) => { console.log(nodes, edges, cell) }) // 我们也可以在插件实例上监听事件 selection.on('node:selected', ({ node }) => { console.log(node) }) ``` ### docs/demos/tutorial/plugins/selection/index.less ```less .selection-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .x6-widget-selection-box { border: 2px dashed #239edd; } .x6-widget-selection-inner { border: 1px solid #239edd; } } ``` ### docs/demos/tutorial/plugins/selection/settings.tsx ```tsx import React from 'react' import { Checkbox, Switch, Card, Row, Col } from 'antd' import type { CheckboxChangeEvent } from 'antd/es/checkbox' export interface Props { onChange: (res: State) => void } export interface State { multiple: boolean rubberband: boolean strict: boolean movable: boolean modifiers: string[] filter?: string[] content: boolean } export class Settings extends React.Component<Props, State> { state: State = { multiple: true, rubberband: true, movable: true, strict: false, modifiers: [], content: false, } notifyChange() { this.props.onChange({ ...this.state, }) } onMultipleChanged = (multiple: boolean) => { this.setState({ multiple }, () => { this.notifyChange() }) } onMovableChanged = (movable: boolean) => { this.setState({ movable }, () => { this.notifyChange() }) } onRubberbandChanged = (rubberband: boolean) => { this.setState({ rubberband }, () => { this.notifyChange() }) } onStrictChanged = (strict: boolean) => { this.setState({ strict }, () => { this.notifyChange() }) } onModifiersChange = (modifiers: string[]) => { this.setState({ modifiers }, () => { this.notifyChange() }) } onFilterChanged = (e: CheckboxChangeEvent) => { this.setState( { filter: e.target.checked ? ['circle'] : undefined, }, () => { this.notifyChange() }, ) } onContentChanged = (e: CheckboxChangeEvent) => { this.setState({ content: e.target.checked }, () => { this.notifyChange() }) } render() { return ( <Card title="Selection Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={14}>Enable Multiple Select</Col> <Col span={10}> <Switch checked={this.state.multiple} onChange={this.onMultipleChanged} /> </Col> </Row> <Row align="middle"> <Col span={14}>Selection Movable</Col> <Col span={10}> <Switch checked={this.state.movable} onChange={this.onMovableChanged} /> </Col> </Row> <Row align="middle" style={{ margin: '0 -12px', }} > <Col span={24} style={{ borderBottom: '1px solid #e9e9e9' }} /> </Row> <Row align="middle"> <Col span={14}>Enable Rubberband</Col> <Col span={10}> <Switch checked={this.state.rubberband} onChange={this.onRubberbandChanged} /> </Col> </Row> <Row align="middle"> <Col span={14}>Is Strict Contains</Col> <Col span={10}> <Switch checked={this.state.strict} onChange={this.onStrictChanged} /> </Col> </Row> <Row align="middle"> <Col span={8}>Modifier Key</Col> <Col span={16}> <Checkbox.Group options={['alt', 'ctrl', 'shift']} value={this.state.modifiers} onChange={this.onModifiersChange} /> </Col> </Row> <Row align="middle" style={{ margin: '0 -12px', }} > <Col span={24} style={{ borderBottom: '1px solid #e9e9e9' }} /> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.filter != null} onChange={this.onFilterChanged} > Add filter (only circle) </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.content} onChange={this.onContentChanged} > Add content(display selected count) </Checkbox> </Col> </Row> </Card> ) } } ``` #### 升级到 3.x 版本 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/update/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/update.md - Description: 升级到 3.x 版本 --- title: "升级到 3.x 版本" description: "升级到 3.x 版本" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/update/" version: "3.x" --- 相比 2.x 版本,3.x 在对外使用上的变化主要集中在插件及子包合并(统一导出),交互默认值优化,并新增动画能力,整体升级成本较低。 ## 变更总览 - 插件及子包合并:`@antv/x6-plugin-xxxx`、`@antv/x6-common`、`@antv/x6-geometry` 全部整合到 `@antv/x6` 主包并统一导出。 - 交互默认值调整:画布 `panning` 默认开启。 - 新增动画能力:支持指令式动画、配置式动画和自定义动画 Shape 实现丰富酷炫的效果。 - 滚动画布(`Scroller`)下,支持虚拟渲染能力 `virtual: true`。 ## 升级参考 ### package.json 3.x 将所有常用插件(Plugins)统一并入 `@antv/x6` 主包,不再需要单独安装 `@antv/x6-plugin-xxxx`。同时,2.x 中的公共包 `@antv/x6-common` 与几何运算包 `@antv/x6-geometry` 也已并入主包,其 API 现在统一从 `@antv/x6` 导入。 ```json { "@antv/x6": "^3.0.0" } ``` 请移除如下依赖(如存在): - `@antv/x6-plugin-selection` - `@antv/x6-plugin-transform` - `@antv/x6-plugin-scroller` - `@antv/x6-plugin-keyboard` - `@antv/x6-plugin-history` - `@antv/x6-plugin-clipboard` - `@antv/x6-plugin-snapline` - `@antv/x6-plugin-dnd` - `@antv/x6-plugin-minimap` - `@antv/x6-plugin-stencil` - `@antv/x6-plugin-export` - `@antv/x6-common` - `@antv/x6-geometry` ### 导入路径变更 3.x 版本,以上插件全部从 `@antv/x6` 导出,使用方式保持不变,仅需替换导入路径: ```ts // 2.x import { Scroller } from '@antv/x6-plugin-scroller' import { Selection } from '@antv/x6-plugin-selection' graph.use(new Scroller()) graph.use(new Selection()) // 3.x import { Scroller, Selection } from '@antv/x6' graph.use(new Scroller()) graph.use(new Selection()) ``` 在 3.x 版本中,之前从 `@antv/x6-common` 和 `@antv/x6-geometry` 导入的工具与几何类型,现在统一从 `@antv/x6` 导出,仅需替换导入路径即可: ```ts // 2.x import { Dom, FunctionExt } from '@antv/x6-common' import { Point, Rectangle } from '@antv/x6-geometry' // 3.x import { Dom, FunctionExt, Point, Rectangle } from '@antv/x6' ``` ### 新增动画能力 3.x 新增 `animate` 动画,移除 2.x 中的 `transition` 使用方式,更加方便地为节点和边添加动效。 更多用法可参考:[动画说明文档](/zh/tutorial/basic/animation)。 ### 配置修改说明 - 画布平移 `panning` 默认开启:`enabled: true`;若需禁用,设置 `panning: false` 即可。 - 当画布 `panning` 与 `Selection` 框选触发存在冲突时,框选优先。 - 使用 `Scroller` 插件时,默认禁用画布 `panning`,且支持开启虚拟渲染 `virtual: true`。 ### Shape 组件升级(React/Vue/Angular) X6 3.x 须使用以下 3.x 版本的 shape 包:`@antv/x6-react-shape@^3.x`、`@antv/x6-vue-shape@^3.x`、`@antv/x6-angular-shape@^3.x`。 ```json { "@antv/x6-react-shape": "^3.0.0", "@antv/x6-vue-shape": "^3.0.0", "@antv/x6-angular-shape": "^3.0.0" } ``` #### React Provider 使用变更 将 2.x 的 `Portal.getProvider()` 改为 3.x 的 `getProvider()`: ```ts // 2.x import { Portal } from '@antv/x6-react-shape' const Provider = Portal.getProvider() // 3.x import { getProvider } from '@antv/x6-react-shape' const Provider = getProvider() ``` #### 数据 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/basic/serialization/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/basic/serialization.md - Description: 数据 --- title: "数据" description: "数据" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/basic/serialization/" version: "3.x" --- > **在本章节中主要介绍画布数据序列化相关的知识,通过阅读,你可以了解到** - 如何导出数据 - 如何导入数据 ## 导出 我们可以调用 `graph.toJSON()` 方法来导出图中的节点和边,返回一个形如 `{ cells: [] }` 的对象,其中 `cells` 数组**按渲染顺序**保存节点和边。 其中,导出的节点结构如下: ```ts { id: string, shape: string, position: { x: number y: number }, size: { width: number height: number }, attrs: object, zIndex: number, } ``` 边的结构如下: ```ts { id: string, shape: string, source: object, target: object, attrs: object, zIndex: number, } ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import hljs from 'highlight.js/lib/core' import json from 'highlight.js/lib/languages/json' hljs.registerLanguage('json', json) export default class Example extends React.Component { private container: HTMLDivElement private code: HTMLElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const source = graph.addNode({ x: 40, y: 40, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode({ x: 240, y: 180, width: 100, height: 40, shape: 'ellipse', label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, label: 'X6', attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) const parse = () => { this.code.innerText = JSON.stringify(graph.toJSON(), null, 2) hljs.highlightBlock(this.code) } parse() graph.on('cell:change:*', parse) } refContainer = (container: HTMLDivElement) => { this.container = container } refCode = (code: HTMLElement) => { this.code = code } render() { return ( <div className="to-json-app"> <div className="app-side"> <pre> <code className="language-json" ref={this.refCode} /> </pre> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 导入 支持传入节点/边元数据数组 `graph.fromJSON(cells: (Node.Metadata | Edge.Metadata)[])`。 ```ts 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]` 顺序渲染。 ```ts graph.fromJSON({ nodes: [], edges: [], }) ``` 通常,我们通过 `graph.fromJSON(...)` 来渲染 `graph.toJSON()` 导出的数据。 > **提示** 当数据中没有提供 `zIndex` 时,则按照节点/边在数组中的顺序渲染,也就是说越靠前的节点/边,其 `zIndex` 越小,在画布中的层级就越低。 ### docs/demos/tutorial/basic/serialization/to-json/index.less ```less .to-json-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { width: 320px; height: 280px; overflow: auto; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### 开发者工具 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/devtool/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/devtool.md - Description: 开发者工具 --- title: "开发者工具" description: "开发者工具" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/devtool/" version: "3.x" --- 我们提供了一个对页面元素进行审查的插件来帮助开发者更方便的开发应用。 ### 安装 ![image](https://user-images.githubusercontent.com/1826685/238003455-d341f598-1b35-4d8c-bb7c-0320cad6a4cb.png) ### 使用 首先在项目代码中初始化: ```javascript // init window hook window.__x6_instances__ = [] const graph = new Graph({ ...blablabla }) window.__x6_instances__.push(graph) ``` 然后就可以在开发者面板看到 AntV X6 这一栏。 ![image](https://user-images.githubusercontent.com/1826685/238013980-2d6018f8-7d85-473c-a043-98b1f03b6674.png) 我们在这里可以审查图对象以及图内元素: ![image](https://user-images.githubusercontent.com/1826685/238014156-e65ec2b0-f719-410e-9a10-89cdb836acde.png) 同时还支持修改元素属性: ![image](https://user-images.githubusercontent.com/1826685/238014353-124feb8e-2049-499d-a13d-3d26f485bab6.png) #### Angular 节点 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/intermediate/angular/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/intermediate/angular.md - Description: Angular 节点 --- title: "Angular 节点" description: "Angular 节点" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/intermediate/angular/" version: "3.x" --- > **在本章节中主要介绍 Angular 节点相关的知识,通过阅读,你可以了解到** - 如何使用 Angular 来渲染节点内容 - 如何更新节点内容 - FAQ ## 渲染节点 我们提供一个独立的包 `@antv/x6-angular-shape`,用于将 Angular 组件/模板渲染为节点。 > **注意** 版本兼容关系:X6 3.x 须使用 x6-angular-shape 3.x 版本。 ### Component 渲染 ```ts @Component({ selector: 'app-node', templateUrl: './node.component.html', styleUrls: ['./node.component.scss'], }) export class NodeComponent implements AfterViewInit, OnChanges { @Input() value: string } ``` ```ts import { register } from '@antv/x6-angular-shape' @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) export class AppComponent implements AfterViewInit { ngAfterViewInit(): void { register({ shape: 'custom-angular-component-node', width: 120, height: 20, content: NodeComponent, injector: this.injector, }) this.graph.addNode({ shape: 'custom-angular-component-node', x: 100, y: 100, data: { // Input 的参数必须放在这里 ngArguments: { value: '糟糕糟糕 Oh my god', }, }, }) } } ``` ### TemplateRef 渲染 ```html <ng-template #template let-data="ngArguments"> <section class="template-container"> <span class="value">{{ data.value }}</span> </section> </ng-template> ``` ```ts import { register } from '@antv/x6-angular-shape' @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) export class AppComponent implements AfterViewInit { @ViewChild('template') template: TemplateRef<{}> ngAfterViewInit(): void { register({ shape: 'custom-angular-template-node', width: 120, height: 20, content: this.template, injector: this.injector, }) this.graph.addNode({ shape: 'custom-angular-template-node', x: 100, y: 100, data: { ngArguments: { value: '魔法怎么失灵啦', }, }, }) } } ``` ## 更新节点 无论使用 Component 还是 TemplateRef,更新方式相同。 ```ts node.setData({ ngArguments: { value: '晚风中闪过 几帧从前啊', }, }) ``` ## 有 demo 吗? 有的。由于 X6 的节点渲染与框架解耦,因此 `x6-angular-shape` 包并非直接在源代码中修改,而是在独立的 Angular 环境中开发。该 demo 还提供多种节点类型的性能测试,详情请参考 [Eve-Sama/x6-angular-shape](https://github.com/Eve-Sama/x6-angular-shape) 和 [X6 与 G6 的性能对比,以及 X6 多节点类型下的 FPS 临界点讨论](https://github.com/antvis/X6/issues/3266)。 ## FAQ ### 为什么输入属性不能直接放在 data 中而需要放在 ngArguments 中?且为什么不叫 ngInput? 因为并非所有 `node.data` 中的属性都是输入属性,所以遍历 `data` 中的所有属性进行赋值并不合适。至于为什么叫 `ngArguments`,主要有两点考虑: - 1.x 版本中已采用该命名,沿用可降低用户升级成本。 - `Input` 的概念来自 `Component`,而在 `TemplateRef` 中是 `context`,在二者基础上抽象出更通用的 `Arguments` 概念。 ### 2.x 版本的 x6-angular-shape 相比较 1.x 版本有什么新特性吗? 实现思路与之前基本一致,但有几点值得一提。 #### Demo 更聚焦 1.x 版本的 demo 除了渲染组件外,还包含连线、清除等一系列案例。看似扩展,实则冗杂。作为 `x6-angular-shape` 的 demo,2.x 版本更聚焦于 shape 的使用与性能测试等。与插件无关的内容请查看 X6 官网。 #### 功能更稳定 在 1.x 版本中,虽然实现了功能,但对使用场景的考虑不够全面。比如 `ngArguments` 的变化在 `TemplateRef` 场景不生效;对 `Component` 生效但无法触发 `ngOnChanges`。在新版本中,这些问题已被修复。 ### 版本要求 Angular 版本需至少 14 及以上。14 以下需通过 hack 的方式实现部分特性,较为麻烦,暂不提供支持。如有需要可提 Issue,我们将说明实现方式。 #### 滚动画布 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/plugins/scroller/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/plugins/scroller.md - Description: 滚动画布 --- title: "滚动画布" description: "滚动画布" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/plugins/scroller/" version: "3.x" --- > **在本章节中主要介绍滚动画布插件相关的知识,通过阅读,你可以了解到** - 如何使画布具备滚动能力 > **注意** 使用 `Scroller` 插件时会默认禁用画布的 `panning` 拖拽平移能力以避免冲突。 ## 使用 你可以通过插件 `Scroller` 启用画布滚动能力,示例: ```ts import { Graph, Scroller } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', } }) graph.use( new Scroller({ enabled: true, }), ) ``` ## 演示 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Button } from 'antd' import { Graph, Scroller } from '@antv/x6' import { Settings, State } from './settings.tsx' const data = { hello: { id: 'hello', x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, world: { id: 'world', shape: 'circle', x: 160, y: 180, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }, rect: { id: 'rect', x: -60, y: 100, width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, } export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.renderGraph() } renderGraph(options: State | null = null) { if (this.graph) { this.graph.dispose() } const graph = new Graph({ container: this.container, panning: false, background: { color: '#F2F7FA', }, }) graph.use( new Scroller({ enabled: true, pageVisible: true, pageBreak: true, pannable: true, ...options, }), ) graph.fromJSON({ nodes: Object.keys(data).map((key) => data[key]) }) graph.on('node:change:position', ({ node, current }) => { const item = data[node.id] item.x = current!.x item.y = current!.y }) graph.centerContent() this.graph = graph } onSettingsChanged = (options: State) => { this.renderGraph(options) } onCenter = () => { this.graph.center() } onCenterContent = () => { this.graph.centerContent() } onCenterCircle = () => { const circle = this.graph.getCellById('world') if (circle) { this.graph.centerCell(circle) } } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="scroller-app"> <div className="app-side"> <Settings onChange={this.onSettingsChanged} /> </div> <div className="app-main"> <div className="app-btns"> <Button onClick={this.onCenter}>Center Graph</Button> <Button onClick={this.onCenterContent}>Center Whole Content</Button> <Button onClick={this.onCenterCircle}>Center The Circle</Button> </div> <div className="app-content" ref={this.refContainer} /> </div> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 选项 | 属性名 | 类型 | 默认值 | 必选 | 描述 | |------------------|---------------------|---------|------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | pannable | boolean | `false` | | 是否启用画布平移能力(在空白位置按下鼠标后拖动平移画布) | | className | string | - | | 附加样式名,用于定制样式 | | width | number | - | | `Scroller` 的宽度,默认为画布容器宽度 | | height | number | - | | `Scroller` 的高度,默认为画布容器高度 | | modifiers | ModifierKey | - | | 设置修饰键后需要点击鼠标并按下修饰键才能触发画布拖拽 | | pageWidth | number | - | | 每一页的宽度,默认为画布容器宽度 | | pageHeight | number | - | | 每一页的高度,默认为画布容器高度 | | pageVisible | boolean | `false` | | 是否分页 | | pageBreak | boolean | `false` | | 是否显示分页符 | | autoResize | boolean | `true` | | 是否自动扩充/缩小画布。开启后,移动节点/边时将自动计算需要的画布大小,当超出当前画布大小时,按照 `pageWidth` 和 `pageHeight` 自动扩充画布。反之,则自动缩小画布 | | minVisibleWidth | number | - | | 当 `padding` 为空时有效,设置画布滚动时画布的最小可见宽度 | | minVisibleHeight | number | - | | 当 `padding` 为空时有效,设置画布滚动时画布的最小可见高度 | | padding | `number \| Padding` | - | | 设置画布四周的 padding 边距。默认根据 `minVisibleWidth` 和 `minVisibleHeight` 自动计算得到,保证画布滚动时,在宽度和高度方向至少有 `minVisibleWidth` 和 `minVisibleHeight` 大小的画布可见 | 上面的 `Padding` 类型定义如下: ```ts type Padding = { top: number; right: number; bottom: number; left: number } ``` `ModifierKey` 的类型定义如下: ```ts type ModifierKey = string | ('alt' | 'ctrl' | 'meta' | 'shift' | 'space')[] | null ``` 支持以下几种形式: - `alt` 表示按下 `alt`。 - `[alt, ctrl]` 表示按下 `alt` 或 `ctrl`。 - `alt|ctrl` 表示按下 `alt` 或 `ctrl`。 - `alt&ctrl` 表示同时按下 `alt` 和 `ctrl`。 - `alt|ctrl&shift` 表示同时按下 `alt` 和 `shift` 或者同时按下 `ctrl` 和 `shift`。 ## API ### graph.lockScroller() 禁止滚动。 ### graph.unlockScroller() 启用滚动。 ### graph.getScrollbarPosition() 获取滚动条位置。 ### graph.setScrollbarPosition(left?: number, top?: number) 设置滚动条位置。 - `left?: number` 水平滚动条的位置,缺省时表示水平方向不滚动。 - `top?: number` 垂直滚动条的位置,缺省时表示垂直方向不滚动。 例如: ```ts graph.setScrollbarPosition(100) graph.setScrollbarPosition(100, null) graph.setScrollbarPosition(null, 200) graph.setScrollbarPosition(100, 200) ``` ### docs/demos/tutorial/plugins/scroller/index.less ```less .scroller-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } .app-main { display: flex; flex-direction: column; width: 100%; margin-right: 8px; margin-left: 8px; .app-btns { display: flex; padding-top: 4px; padding-bottom: 12px; } .app-btns .ant-btn { margin-right: 16px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; } } .x6-graph-scroller { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } } ``` ### docs/demos/tutorial/plugins/scroller/settings.tsx ```tsx import React from 'react' import { Checkbox, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { pannable: boolean pageVisible: boolean pageBreak: boolean autoResize: boolean minVisibleWidth: number minVisibleHeight: number } export class Settings extends React.Component<Props, State> { state: State = { pannable: true, pageVisible: true, pageBreak: true, autoResize: true, minVisibleWidth: 50, minVisibleHeight: 50, } notifyChange() { this.props.onChange({ ...this.state, }) } onPanningChanged = (e: any) => { this.setState({ pannable: e.target.checked }, () => { this.notifyChange() }) } onAutoResizeChanged = (e: any) => { this.setState({ autoResize: e.target.checked }, () => { this.notifyChange() }) } onPageVisibleChanged = (e: any) => { this.setState({ pageVisible: e.target.checked }, () => { this.notifyChange() }) } onPageBreakChanged = (e: any) => { this.setState({ pageBreak: e.target.checked }, () => { this.notifyChange() }) } onMinVisibleWidthChanged = (minVisibleWidth: number) => { this.setState({ minVisibleWidth }, () => { this.notifyChange() }) } onMinVisibleHeightChanged = (minVisibleHeight: number) => { this.setState({ minVisibleHeight }, () => { this.notifyChange() }) } render() { return ( <Card title="Scroller Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.pannable} onChange={this.onPanningChanged} > Enable Panning </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.autoResize} onChange={this.onAutoResizeChanged} > Enable Auto Resize </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.pageVisible} onChange={this.onPageVisibleChanged} > Show Page </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.pageBreak} onChange={this.onPageBreakChanged} > Show Page Break </Checkbox> </Col> </Row> <Row align="middle" style={{ borderTop: '1px solid #f0f0f0', paddingTop: 16, marginBottom: 0, }} > <Col span={10} offset={1}> Min Visible Width </Col> <Col span={2} offset={1}> <div className="slider-value">{this.state.minVisibleWidth}</div> </Col> </Row> <Row align="middle" style={{ marginTop: 0 }}> <Col span={22} offset={1}> <Slider min={10} max={200} step={1} value={this.state.minVisibleWidth} onChange={this.onMinVisibleWidthChanged} /> </Col> </Row> <Row align="middle" style={{ borderTop: '1px solid #f0f0f0', paddingTop: 16, marginBottom: 0, }} > <Col span={10} offset={1}> Min Visible Height </Col> <Col span={2} offset={1}> <div className="slider-value">{this.state.minVisibleHeight}</div> </Col> </Row> <Row align="middle" style={{ marginTop: 0 }}> <Col span={22} offset={1}> <Slider min={10} max={200} step={1} value={this.state.minVisibleHeight} onChange={this.onMinVisibleHeightChanged} /> </Col> </Row> </Card> ) } } ``` #### 动画 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/basic/animation/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/basic/animation.md - Description: 动画 --- title: "动画" description: "动画" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/basic/animation/" version: "3.x" --- ## 添加动画 在 X6 中,为节点或边添加动画能够显著提升画布的视觉效果和交互体验。支持多种动画实现方式,包括 `animate` API、CSS 动画和 SMIL 动画等。其中,`animate` API 功能最为强大,本文将重点介绍其使用方法。以下是一个基础动画示例: ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) const node = graph.addNode({ shape: 'rect', label: 'hello X6', x: 100, y: 140, width: 100, height: 50, attrs: { body: { strokeWidth: 1, rx: 6, ry: 6, }, }, }) node.animate( // 将节点的位置从原本的位置通过动画移动到 (300, 140) { 'position/x': 300 }, // 动画持续时间为 1000ms,当动画运动到终点后下次将从终点开始运动,重复次数为无穷 { duration: 1000, direction: 'alternate', iterations: Infinity }, ) ``` 对于熟悉 Web Animations API 的开发者而言,可以明显看出 X6 的 `animate` API 是基于 [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) 标准实现的。因此,其使用方式与 Web Animations API 基本兼容,并实现了该标准中的大部分核心功能。 `animate` API 接收两个关键参数: 1. **关键帧配置**:指定需要添加动画效果的属性及其目标值。在本例中,通过对象形式配置,其中: - **key**(position/x):表示需要添加动画的属性路径 - **value**(300):表示该属性的目标值,即将节点从当前位置移动到 x 坐标为 300 的位置 2. **动画配置**:控制动画行为的参数对象,包含以下主要配置项: - `duration` 为 1000 表示动画运行一次的时间为 1000ms - `direction` 为 alternate 表示动画将在起点和终点之间来回运动 - `iterations` 为 Infinity 表示动画将无限次重复 ### 动画属性 部分开发者可能对使用 `position/x` 而非直接使用 `x` 属性存在疑问,这是因为 X6 的动画系统基于 `cell.setPropByPath()` 方法实现,该方法通过属性路径来修改属性值。节点的属性结构通常如下所示(也可通过 `cell.prop()` 方法获取): ```js { // ... position: {x: 370, y: 180}, size: {width: 100, height: 60}, attrs: { // ... } } ``` 所以给位置添加动画是通过 `position/x` 来设置动画效果,子属性使用 `/` 来拼接,其他动画属性同理。例如想要给节点大小添加动画,就可以这样写: ```js // 将节点的宽度设置为 200 并且添加动画 node.animate({'size/width': 200 },1000) ``` 理解了动画支持的属性机制,那么就可以给 X6 节点内置属性外的额外属性也添加动画效果,例如给自定义节点 `data` 内的 `ratio` 自定义属性添加动画效果: ```ts import { Dom, Graph, Shape } from '@antv/x6' Shape.HTML.register({ shape: 'custom-html', width: 160, height: 80, effect: ['data'], html(cell) { // 自定义的 HTML 节点接收一个面积数据来控制 div 大小,可以对该自定义属性添加动画效果 const { ratio } = cell.getData() ?? {} const div = document.createElement('div') const area = 12000 const width = Math.sqrt(area / ratio) const height = width * ratio Dom.css(div, { width, height, background: '#fff', borderRadius: 10, border: "1px solid #000" }) return div }, }) const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) graph.addNode({ shape: 'custom-html', x: 80, y: 80, data: { ratio: 1 }, animation: [ // 对自定义节点的宽高比添加动画效果 [{ 'data/ratio': 3 / 5 }, { duration: 1000, iterations: Infinity }], ], }) ``` ### 配置式动画 除了通过 API 的形式来调用添加动画,`animate` API 还支持通过节点的 `animation` 配置的方式来添加动画,可以省去一个额外的步骤,在很多场景下会很实用,下面的例子将实现一个和上面的例子一样的动画: ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) graph.addNode({ shape: 'rect', label: 'hello X6', x: 100, y: 140, width: 100, height: 50, attrs: { body: { strokeWidth: 1, rx: 6, ry: 6, }, }, // 将动画相关配置直接写到节点的配置中,当节点被添加到画布中时,动画会自动开始 animation: [ [ { 'position/x': 300 }, { duration: 1000, direction: 'alternate', iterations: Infinity }, ], ], }) ``` `animation` 配置是一个数组: - 数组中的每一项都是一个动画参数数组(对应 `animate` 的参数) - 节点被添加到画布中后,动画将会自动根据动画配置来开始自动播放 ### 注册动画Shape 一个常见的场景是对很多节点添加相同的动画,不管是重复调用 `animate` API 还是通过配置的方式来添加动画,都需要对每一个节点进行重复的操作,而在 X6 中本身就支持通过注册 `Shape` 的方式来复用节点,同样动画也支持被注册进 `Shape` 中,来使一批节点都支持动画: ```ts import { Graph } from '@antv/x6' Graph.registerNode( 'animated-rect', { inherit: 'rect', width: 150, height: 60, attrs: { body: { strokeWidth: 1, rx: 6, ry: 6, }, }, // 注册 Shape 时先添加好动画效果 animation: [ [ { 'position/x': 300 }, { duration: 1000, direction: 'alternate', iterations: Infinity }, ], ], }, true, ) const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) graph.addNode({ shape: 'animated-rect', x: 100, y: 50, label: "hello X6" }) graph.addNode({ shape: 'animated-rect', x: 100, y: 150, label: "hello Animation" }) ``` ## 动画控制 X6 中的 `animate` API 基于 Web Animations API 设计,所以同样支持灵活的动画控制能力,例如暂停、恢复、取消等。下面是一个简单的动画控制例子: ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) const node = graph.addNode({ shape: 'rect', label: '点击我将暂停动画', x: 100, y: 140, width: 150, height: 60, attrs: { body: { strokeWidth: 1, rx: 6, ry: 6, }, }, animation: [ [ { 'position/x': 300 }, { duration: 1000, direction: 'alternate', iterations: Infinity }, ], ], }) graph.on('node:click', ({ cell }) => { if (cell === node) { const [animation] = cell.getAnimations() animation.pause() } }) ``` 除了暂停动画的能力外,`animate` API 总共支持以下能力: - `pause()` 暂停动画 - `play()` 播放/恢复动画 - `cancel()` 取消动画 - `finish()` 结束动画 - `reverse()` 反向播放动画 - `updatePlaybackRate()` 更新动画播放速度 ## 动画事件 X6 中的动画事件支持两种风格,一种是基于 Web Animations API 的设计,例如通过设置 `onfinish` 等事件来监听动画的事件: ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) const node = graph.addNode({ shape: 'rect', label: '点击我将开始动画', x: 100, y: 140, width: 150, height: 60, attrs: { body: { strokeWidth: 1, rx: 6, ry: 6, }, }, }) graph.on('node:click', ({ cell }) => { if (cell !== node) return const animation = node.animate( { 'position/x': [100, 300] }, { duration: 1000, direction: 'alternate', iterations: 1 }, ) // 通过 onfinish 监听动画事件 animation.onfinish = () => { console.log('动画结束') } }) ``` 基于 Web Animations API 的设计总共支持如下事件: - `onfinish` 动画结束 - `oncancel` 动画取消 另外一种是基于 X6 自身的设计,通过 `on` API 来监听动画事件: ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) const node = graph.addNode({ shape: 'rect', label: '点击我将开始动画', x: 100, y: 140, width: 150, height: 60, attrs: { body: { strokeWidth: 1, rx: 6, ry: 6, }, }, }) graph.on('node:click', ({ cell }) => { if (cell !== node) return node.animate( { 'position/x': [100, 300] }, { duration: 1000, direction: 'alternate', iterations: 1 }, ) }) // 通过 on 监听动画事件 graph.on('node:animation:finish', () => { console.log('动画结束') }) ``` 基于 X6 自身的设计总共支持如下事件: - `cell:animation:finish` 动画结束 - `cell:animation:cancel` 动画取消 X6 事件系统详细可见[事件](/zh/tutorial/basic/events)。 #### HTML 节点 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/intermediate/html/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/intermediate/html.md - Description: HTML 节点 --- title: "HTML 节点" description: "HTML 节点" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/intermediate/html/" version: "3.x" --- > **在本章节中主要介绍 HTML 节点相关的知识,通过阅读,你可以了解到** - 如何使用 HTML 来渲染节点内容 - 如何更新节点内容 ## 渲染节点 X6 默认内置 `HTML` 渲染能力,使用方法也非常简单: ```ts import { Shape } from '@antv/x6' Shape.HTML.register({ shape: 'custom-html', width: 160, height: 80, html() { const div = document.createElement('div') div.className = 'custom-html' return div }, }) graph.addNode({ shape: 'custom-html', x: 60, y: 100, }) ``` 下面例子中,在 `HTML` 标签上加上悬浮动画效果,使用 `SVG` 实现起来会非常复杂。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Shape } from '@antv/x6' Shape.HTML.register({ shape: 'custom-html', width: 160, height: 80, effect: ['data'], html() { const div = document.createElement('div') div.className = 'custom-html' return div }, }) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addNode({ shape: 'custom-html', x: 60, y: 100, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="html-basic-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 更新节点 在注册节点时,提供 `effect` 字段(当前节点的 `prop` 数组)。当 `effect` 中任一 `prop` 发生变化时,会重新执行 `html` 方法,返回新的 DOM,从而更新节点内容。 ```ts Shape.HTML.register({ shape: 'custom-html', width: 160, height: 80, effect: ['data'], html(cell) { const { color } = cell.getData() const div = document.createElement('div') div.className = 'custom-html' div.style.background = color return div }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Shape, Color } from '@antv/x6' Shape.HTML.register({ shape: 'custom-update-html', width: 160, height: 80, effect: ['data'], html(cell) { const { color } = cell.getData() const div = document.createElement('div') div.className = 'custom-html' div.style.background = color return div }, }) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const node = graph.addNode({ shape: 'custom-update-html', x: 60, y: 100, data: { color: '#333232', }, }) setInterval(() => { node.setData({ color: Color.randomHex(), }) }, 2000) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="html-update-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/tutorial/intermediate/html/basic/index.less ```less .html-basic-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .custom-html { width: 100%; height: 100%; background: #333232; border-radius: 1em; box-shadow: 0 0.125em 0.3125em rgb(0 0 0 / 25%), 0 0.02125em 0.06125em rgb(0 0 0 / 25%); perspective: 600px; &::before { position: absolute; top: 50%; left: 0; width: 100%; height: 50%; background: #333232 linear-gradient(180deg, rgb(0 0 0 / 10%) 50%, rgb(0 0 0 / 40%)); border-radius: 0 0 1em 1em; transform: rotateX(180deg); transform-origin: center top; transition: 0.7s ease-in-out transform; content: ' '; } &:hover::before { transform: rotateX(0); } } } ``` ### docs/demos/tutorial/intermediate/html/update/index.less ```less .html-update-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .custom-html { width: 100%; height: 100%; background: #333232; border-radius: 1em; box-shadow: 0 0.125em 0.3125em rgb(0 0 0 / 25%), 0 0.02125em 0.06125em rgb(0 0 0 / 25%); perspective: 600px; &::before { position: absolute; top: 50%; left: 0; width: 100%; height: 50%; background: #333232 linear-gradient(180deg, rgb(0 0 0 / 10%) 50%, rgb(0 0 0 / 40%)); border-radius: 0 0 1em 1em; transform: rotateX(180deg); transform-origin: center top; transition: 0.7s ease-in-out transform; content: ' '; } &:hover::before { transform: rotateX(0); } } } ``` #### Dnd - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/plugins/dnd/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/plugins/dnd.md - Description: Dnd --- title: "Dnd" description: "Dnd" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/plugins/dnd/" version: "3.x" --- > **在本章节中主要介绍拖拽插件相关的知识,通过阅读,你可以了解到** - 如何通过拖拽交互往画布中添加节点 - 如何自定义拖拽节点与放置节点 ## 使用 我们经常需要通过拖拽交互往画布中添加节点,如流程图编排场景,从流程图组件库中拖拽组件到画布中。你可以通过插件 `Dnd` 启用拖拽交互,示例: ```ts import { Graph, Dnd } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) const dnd = new Dnd({ target: graph, }) ``` 当开始拖拽时,需要调用 `dnd.start(node, e)` 方法,在 `React` 中这样使用: ```tsx export default () => { const startDrag = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => { // 该 node 为拖拽的节点,默认也是放置到画布上的节点,可以自定义任何属性 const node = graph.createNode({ shape: 'rect', width: 100, height: 40, }) dnd.start(node, e.nativeEvent) } return ( <ul> <li onMouseDown={startDrag}></li> </ul> ) } ``` ## 演示 ```ts import './index.less' import { createRoot } from 'react-dom/client' /* eslint-disable jsx-a11y/no-static-element-interactions */ import React from 'react' import { Graph, Dnd, Snapline } from '@antv/x6' export default class Example extends React.Component { private graph: Graph private container: HTMLDivElement private dndContainer: HTMLDivElement private dnd: Dnd componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, mousewheel: { enabled: true, modifiers: ['ctrl', 'meta'], }, }) graph.use( new Snapline({ enabled: true, sharp: true, }), ) const source = graph.addNode({ x: 130, y: 30, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode({ x: 180, y: 160, width: 100, height: 40, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.centerContent() this.dnd = new Dnd({ target: graph, scaled: false, dndContainer: this.dndContainer, }) this.graph = graph } startDrag = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => { const target = e.currentTarget const type = target.getAttribute('data-type') const node = type === 'rect' ? this.graph.createNode({ width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) : this.graph.createNode({ width: 60, height: 60, shape: 'circle', label: 'Circle', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.dnd.start(node, e.nativeEvent as any) } refContainer = (container: HTMLDivElement) => { this.container = container } dndContainerRef = (container: HTMLDivElement) => { this.dndContainer = container } render() { return ( <div className="dnd-app"> <div className="dnd-wrap" ref={this.dndContainerRef}> <div data-type="rect" className="dnd-rect" onMouseDown={this.startDrag} > Rect </div> <div data-type="circle" className="dnd-circle" onMouseDown={this.startDrag} > Circle </div> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 配置 | 选项 | 类型 | 必选 | 默认值 | 说明 | |-------------------|-------------------------------------------------------------------------------------|:----:|-----------------|-----------------------------------------------------------------------------------------------------| | target | Graph | ✓️ | | 目标画布。 | | getDragNode | (sourceNode: Node, options: GetDragNodeOptions) => Node | | | 拖拽开始时,获取被拖拽的节点,默认克隆 `dnd.start` 传入的节点。 | | getDropNode | (draggingNode: Node, options: GetDropNodeOptions) => Node | | | 拖拽结束时,获取放置到目标画布的节点,默认克隆被拖拽的节点。 | | validateNode | (droppingNode: Node, options: ValidateNodeOptions) => boolean \| Promise\<boolean\> | | | 拖拽结束时,验证节点是否可以放置到目标画布中。 | | dndContainer | HTMLElement | | | 如果设置 `dndContainer`,在 `dndContainer` 上放开鼠标不会放置节点,常用于 `dnd` 容器处于画布上面的场景。 | | draggingContainer | HTMLElement | | `document.body` | 自定义拖拽画布容器。 | ## 常见问题 1. 为什么拖拽节点到画布后,ID 发生了改变 根据上面的拖拽细节我们会发现整体拖拽流程是:源节点 -> 拖拽节点 -> 放置节点,默认是将源节点克隆一份变为拖拽节点,拖拽节点克隆一份变为放置节点,在克隆的过程中会重置节点 ID,如果想保持原来节点 ID,可以进行以下操作: ```ts // 这样放置到画布上的节点 ID 和 dnd start 传入的 node ID 一致 const dnd = new Dnd({ getDragNode: (node) => node.clone({ keepId: true }), getDropNode: (node) => node.clone({ keepId: true }), }) ``` 2.怎么自定义拖拽节点的样式? ```ts const dnd = new Dnd({ getDragNode(node) { // 这里返回一个新的节点作为拖拽节点 return graph.createNode({ width: 100, height: 100, shape: 'rect', attrs: { body: { fill: '#ccc', }, }, }) }, }) ``` 3.怎么自定义放置到画布上的节点样式? ```ts const dnd = new Dnd({ getDropNode(node) { const { width, height } = node.size() // 返回一个新的节点作为实际放置到画布上的节点 return node.clone().size(width * 3, height * 3) }, }) ``` 4.怎么获取放置到画布上节点的位置? ```ts graph.on('node:added', ({ node }) => { const { x, y } = node.position() }) ``` 5. 怎么设置放置到画布上节点的 zIndex? ```ts graph.on('node:added', ({ node }) => { node.setZIndex(5) }) ``` ### docs/demos/tutorial/plugins/dnd/index.less ```less .dnd-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .dnd-wrap { display: flex; flex-direction: column; align-items: center; width: 200px; padding: 16px; border: 1px solid #f0f0f0; user-select: none; .dnd-rect { width: 100px; height: 40px; margin: 16px; line-height: 40px; text-align: center; border: 1px solid #8f8f8f; border-radius: 6px; cursor: move; } .dnd-circle { width: 60px; height: 60px; margin: 16px; line-height: 60px; text-align: center; border: 1px solid #8f8f8f; border-radius: 100%; cursor: move; } } } ``` #### 小地图 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/plugins/minimap/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/plugins/minimap.md - Description: 小地图 --- title: "小地图" description: "小地图" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/plugins/minimap/" version: "3.x" --- > **在本章节中主要介绍小地图插件相关的知识,通过阅读,你可以了解到** - 如何使用小地图功能 ## 使用 你可以通过插件 `MiniMap` 启用小地图功能,示例: ```ts import { Graph, MiniMap } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use( new MiniMap({ container: document.getElementById('minimap'), }), ) ``` ## 演示 - 移动小地图视口来移动画布。 - 缩放小地图视口来缩放画布。 ```ts import './index.less' import { createRoot } from 'react-dom/client' import * as React from 'react' import { Graph, MiniMap, Scroller } from '@antv/x6' import { Radio } from 'antd' import { SimpleNodeView } from './simple-view.tsx' const options = [ { label: '简单视图', value: 'simple' }, { label: '详细视图', value: 'detailed' }, ] export default class Example extends React.Component { private container: HTMLDivElement private minimapContainer: HTMLDivElement private graph: Graph componentDidMount() { this.graph = new Graph({ container: this.container, width: 600, height: 320, panning: false, background: { color: '#F2F7FA', }, }) this.graph.use( new Scroller({ enabled: true, pageVisible: true, pageBreak: false, pannable: true, }), ) this.graph.use( new MiniMap({ container: this.minimapContainer, width: 200, height: 160, padding: 10, }), ) this.graph.addNode({ x: 200, y: 100, width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const source = this.graph.addNode({ x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 160, y: 180, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } onMinimapViewChange = (val: string) => { this.graph.disposePlugins('minimap') if (val === 'simple') { this.graph.use( new MiniMap({ container: this.minimapContainer, width: 200, height: 160, padding: 10, graphOptions: { createCellView(cell) { // 可以返回三种类型数据 // 1. null: 不渲染 // 2. undefined: 使用 X6 默认渲染方式 // 3. CellView: 自定义渲染 if (cell.isEdge()) { return null } if (cell.isNode()) { return SimpleNodeView } }, }, }), ) } else { this.graph.use( new MiniMap({ container: this.minimapContainer, width: 200, height: 160, padding: 10, }), ) } } refContainer = (container: HTMLDivElement) => { this.container = container } refMiniMapContainer = (container: HTMLDivElement) => { this.minimapContainer = container } render() { return ( <div className="minimap-app"> <div className="app-btns"> <Radio.Group options={options} onChange={(e) => this.onMinimapViewChange(e.target.value)} defaultValue={'detailed'} optionType="button" /> </div> <div className="app-content" ref={this.refContainer} /> <div className="app-minimap" ref={this.refMiniMapContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 选项 | 属性名 | 类型 | 默认值 | 必选 | 描述 | |--------------|---------------|--------|------|-------------------------| | container | HTMLElement | - | ✓️ | 挂载小地图的容器 | | width | number | `300` | | 小地图的宽度 | | height | number | `200` | | 小地图的高度 | | padding | number | `10` | | 小地图容器的 padding 边距 | | scalable | boolean | `true` | | 是否启用缩放 | | minScale | number | `0.01` | | 最小缩放比例 | | maxScale | number | `16` | | 最大缩放比例 | | graphOptions | Graph.Options | `{}` | | 小地图内部 Graph 的选项 | | createGraph | (options: Graph.Options) => Graph | `options => new Graph(options)` | | 自定义创建小地图内部 Graph 的方法 | ### docs/demos/tutorial/plugins/minimap/index.less ```less .minimap-app { position: relative; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-btns { margin-bottom: 8px; } .app-minimap { position: absolute; bottom: 0; left: 620px; width: 200px; height: 150px; } .x6-widget-minimap-viewport { border: 2px solid #8f8f8f; } .x6-widget-minimap-viewport-zoom { border: 2px solid #8f8f8f; } } ``` ### docs/demos/tutorial/plugins/minimap/simple-view.tsx ```tsx import { NodeView } from '@antv/x6' export class SimpleNodeView extends NodeView { protected renderMarkup() { return this.renderJSONMarkup({ tagName: 'rect', selector: 'body', }) } update() { super.update({ body: { refWidth: '100%', refHeight: '100%', fill: '#8f8f8f', }, }) } } ``` #### Stencil - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/plugins/stencil/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/plugins/stencil.md - Description: Stencil --- title: "Stencil" description: "Stencil" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/plugins/stencil/" version: "3.x" --- > **在本章节中主要介绍 Stencil 插件相关的知识,通过阅读,你可以了解到** - 如何通过 Stencil 提升 Dnd 能力 - 如何配置分组、折叠与搜索 ## 使用 Stencil 是在 Dnd 基础上的进一步封装,提供了一个类似侧边栏的 UI 组件,并支持分组、折叠、搜索等能力。你可以通过插件 `Stencil` 启用该侧边栏模板功能,示例: ```ts import { Graph, Stencil } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) const stencil = new Stencil({ target: graph, groups: [ { name: 'group1', }, { name: 'group2', }, ], }) const rect1 = graph.createNode({ shape: 'rect', width: 100, height: 40, }) const rect2 = rect1.clone() // 需要一个容纳 stencil 的 Dom 容器 stencilContainer stencilContainer.appendChild(stencil.container) stencil.load([rect1, rect2], 'group1') ``` ## 演示 ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Stencil, Snapline } from '@antv/x6' const commonAttrs = { body: { fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, } export default class Example extends React.Component { private container: HTMLDivElement private stencilContainer: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.use( new Snapline({ enabled: true, sharp: true, }), ) const source = graph.addNode({ x: 130, y: 30, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode({ x: 320, y: 240, width: 100, height: 40, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.centerContent() const stencil = new Stencil({ title: 'Stencil', target: graph, search(cell, keyword) { return cell.shape.indexOf(keyword) !== -1 }, placeholder: 'Search by shape name', notFoundText: 'Not Found', collapsable: true, stencilGraphHeight: 0, groups: [ { name: 'group1', title: 'Group(Collapsable)', }, { name: 'group2', title: 'Group', collapsable: false, }, ], }) this.stencilContainer.appendChild(stencil.container) const n1 = graph.createNode({ shape: 'rect', x: 40, y: 40, width: 80, height: 40, label: 'rect', attrs: commonAttrs, }) const n2 = graph.createNode({ shape: 'circle', x: 180, y: 40, width: 40, height: 40, label: 'circle', attrs: commonAttrs, }) const n3 = graph.createNode({ shape: 'ellipse', x: 280, y: 40, width: 80, height: 40, label: 'ellipse', attrs: commonAttrs, }) const n4 = graph.createNode({ shape: 'path', x: 420, y: 40, width: 40, height: 40, // https://www.svgrepo.com/svg/13653/like path: 'M24.85,10.126c2.018-4.783,6.628-8.125,11.99-8.125c7.223,0,12.425,6.179,13.079,13.543c0,0,0.353,1.828-0.424,5.119c-1.058,4.482-3.545,8.464-6.898,11.503L24.85,48L7.402,32.165c-3.353-3.038-5.84-7.021-6.898-11.503c-0.777-3.291-0.424-5.119-0.424-5.119C0.734,8.179,5.936,2,13.159,2C18.522,2,22.832,5.343,24.85,10.126z', attrs: commonAttrs, label: 'path', }) stencil.load([n1, n2], 'group1') stencil.load([n3, n4], 'group2') } refContainer = (container: HTMLDivElement) => { this.container = container } refStencil = (container: HTMLDivElement) => { this.stencilContainer = container } render() { return ( <div className="stencil-app"> <div className="app-stencil" ref={this.refStencil} /> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## 配置 | 选项 | 类型 | 必选 | 默认值 | 说明 | |---------------------|-------------------------------------------------------------|:----:|----------------------|-------------------------------------| | title | string | | `'Stencil'` | 标题。 | | groups | Group[] | ✓️ | - | 分组信息。 | | search | Filter | | `false` | 搜索选项。 | | placeholder | string | | `'Search'` | 搜索文本框的 placeholder 文本。 | | notFoundText | string | | `'No matches found'` | 未匹配到搜索结果时的提示文本。 | | collapsable | boolean | | `false` | 是否显示全局折叠/展开按钮。 | | layout | (this: Stencil, model: Model, group?: Group \| null) => any | | 网格布局 | 模板画布中节点的布局方法。 | | layoutOptions | any | | - | 布局选项。 | | stencilGraphWidth | number | | `200` | 模板画布宽度。 | | stencilGraphHeight | number | | `800` | 模板画布高度。设置为 0 时高度会自适应。 | | stencilGraphPadding | number | | `10` | 模板画布边距。 | | stencilGraphOptions | Graph.Options | | - | 模板画布选项。 | > **提示** 除了上面的配置,Stencil 还继承了 Dnd 的所有配置。 ### 分组 初始化时,按照 `groups` 提供的分组,在每个分组中会渲染一个模板画布。分组的类型定义如下: ```ts export interface Group { name: string // 分组名称 title?: string // 分组标题,缺省时使用 `name` collapsable?: boolean // 分组是否可折叠,默认为 true collapsed?: boolean // 初始状态是否为折叠状态 nodeMovable?: boolean // 模板画布中的节点能否拖动 graphWidth?: number // 模板画布宽度 graphHeight?: number // 模板画布高度 graphPadding?: number // 模板画布边距 graphOptions?: Graph.Options // 模板画布选项 layout?: (this: Stencil, model: Model, group?: Group | null) => any layoutOptions?: any // 布局选项 } ``` 可以看到分组内的一些配置和外层的配置有重合,比如 `graphWidth`/`stencilGraphWidth` 与 `graphHeight`/`stencilGraphHeight`,分组内的配置优先级更高。 ### 布局 添加节点时,使用分组或全局的 `layout` 和 `layoutOptions` 来对节点进行自动布局,默认使用网格布局方法来布局模板节点,支持的布局选项有: | 选项 | 类型 | 默认值 | 说明 | |-------------|-------------------------------|----------|----------------------------------------------------------------------------------| | columns | number | `2` | 网格布局的列数,默认为 `2`。行数根据节点数自动计算。 | | columnWidth | number \| 'auto' \| 'compact' | `'auto'` | 列宽。auto: 所有节点中最宽节点的宽度作为列宽,compact: 该列中最宽节点的宽度作为列宽。 | | rowHeight | number \| 'auto' \| 'compact' | `'auto'` | 行高。auto: 所有节点中最高节点的高度作为行高,compact: 该行中最高节点的高度作为行高。 | | dx | number | `10` | 单元格在 X 轴的偏移量,默认为 `10`。 | | dy | number | `10` | 单元格在 Y 轴的偏移量,默认为 `10`。 | | marginX | number | `0` | 单元格在 X 轴的边距,默认为 `0`。 | | marginY | number | `0` | 单元格在 Y 轴的边距,默认为 `0`。 | | center | boolean | `true` | 节点是否与网格居中对齐,默认为 `true`。 | | resizeToFit | boolean | `false` | 是否自动调整节点的大小来适应网格大小,默认为 `false`。 | 也可以按照 `(this: Stencil, model: Model, group?: Group | null) => any` 签名进行自定义布局。 ### 搜索 Stencil 还提供了搜索能力。 ```ts // 只搜索 rect 节点 const stencil = new Stencil({ search: (cell, keyword, groupName, stencil) => { if (keyword) { return cell.shape === 'rect' } return true }, }) // 搜索 text 包含关键字的 rect 节点 const stencil = new Stencil({ search: (cell, keyword, groupName, stencil) => { if (keyword) { return cell.shape === 'rect' && cell.attr('text/text').includes(keyword) } return true }, }) ``` ## API ### stencil.load(...) ```ts load(nodes: (Node | Node.Metadata)[], groupName?: string): this ``` 加载节点。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |-----------|-----------------------------|:----:|--------|-------------------| | nodes | `(Node \| Node.Metadata)[]` | ✓️ | - | 加载的节点。 | | groupName | string | | - | 加载节点的分组名称。 | ### stencil.unload(...) ```ts unload(nodes: (Node | Node.Metadata)[], groupName?: string): this ``` 卸载节点。参数如下: | 名称 | 类型 | 必选 | 默认值 | 描述 | |-----------|-----------------------------|:----:|--------|-------------------| | nodes | `(Node \| Node.Metadata)[]` | ✓️ | - | 卸载的节点。 | | groupName | string | | - | 卸载节点的分组名称。 | ### stencil.addGroup(...) ```ts addGroup(group: Stencil.Group | Stencil.Group[]) ``` 新增分组。 ```ts stencil.addGroup({ name: 'group1', graphHeight: 100, }) ``` ### stencil.removeGroup(...) ```ts removeGroup(groupName: string | string[]) ``` 删除分组。 ### stencil.resizeGroup(...) ```typescript resizeGroup(groupName: string, size: { width: number; height: number }) ``` 修改分组的尺寸。 ### docs/demos/tutorial/plugins/stencil/index.less ```less .stencil-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-stencil { position: relative; width: 200px; border: 1px solid #f0f0f0; } .app-content { flex: 1; height: 380px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .x6-widget-stencil-group-title { // 避免 dumi 的样式影响 margin-top: 0 !important; } } ``` #### 导出 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/plugins/export/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/plugins/export.md - Description: 导出 --- title: "导出" description: "导出" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/plugins/export/" version: "3.x" --- > **本章节主要介绍导出插件相关的知识,通过阅读,你可以了解到** - 如何将画布内容通过图片格式导出 ## 使用 你可以通过插件 `Export` 将画布内容导出为图片格式,示例: ```ts import { Graph, Export } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use(new Export()) ``` ## API ### graph.exportSVG(...) ```ts exportSVG(fileName?: string, options?: ExportToSVGOptions): void ``` `fileName` 为文件名称,缺省为 `chart`,`ExportToSVGOptions` 描述如下: | 属性名 | 类型 | 默认值 | 必选 | 描述 | |--------------------|--------------------------------------------|--------|------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | preserveDimensions | `boolean \| Size` | - | | 控制导出 SVG 的尺寸;未设置时 width/height 为 100%;设置为 true 时自动计算为图形区域的实际大小;也可传入 `{ width, height }` 显式指定导出尺寸。 | | viewBox | `RectangleLike` | - | | 设置导出 SVG 的 viewBox | | copyStyles | `boolean` | `true` | | 是否复制外部样式表中的样式,默认 true。开启后会将节点的计算样式按差异内联到导出的 SVG,以保持页面与导出视觉一致,但会增加导出耗时;若样式已通过 `attrs` 指定或追求速度,可设为 `false`,并结合 `stylesheet` 注入必要 CSS。 | | stylesheet | `string` | - | | 自定义样式表 | | serializeImages | `boolean` | `true` | | 是否将 image 元素的 `xlink:href`/`href` 链接转化为 DataURI(导出 PNG/JPEG 时会强制启用)。 | | beforeSerialize | `(this: Graph, svg: SVGSVGElement) => any` | - | | 可以在导出 svg 字符串之前调用 beforeSerialize 来修改它 | ### graph.exportPNG(...) ```ts exportPNG(fileName?: string, options?: ExportToImageOptions): void ``` `fileName` 为文件名称,缺省为 `chart`,`ExportToImageOptions` 除了继承上面 `ExportToSVGOptions` 外,还有以下配置: | 属性名 | 类型 | 默认值 | 必选 | 描述 | |-----------------|-----------------------|--------|------|----------------------------------------------------------------------------------| | width | `number` | - | | 导出图片的宽度 | | height | `number` | - | | 导出图片的高度 | | ratio | `number` | `1` | | 输出缩放比例(如设备像素比),用于计算导出分辨率。 | | backgroundColor | `string` | `'white'` | | 导出图片的背景色。 | | padding | `NumberExt.SideOptions` | - | | 图片的 padding | | quality | `number` | - | | 图片质量,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92 | ### graph.exportJPEG(...) ```ts exportJPEG(fileName?: string, options?: ExportToImageOptions): void ``` ### graph.toSVG(...) ```ts toSVG(callback: ExportToSVGCallback, options?: ExportToSVGOptions): void ``` ### graph.toPNG(...) ```ts toPNG(callback: ExportToSVGCallback, options?: ExportToImageOptions): void ``` ### graph.toJPEG(...) ```ts toJPEG(callback: ExportToSVGCallback, options?: ExportToImageOptions): void ``` ### graph.toSVGAsync(...) ```ts toSVGAsync(options?: ExportToSVGOptions): Promise<string> ``` ### graph.toPNGAsync(...) ```ts toPNGAsync(options?: ExportToImageOptions): Promise<string> ``` ### graph.toJPEGAsync(...) ```ts toJPEGAsync(options?: ExportToImageOptions): Promise<string> ``` #### 引脚编辑器 - Canonical page: https://x6.antv.antgroup.com/zh/tutorial/plugins/port-editor/ - Markdown source: https://x6.antv.antgroup.com/zh/tutorial/plugins/port-editor.md - Description: 引脚编辑器 --- title: "引脚编辑器" description: "引脚编辑器" language: "zh" canonical: "https://x6.antv.antgroup.com/zh/tutorial/plugins/port-editor/" version: "3.x" --- :::info{title=在本章节中主要介绍引脚编辑器插件相关的知识,通过阅读,你可以了解到} - 如何在画布上交互式地添加、删除引脚 - 引脚如何复用 X6 的端口(port)体系,从而天然支持连线与导出 - 如何配置引脚落点、编号与连线样式 ::: ## 使用 X6 的端口是一份**声明式数据**:位置、样式、所属分组都由使用者在创建节点时写好,运行期只有 `node.addPort` / `node.removePort` 这类程序化接口。引脚编辑器 `PortEditor` 补上的是**编辑动作**: 让"在哪里加一个引脚"由人在画布上直接点出来。 ```ts import { Graph, PortEditor } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container')!, }) const portEditor = new PortEditor({ group: 'pin', outlineSelector: '.x6-node-body', }) graph.use(portEditor) // 进入"添加引脚"模式:鼠标划过元件轮廓时出现落点预览,点击即在该处生成引脚 document.getElementById('add-pin')!.addEventListener('click', () => { portEditor.toggleAdding() }) ``` 引脚落在**轮廓上的最近点**:插件会对元件 markup 中的几何图形(`rect` / `circle` / `ellipse` / `path` / `polygon` / `polyline`)按弧长采样,再把鼠标位置投影到最近的一条边上, 因此异形元件(六边形、圆形、变压器符号…)也能贴边落点,而不是落在包围盒的四个角上。 投影全程走 SVG CTM 链,画布平移、缩放以及元件自身的旋转都不需要特判。 生成的引脚就是一个普通的端口:可以直接从它拖拽连线,也会随 `graph.toJSON()` 一起导出、 并能通过 `graph.fromJSON()` 原样还原。 ## 配置 ```ts interface PortEditorOptions { // 引脚使用的端口分组,节点上没有该分组时自动创建 group?: string // 生成引脚 id 的前缀:pin-1、pin-2 ... idPrefix?: string // 'local' 存节点局部坐标(像素,缩放元件时保持像素偏移) // 'percent' 存节点尺寸的百分比(缩放元件时保持相对位置) positionUnit?: 'local' | 'percent' // 轮廓选择器;缺省用 markup 中所有几何图形,取最近的一个 outlineSelector?: string // 每条轮廓的最大采样点数 sampleCount?: number // 节点缺少端口分组时是否自动创建 autoCreateGroup?: boolean // 自动创建时使用的分组定义 groupConfig?: Record<string, any> // 悬停已有引脚时是否显示删除角标 deleteBadge?: boolean // 放好一个引脚后是否自动退出添加模式 stopAfterAdd?: boolean // 添加模式下禁止拖动元件(默认 true) blockNodeMove?: boolean // 添加模式下禁用画布平移(默认 true) blockPanning?: boolean } ``` `blockNodeMove` 与 `blockPanning` 默认开启,因为"按下元件本体加引脚"这个动作在 X6 中同时是 元件拖动与画布平移的起手式:不屏蔽就会变成拖元件或拖画布。这两个开关只在添加模式内生效, 退出时会**逐项还原**,不会在你的图配置里留下痕迹。 ## API ```ts // 模式 portEditor.startAdding(): this portEditor.stopAdding(): this portEditor.toggleAdding(): this portEditor.isAdding(): boolean // 引脚 portEditor.addPin(node: Node, local: Point): string | null portEditor.addPinAtClient(node: Node, clientX: number, clientY: number): string | null portEditor.addPinFromDrop(clientX: number, clientY: number, node?: Node): string | null portEditor.removePin(node: Node, portId: string): this portEditor.clearPins(node: Node): this portEditor.getPins(node: Node): Port[] // 拖拽落点(配合 Dnd 插件使用) portEditor.createDndDropValidator(options?: { isPinTemplate?: (node: Node) => boolean }): ( droppingNode: Node, ctx?: { targetGraph?: Graph }, ) => boolean // 命中测试 portEditor.findNodeAtClient(clientX: number, clientY: number): Node | null portEditor.dispose(): void ``` ## 与其它插件的配合 - **Selection**:选中元件后,选择框默认会覆盖元件轮廓,而引脚正好也落在轮廓上。请把选择框 设为不接收指针事件,否则在选中态下按下轮廓无法生成引脚、也无法从引脚起线: ```ts graph.use( new Selection({ showNodeSelectionBox: true, pointerEvents: 'none', }), ) ``` - **Dnd**:把「引脚模板」拖到元件上生成引脚,并阻止模板节点本身落地: ```ts graph.use( new Dnd({ target: graph, validateNode: portEditor.createDndDropValidator(), }), ) ``` 默认把 `node.getData().pinTemplate === true` 或 `shape === 'pin-dot'` 的节点视为引脚模板。 - **History**:引脚的新增与删除都包在 `model.startBatch/stopBatch` 中,一次操作对应一步撤销。 ## English documentation ### api #### Graph - Canonical page: https://x6.antv.antgroup.com/en/api/graph/graph/ - Markdown source: https://x6.antv.antgroup.com/en/api/graph/graph.md - Description: Graph --- title: "Graph" description: "Graph" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/graph/graph/" version: "3.x" --- ## Configuration ```ts new Graph(options: Options) ``` | Option | Type | Required | Description | Default Value | | --- | --- | :-: | --- | --- | | container | `HTMLElement \| string` | ✓ | The container of the canvas. A string is resolved only as an element ID, without a leading `#`. | | | width | `number` | | The width of the canvas, defaults to the container width. | - | | height | `number` | | The height of the canvas, defaults to the container height. | - | | scaling | `{ min?: number, max?: number }` | | The minimum and maximum zoom levels of the canvas. | `{ min: 0.01, max: 16 }` | | [autoResize](/en/tutorial/basic/graph#canvas-size) | `boolean \| Element \| Document` | | Whether to listen to container size changes and automatically update the canvas size. | `false` | | [panning](/en/api/graph/panning) | `boolean \| PanningManager.Options` | | Whether the canvas can be panned, defaults to disabled. | `false` | | [mousewheel](/en/api/graph/mousewheel) | `boolean \| MouseWheel.Options` | | Whether the mouse wheel can zoom, defaults to disabled. | `false` | | [grid](/en/api/graph/grid) | `boolean \| number \| GridManager.Options` | | The grid, defaults to a 10px grid but does not draw the grid background. | `false` | | [background](/en/api/graph/background) | `false \| BackgroundManager.Options` | | The background, defaults to not drawing the background. | `false` | | [translating](/en/api/model/interaction#movement-range) | `Translating.Options` | | Restricts node movement. After a node is moved, automatically offset when it overlaps with other nodes. | `{ restrict: false, autoOffset: true }` | | [embedding](/en/api/model/interaction#embedding) | `boolean \| Embedding.Options` | | Whether to enable nested nodes, defaults to disabled. | `false` | | [connecting](/en/api/model/interaction#connecting) | `Connecting.Options` | | The connection options. | `{ snap: false, ... }` | | [highlighting](/en/api/model/interaction#highlighting) | `Highlighting.Options` | | The highlighting options. | `{...}` | | [interacting](/en/api/model/interaction#restrictions) | `Interacting.Options` | | Customizes the interaction behavior of nodes and edges. | `{ edgeLabelMovable: false }` | | [magnetThreshold](/en/api/mvc/view#magnetthreshold) | `number \| onleave` | | The number of times the mouse can move before triggering a connection, or set to `onleave` to trigger a connection when the mouse leaves an element. | `0` | | [moveThreshold](/en/api/mvc/view#movethreshold) | `number` | | The number of times the mouse can move before triggering a `mousemove` event. | `0` | | [clickThreshold](/en/api/mvc/view#clickthreshold) | `number` | | When the mouse moves more than the specified number of times, the mouse click event will not be triggered. | `0` | | [preventDefaultContextMenu](/en/api/mvc/view#preventdefaultcontextmenu) | `boolean` | | Whether to disable the browser's default right-click menu. | `true` | | [preventDefaultBlankAction](/en/api/mvc/view#preventdefaultblankaction) | `boolean` | | Whether to disable the default mouse behavior when clicking on a blank area of the canvas. | `true` | | [async](/en/api/mvc/view#async) | `boolean` | | Whether to render asynchronously. | `true` | | [virtual](/en/api/mvc/view#virtual) | `boolean \| VirtualOptions` | | Whether to only render the visible area of the canvas; when an object is provided, you can configure the buffer margin `margin` and the switch `enabled`. | `false` | | [onPortRendered](/en/api/mvc/view#onportrendered) | `(args: OnPortRenderedArgs) => void` | | The callback triggered when a port is rendered. | - | | [onEdgeLabelRendered](/en/api/mvc/view#onedgelabelrendered) | `(args: OnEdgeLabelRenderedArgs) => void \| ((args: OnEdgeLabelRenderedArgs) => void)` | | The callback triggered when an edge label is rendered, And it can return a cleanup function, which will be executed when the label is destroyed. | - | | [createCellView](/en/api/mvc/view#createcellview) | `(cell: Cell) => CellView \| null \| undefined` | | Customizes the view of a cell. | - | #### Cell - Canonical page: https://x6.antv.antgroup.com/en/api/model/cell/ - Markdown source: https://x6.antv.antgroup.com/en/api/model/cell.md - Description: Cell --- title: "Cell" description: "Cell" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/model/cell/" version: "3.x" --- Cell is the base class for [Node](/en/api/model/node) and [Edge](/en/api/model/edge), containing common property and method definitions for nodes and edges, such as attribute styles, visibility, business data, etc. It also exhibits the same behavior in terms of instantiation, style customization, default options, and custom options. ## Properties | Option | Type | Default | Required | Description | |----------|---------------------------------|---------|:--------:|------------------------------------------------------------------------------------------------| | id | string | | | Unique identifier for the node/edge. It's recommended to use an ID with business meaning. By default, a UUID is automatically generated. | | markup | Markup | | | SVG/HTML fragment for the node/edge. | | attrs | Attr.CellAttrs | | | Attribute styles for the node/edge. | | shape | string | | | Shape used to render the node/edge. Default value for nodes is `rect`, for edges is `edge`. | | view | string | | | View used to render the node/edge. | | zIndex | number | | | Layer level of the node/edge in the canvas. By default, it's automatically determined based on the order of node/edge addition. | | visible | boolean | `true` | | Whether the node/edge is visible. | | parent | string | | | Parent node. | | children | string[] | | | Child nodes/edges. | | tools | ToolItem \| ToolItem[] \| Tools | | | Tool options. | | data | any | | | Business data associated with the node/edge. | ### id `id` is the unique identifier for the node/edge. It's recommended to use an ID with business meaning. By default, a UUID is automatically generated. ### markup `markup` specifies the SVG/HTML fragment used to render the node/edge, described in JSON format. For example, the `markup` definition for the built-in node `Shape.Rect` is as follows: ```ts { markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'text', selector: 'label', }, ], } ``` This indicates that the node internally contains two SVG elements: `<rect>` and `<text>`. After rendering to the page, the SVG element corresponding to the node looks like this: ```html <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> ``` From the above introduction, we have a general understanding of the `Markup` structure. Now, let's detail the `Markup` definition. ```ts 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[] } ``` | Option | Type | Default | Required | Description | |---------------|------------------|--------------------------------|:--------:|------------------------------------------------------------------------------------------------| | tagName | string | | ✓ | SVG/HTML element tag name. | | ns | string | `"http://www.w3.org/2000/svg"` | | SVG/HTML element namespace. | | selector | string | - | | Unique selector for the element, used to locate the element or specify attribute styles for it. | | groupSelector | string | - | | Group selector for the element, can be used to specify styles for multiple elements in the group simultaneously. | | attrs | Attr.SimpleAttrs | - | | Default attribute key-value pairs for the element. | | style | KeyValue | - | | Inline style key-value pairs for the element. | | className | string | - | | CSS class name for the element. | | textContent | string | - | | Text content of the element. | | children | Markup[] | - | | Nested child elements. | #### tagName Specifies which type of SVG/HTML element to create through `tagName`. #### ns The namespace of the element. It should correspond to the element type specified by `tagName`. By default, it uses the SVG element namespace `"http://www.w3.org/2000/svg"`. - SVG element namespace is `"http://www.w3.org/2000/svg"` - HTML element namespace is `"http://www.w3.org/1999/xhtml"` #### selector The unique selector for the element, used to specify [attribute styles](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Fills_and_Strokes) for the element. For example, to specify attribute styles for `<rect>` and `<text>` elements of the built-in node `Shape.Rect`: ```ts const rect = new Shape.Rect({ x: 40, y: 40, width: 100, height: 40, attrs: { // Specify styles for the rect element body: { stroke: '#000', // Border color fill: '#fff', // Fill color }, // Specify styles for the text element label: { text: 'rect', // Text content fill: '#333', // Text color }, }, }) ``` #### groupSelector The group selector for the element. Through the group selector, styles can be specified for multiple elements associated with the group. For example, in the following Markup, two `<rect>` elements have the same `groupSelector` value `group1`: ```ts { markup: [ { tagName: 'rect', selector: 'body', groupSelector: 'group1', }, { tagName: 'rect', selector: 'wrap', groupSelector: 'group1', }, { tagName: 'text', selector: 'label', }, ], } ``` When creating a node, we can specify group styles like this: ```ts new SomeNode({ attrs: { group1: { fill: '#2ECC71', }, }, }) ``` #### attrs Default attribute key-value pairs for the element, typically used to define unchanging common attributes. These default attributes can also be overridden when instantiating the node. Note that the `attrs` property in `markup` only supports native SVG attributes, meaning X6's [custom attributes]() are not available here. For example, we specified the following default attributes for the `<rect>` and `<text>` elements of the built-in node `Shape.Rect`: ```js { markup: [ { tagName: 'rect', selector: 'body', attrs: { fill: '#fff', stroke: '#000', strokeWidth: 2, } }, { tagName: 'text', selector: 'label', attrs: { fill: '#333', textAnchor: 'middle', textVerticalAnchor: 'middle', } }, ], } ``` #### style Inline style key-value pairs for the element. #### className CSS class name for the element. #### textContent Text content of the element. #### children Nested child elements. ### attrs The attribute option `attrs` is a complex object. The keys of this object are the selectors ([selector](#selector)) of elements defined in the node's Markup, and the corresponding values are [SVG attribute values](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute) (such as [fill](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill) and [stroke](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke)) applied to that SVG element. If you're not familiar with SVG attributes yet, you can refer to the beginner's tutorial on [Fills and Strokes](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Fills_and_Strokes) provided by MDN. For example, the Markup of the built-in node `Shape.Rect` defines two selectors: `body` (representing the `<rect>` element) and `label` (representing the `<text>` element). We can specify attribute styles for elements in this node like this: ```ts 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, }, }, }) ``` After the node is rendered to the canvas, the DOM structure looks like this: ```html <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> ``` Additionally, we can use CSS selectors to specify node styles, so we don't have to remember predefined selector names. We can simply define styles based on the rendered DOM structure. When using CSS selectors, it's important to note that the specified CSS selector may match multiple elements, in which case the corresponding attribute styles will be applied to multiple elements simultaneously. ```ts 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's worth mentioning that camelCase format for property names is supported, such as `fontSize`. This avoids the hassle of having to add quotes to property names like `font-size` when used as object keys. In addition to standard SVG attributes, we have defined a series of special attributes in X6. For details, please refer to [Special Attributes](/en/api/model/attrs) and [Custom Attributes](/en/api/model/cell/#custom-attributes). Furthermore, we can use CSS to customize styles. Nodes and edges rendered on the canvas have the class names `x6-node` and `x6-edge` respectively. The default style definitions can be [referenced here](https://github.com/antvis/X6/blob/master/src/style/index.less). For example, we can specify the style of the `<rect>` element in nodes like this: ```css .x6-node rect { fill: #2ecc71; stroke: #000; } ``` After creating nodes/edges, we can call the `attr()` method on the instance to modify node attribute styles. In the code below, the path separated by `/` modifies the style. The `label` selector corresponds to the `<text>` element, `text` is the attribute name of that element, and `hello` is the new attribute value. ```ts rect.attr('label/text', 'hello') // Equivalent to rect.attr('label', { text: 'hello', }) // Equivalent to rect.attr({ label: { text: 'hello', }, }) ``` When the attribute value passed in is `null`, that attribute can be removed. ```ts rect.attr('label/text', null) ``` ### shape The shape of the node/edge, similar to the Model in the MVC pattern, determines the structured data of the node/edge. This option is typically used when adding nodes and edges with the `graph.addNode` and `graph.addEdge` methods. ```ts 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', }) const edge = graph.addEdge({ shape: 'edge', source: rect, target: circle, }) ``` In X6's internal implementation, we use the shape specified by `shape` to find the corresponding constructor to initialize the node/edge and add it to the canvas. The default values for this option are: - The default value of `shape` in the `graph.addNode` method is `rect` - The default value of `shape` in the `graph.addEdge` method is `edge` At the same time, we have built-in a series of nodes and edges in X6. | Constructor | shape name | Description | |---------------|------------|--------------------------------------------------| | Shape.Rect | rect | Rectangle. | | Shape.Circle | circle | Circle. | | Shape.Ellipse | ellipse | Ellipse. | | Shape.Polygon | polygon | Polygon. | | Shape.Polyline| polyline | Polyline. | | Shape.Path | path | Path. | | Shape.Image | image | Image. | | Shape.HTML | html | HTML node, renders HTML fragment using `foreignObject`. | ### view Specifies the view used to render the node/edge. The concept of view is consistent with the View in the MVC pattern. Generally, there's no need to set the view field, as X6's built-in view is used by default. ### zIndex The layer level of the node/edge in the canvas, automatically determined by the order of node/edge addition by default. After the node/edge is rendered to the canvas, you can use `cell.getZIndex()` and `cell.setZIndex(z: number)` to get or set the `zIndex` value, or call `cell.toFront()` and `cell.toBack()` to move it to the top or bottom layer. ### visible Whether the node/edge is visible, visible by default. ### parent Parent node ID. ### children Array of child node/edge IDs. ### tools Tools for nodes/edges. Tools can enhance the interaction capabilities of nodes/edges. We provide the following built-in tools for nodes and edges respectively: Nodes - [button](/en/api/registry/node-tool#button) Renders a button at the specified position, supports customizing button click interactions. - [button-remove](/en/api/registry/node-tool#button-remove) Renders a delete button at the specified position, deletes the corresponding node when clicked. - [boundary](/en/api/registry/node-tool#boundary) Renders a rectangle surrounding the node based on the node's bounding box. Note that this tool only renders a rectangle without any interaction. - [node-editor](/en/api/registry/node-tool#node-editor) Provides text editing functionality on the node. Edges - [vertices](/en/api/registry/edge-tool#vertices) Path point tool, renders a small dot at the path point position, drag the dot to modify the path point position, double-click the dot to delete the path point, click on the edge to add a path point. - [segments](/en/api/registry/edge-tool#segments) Segment tool. Renders a toolbar at the center of each edge segment, which can be dragged to adjust the positions of the path points at both ends of the segment. - [boundary](/en/api/registry/edge-tool#boundary) Renders a rectangle surrounding the edge based on the edge's bounding box. Note that this tool only renders a rectangle without any interaction. - [button](/en/api/registry/edge-tool#button) Renders a button at the specified position, supports customizing button click interactions. - [button-remove](/en/api/registry/edge-tool#button-remove) Renders a delete button at the specified position, deletes the corresponding edge when clicked. - [source-arrowhead-and-target-arrowhead](/en/api/registry/edge-tool#source-arrowhead-and-target-arrowhead) Renders a shape (arrow by default) at the start or end point of the edge, drag the shape to modify the start or end point of the edge. - [edge-editor](/en/api/registry/edge-tool#edge-editor) Provides text editing functionality on the edge. You can specify a single tool: ```ts graph.addNode({ x: 40, y: 40, width: 100, height: 40, tools: 'button-remove', // or { name: 'button-remove' } }) ``` Also, you can specify the parameter options for the tool like this: ```ts graph.addNode({ x: 40, y: 40, width: 100, height: 40, tools: { name: 'button-remove', args: { x: 10, // x coordinate of the button, relative to the top-left corner of the node y: 10, // y coordinate of the button, relative to the top-left corner of the node }, }, }) ``` You can also specify multiple tools at the same time: ```ts graph.addNode({ x: 40, y: 40, width: 100, height: 40, tools: [ 'button-remove', { name: 'boundary', args: { padding: 5, }, }, ], }) ``` ### data Business data associated with the node/edge. For example, in actual use, we usually store certain business data on the `data` of the node/edge. ```ts const rect = new Shape.Rect({ x: 40, y: 40, width: 100, height: 40, data: { bizID: 125, date: '20200630', price: 89.0, }, }) ``` ## Methods ### General #### get shape Get the shape of the node/edge, returns the name of the shape registered to X6. ```ts if (node.shape === 'rect') { // do something if the node is a 'rect' node. } ``` #### get view Get the view of the node/edge, returns the name of the view registered to X6. ```ts if (node.view === 'rect') { // do something if the node is a 'rect' view. } ``` #### isNode() ```ts isNode(): boolean ``` Checks if the instance is a [Node](/en/api/model/node) instance. Returns `true` if it's a [Node](/en/api/model/node) instance, otherwise returns `false`. All nodes inheriting from [Node](/en/api/model/node) return `true`. ```ts if (cell.isNode()) { // do something if the cell is a node. } ``` #### isEdge() ```ts isEdge(): boolean ``` Checks if the instance is an [Edge](/en/api/model/edge) instance. Returns `true` if it's an [Edge](/en/api/model/edge) instance, otherwise returns `false`. All edges inheriting from [Edge](/en/api/model/edge) return `true`. ```ts if (cell.isEdge()) { // do something if the cell is an edge. } ``` #### toJSON(...) ```ts toJSON(options?: Cell.ToJSONOptions): Object ``` Converts the structured data of the node/edge to JSON data for persistent storage (usually we call `graph.toJSON` to export the data of the entire canvas). | Option | Type | Default | Required | Description | |--------------|---------|---------|:--------:|------------------------------------------------| | options.diff | boolean | `false` | | Whether to return data that differs from the default values (still exports data for the entire canvas). | - When `options.diff` is `false`, returns complete data. - When `options.diff` is `true`, returns differential data (removes default values of properties). #### clone(...) ```ts clone(options?: Cell.CloneOptions): Cell | Node | Edge | { [id:string]: Node | Edge } ``` Clone the node/edge. | Option | Type | Default | Required | Description | |--------------|---------|---------|:--------:|------------------------------------------------------------| | options.deep | boolean | `false` | | Whether to clone descendant nodes and edges, default is `false` which means only cloning itself. | - When `options.deep` is `false`, returns the newly created node/edge by cloning. - When `options.deep` is `true`, returns an object where the Key is the ID of the cloned node/edge, and the Value is the cloned node/edge. #### on(...) ```ts on(name: string, handler: Events.Handler, context?: any): this ``` Listen to events. | Option | Type | Default | Required | Description | |---------|----------------|--------|:----:|----------------------------------| | name | string | | ✓ | Event name. | | handler | Events.Handler | | ✓ | Callback function. | | context | any | | | Calling context of the callback. | #### once(...) ```ts once(name: string, handler: Events.Handler, context?: any): this ``` Listen to an event once, automatically remove the listener after the event is triggered. | Option | Type | Default | Required | Description | |---------|----------------|---------|:--------:|---------------------------------------| | name | string | | ✓ | Event name. | | handler | Events.Handler | | ✓ | Callback function. | | context | any | | | Calling context of callback function. | #### off(...) ```ts /** * Remove all event listeners. */ off(): this /** * Remove all event listeners for the specified name. */ off(name: string): this /** * Remove the event listener corresponding to the specified handler. */ off(name: null, handler: Events.Handler): this /** * Remove the event listener for the specified name and handler. */ off(name: string, handler: Events.Handler, context?: any): this ``` Remove event listeners. #### trigger(...) ```ts trigger(name: string, ...args?: any[]): boolean | Promise<boolean> ``` Trigger an event. | Option | Type | Default | Required | Description | |---------|--------|---------|:--------:|-------------------------------------------| | name | string | | ✓ | Event name. | | ...args | any[] | | | Parameters passed to callback functions. | - When all callback functions are synchronous, it returns `false` if any callback function returns `false`, otherwise returns `true`. - When there are asynchronous functions among the callbacks, it returns `Promise<boolean>` based on the same logic as synchronous callbacks. #### dispose() ```ts dispose(): void ``` Destroy and remove the node/edge from its parent. ### Element Structure markup Specifies the SVG/HTML structure used to render nodes/edges, described in [JSON format](#markup), usually set through the [`config`]() method when defining nodes/edges to be shared by all instances. When modifying `markup`, it will trigger the `change:markup` event and canvas redraw. #### get markup Get `markup`. ```ts const markup = cell.markup ``` #### set markup Set `markup`, and trigger the `change:markup` event and canvas redraw. ```ts cell.markup = markup ``` #### getMarkup() ```ts getMarkup(): Markup ``` Get `markup`. ```ts const markup = cell.getMarkup() ``` #### setMarkup(...) ```ts setMarkup(markup: Markup, options?: Cell.SetOptions): this ``` Set `markup`. By default, it triggers the `change:markup` event and canvas redraw. When `options.silent` is `true`, it does not trigger the `change:markup` event and canvas redraw. | Name | Type | Required | Default | Description | |------------------|-------------------|:--------:|---------|----------------------------------------------------------------------| | markup | [Markup](#markup) | ✓ | | | | options.silent | boolean | | `false` | When `true`, do not trigger `change:markup` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### removeMarkup(...) ```ts removeMarkup(options?: Cell.SetOptions): this ``` Remove `markup`. By default, it triggers the `change:markup` event and canvas redraw. When `options.silent` is `true`, it does not trigger the `change:markup` event and canvas redraw. | Name | Type | Required | Default | Description | |------------------|---------|----------|---------|----------------------------------------------------------------------| | options.silent | boolean | | `false` | When `true`, do not trigger `change:markup` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### Element Attributes attrs The `attrs` property is a [complex object](#attrs-1). When modifying `attrs`, it will trigger the `change:attrs` event and canvas redraw. #### get attrs Get attributes. ```ts const atts = cell.attrs ``` #### set attrs Set attributes, and trigger the `change:attrs` event and canvas redraw. ```ts cell.atts = attrs ``` #### getAttrs() ```ts getAttrs(): Attr.CellAttrs ``` Get attributes. ```ts const atts = cell.getAttrs() ``` #### setAttrs(...) ```ts setAttrs(attrs: Attr.CellAttrs, options?: Cell.SetAttrOptions): this ``` Set attributes. By default, it triggers the `change:attrs` event and canvas redraw. | Name | Type | Required | Default | Description | |-------------------|-------------------------------------|:--------:|---------|--------------------------------------------------------------------------------------------------------------------------| | attrs | Attr.CellAttrs \| null \| undefined | ✓ | | | | options.overwrite | boolean | | `false` | When `true`, replace existing attributes; otherwise, perform deep or shallow merge based on the `options.deep` option. | | options.deep | boolean | | `true` | Effective when `options.overwrite` is `false`. When `true`, perform deep merge; otherwise, perform shallow merge. | | options.silent | boolean | | `false` | When `true`, do not trigger `change:attrs` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | By default, the specified attributes will be [deeply merged](https://lodash.com/docs/4.17.15#merge) with the old attributes: ```ts console.log(cell.getAttrs()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } cell.setAttrs({ body: { fill: '#f5f5f5' }, label: { text: 'My Label' }, }) console.log(cell.getAttrs()) // { // body: { fill: '#f5f5f5' }, // label: { fill: '#333333', text: 'My Label' }, // } ``` When `options.deep` is `false`, perform shallow merge: ```ts console.log(cell.getAttrs()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } cell.setAttrs({ label: { text: 'My Label' } }, { deep: false }) console.log(cell.getAttrs()) // { // body: { fill: '#ffffff' }, // label: { text: 'My Label' }, // } ``` When `options.overwrite` is `true`, directly replace old attributes: ```ts console.log(cell.getAttrs()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } cell.setAttrs({ label: { text: 'My Label' } }, { overwrite: true }) console.log(cell.getAttrs()) // { // label: { text: 'My Label' }, // } ``` #### replaceAttrs(...) ```ts replaceAttrs(attrs: Attr.CellAttrs, options: Cell.SetOptions = {}): this ``` Replace original attributes with given attributes, equivalent to calling `setAttrs(attrs, { ...options, overwrite: true })`. | Name | Type | Required | Default | Description | |------------------|-------------------------------------|:--------:|---------|-------------------------------------------------------------------| | attrs | Attr.CellAttrs \| null \| undefined | ✓ | | | | options.silent | boolean | | `false` | When `true`, do not trigger `change:attrs` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### updateAttrs(...) ```ts updateAttrs(attrs: Attr.CellAttrs, options: Cell.SetOptions = {}): this ``` Update attributes using shallow merge, equivalent to calling `setAttrs(attrs, { ...options, deep: false })`. | Name | Type | Required | Default | Description | |------------------|-------------------------------------|:----:|---------|---------------------------------------------------| | attrs | Attr.CellAttrs \| null \| undefined | ✓ | | | | options.silent | boolean | | `false` | When `true`, it doesn't trigger the `change:attrs` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### removeAttrs(...) ```ts removeAttrs(options?: Cell.SetOptions): this ``` Remove attributes. | Name | Type | Required | Default | Description | |------------------|---------|:--------:|---------|----------------------------------------------------------------------------------| | options.silent | boolean | | `false` | When `true`, it won't trigger the `change:attrs` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### getAttrByPath(...) ```ts getAttrByPath<T>(path?: string | string[]): T ``` Get attribute value by attribute path. | Name | Type | Required | Default | Description | |------|--------------------|:--------:|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| | path | string \| string[] | | | Attribute path. When `path` is of type `string`, the path is a string separated by `\`. When `path` is of type `string[]`, the path is an array of keys on the attribute object path. | The attribute value of a certain node is as follows: ```ts console.log(cell.getAttrs()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } ``` When the path is empty, it returns all attributes: ```ts console.log(cell.getAttrByPath()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } ``` Get attribute value through string path: ```ts console.log(cell.getAttrByPath('body')) // { fill: '#ffffff' } console.log(cell.getAttrByPath('body/fill')) // '#ffffff' console.log(cell.getAttrByPath('unknown')) // undefined console.log(cell.getAttrByPath('body/unknown')) // undefined ``` Get attribute value through a path composed of an array of keys of the attribute object: ```ts console.log(cell.getAttrByPath(['body'])) // { fill: '#ffffff' } console.log(cell.getAttrByPath(['body', 'fill'])) // '#ffffff' console.log(cell.getAttrByPath(['unknown'])) // undefined console.log(cell.getAttrByPath(['body', 'unknown'])) // undefined ``` #### setAttrByPath(...) ```ts setAttrByPath(path: string | string[], value: Attr.ComplexAttrValue, options?: Cell.SetOptions): this ``` Set attribute value by attribute path. | Name | Type | Required | Default | Description | |------------------|-----------------------|:--------:|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| | path | string \| string[] | ✓ | | Attribute path. When `path` is of type `string`, the path is a string separated by `\`. When `path` is of type `string[]`, the path is an array of keys on the attribute object path. | | value | Attr.ComplexAttrValue | ✓ | | New attribute value. | | options.silent | boolean | | `false` | When `true`, it won't trigger the `change:attrs` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | The initial attribute value of a certain node is as follows: ```ts console.log(cell.getAttrs()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } ``` Set attribute value through string path: ```ts cell.setAttrByPath('body', { stroke: '#000000' }) // Replace body attribute value console.log(cell.getAttrs()) // { // body: { stroke: '#000000' }, // label: { fill: '#333333' }, // } cell.setAttrByPath('body/fill', '#f5f5f5') // Set body.fill attribute value console.log(cell.getAttrs()) // { // body: { stroke: '#000000', fill: '#f5f5f5' }, // label: { fill: '#333333' }, // } ``` Or set attribute value through a path composed of an array of keys of the attribute object: ```ts cell.setAttrByPath(['body'], { stroke: '#000000' }) // Replace body attribute value console.log(cell.getAttrs()) // { // body: { stroke: '#000000' }, // label: { fill: '#333333' }, // } cell.setAttrByPath(['body', 'fill'], '#f5f5f5') // Set body.fill attribute value console.log(cell.getAttrs()) // { // body: { stroke: '#000000', fill: '#f5f5f5' }, // label: { fill: '#333333' }, // } ``` #### removeAttrByPath(...) ```ts removeAttrByPath(path: string | string[], options?: Cell.SetOption ): this ``` Remove attribute value at the specified path. | Name | Type | Required | Default | Description | |------------------|--------------------|:--------:|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| | path | string \| string[] | ✓ | | Attribute path. When `path` is of type `string`, the path is a string separated by `\`. When `path` is of type `string[]`, the path is an array of keys on the attribute object path. | | options.silent | boolean | | `false` | When `true`, it won't trigger the `change:attrs` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | The initial attribute value of a certain node is as follows: ```ts console.log(cell.getAttrs()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } ``` Remove attribute value through string path: ```ts cell.removeAttrByPath('body/fill') console.log(cell.getAttrs()) // { // body: { }, // label: { fill: '#333333' }, // } cell.removeAttrByPath('body') console.log(cell.getAttrs()) // { // label: { fill: '#333333' }, // } ``` Or remove attribute value through a path composed of an array of keys of the attribute object: ```ts cell.removeAttrByPath(['body', 'fill']) console.log(cell.getAttrs()) // { // body: { }, // label: { fill: '#333333' }, // } cell.removeAttrByPath(['body']) console.log(cell.getAttrs()) // { // label: { fill: '#333333' }, // } ``` #### attr(...) ```ts /** * Get attributes. */ attr(): Cell.CellAttrs /** * Get attribute value at the specified path. */ attr<T>(path: string | string[]): T /** * Set attribute value at the specified path. */ attr(path: string | string[], value: Attr.ComplexAttrValue | null, options?: Cell.SetOptions): this /** * Set attribute values, the passed attributes are deeply merged with the old attributes. */ attr(attrs: Attr.CellAttrs, options?: Cell.SetOptions): this ``` This method is an integration of [`getAttrByPath`](#getattrbypath), [`setAttrByPath`](#setattrbypath), and [`setAttrs`](#setattrs) methods, providing the above four function signatures, making it a very practical method. Get all attribute values: ```ts console.log(cell.attr()) // { // body: { fill: '#ffffff' }, // label: { fill: '#333333' }, // } ``` Get attribute value at the specified path: ```ts console.log(cell.attr('body/fill')) // '#ffffff' ``` Set attribute value at the specified path: ```ts cell.attr('body/fill', '#f5f5f5') console.log(cell.attr()) // { // body: { fill: '#f5f5f5' }, // label: { fill: '#333333' }, // } ``` Set attribute values through the attribute object, and perform a [deep merge](https://lodash.com/docs/4.17.15#merge) with the existing attribute object. ```ts cell.attr({ body: { stroke: '#000000' }, label: { fill: 'blue', text: 'my label' }, }) console.log(cell.attr()) // { // body: { fill: '#f5f5f5', stroke: '#000000' }, // label: { fill: 'blue', text: 'my label' }, // } ``` ### Hierarchy zIndex `zIndex` is the layer level of a node/edge in the canvas, which is automatically determined based on the order of node/edge addition by default. When modifying `zIndex`, it will trigger the `change:zIndex` event and canvas redraw. #### get zIndex Get the `zIndex`. ```ts const z = cell.zIndex ``` #### set zIndex Set the `zIndex`, triggering the `change:zIndex` event and canvas redraw. ```ts cell.zIndex = 2 ``` #### getZIndex() ```ts getZIndex(): number ``` Get the `zIndex`. ```ts const z = cell.getZIndex() ``` #### setZIndex(...) ```ts setZIndex(zIndex: number, options?: Cell.SetOptions): this ``` Set the `zIndex`. By default, it triggers the `change:zIndex` event and canvas redraw. When `options.silent` is `true`, it doesn't trigger the `change:zIndex` event and canvas redraw. | Name | Type | Required | Default | Description | |------------------|---------|:--------:|---------|----------------------------------------------------------------| | zIndex | number | ✓ | | | | options.silent | boolean | | `false` | When `true`, doesn't trigger `change:zIndex` event and redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### removeZIndex(...) ```ts removeZIndex(options?: Cell.SetOptions): this ``` Remove the `zIndex`. By default, it triggers the `change:zIndex` event and canvas redraw. When `options.silent` is `true`, it doesn't trigger the `change:zIndex` event and canvas redraw. | Name | Type | Required | Default | Description | |------------------|---------|----------|---------|----------------------------------------------------------------| | options.silent | boolean | | `false` | When `true`, doesn't trigger `change:zIndex` event and redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### toFront(...) ```ts toFront(options?: Cell.ToFrontOptions): this ``` Move the node/edge to the topmost layer. | Name | Type | Required | Default | Description | |------------------|---------|----------|---------|----------------------------------------------------------------| | options.deep | boolean | | `false` | When `true`, also updates the hierarchy of all child nodes/edges. | | options.silent | boolean | | `false` | When `true`, doesn't trigger `change:zIndex` event and redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### toBack(...) ```ts toBack(options?: Cell.ToBackOptions): this ``` Move the node/edge to the bottommost layer. | Name | Type | Required | Default | Description | |------------------|---------|----------|---------|----------------------------------------------------------------| | options.deep | boolean | | `false` | When `true`, also updates the hierarchy of all child nodes/edges. | | options.silent | boolean | | `false` | When `true`, doesn't trigger `change:zIndex` event and redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | By default, updating `zIndex` triggers the `change:zIndex` event and canvas redraw: ```ts cell.toBack() ``` When `options.deep` is `true`, it also updates the hierarchy of all child nodes/edges: ```ts cell.toBack({ deep: true }) ``` ### Visibility Visible #### get visible Returns whether the node/edge is visible. ```ts if (cell.visible) { // do something } ``` #### set visible Sets whether the node/edge is visible and triggers the `change:visible` event and canvas redraw. #### show(...) ```ts show(options?: Cell.SetOptions): this ``` Show the node/edge. | Name | Type | Required | Default | Description | |------------------|---------|:--------:|---------|-----------------------------------------------------------------| | options.silent | boolean | | `false` | When `true`, doesn't trigger `change:visible` event and redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### hide(...) ```ts hide(options?: Cell.SetOptions): this ``` Hide the node/edge. | Name | Type | Required | Default | Description | |------------------|---------|:--------:|---------|-----------------------------------------------------------------| | options.silent | boolean | | `false` | When `true`, doesn't trigger `change:visible` event and redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | > **Note** X6 2.x implements element hiding by adding `display: none` to the node label. #### isVisible() ```ts isVisible(): boolean ``` Returns whether the node/edge is visible. #### setVisible(...) ```ts setVisible(visible: boolean, options?: Cell.SetOptions): this ``` Set the visibility of the node/edge. By default, it triggers the `change:visible` event and canvas redraw. When `options.silent` is `true`, it doesn't trigger the `change:visible` event and canvas redraw. | Name | Type | Required | Default | Description | |------------------|---------|:--------:|---------|-----------------------------------------------------------------| | visible | boolean | ✓ | | | | options.silent | boolean | | `false` | When `true`, doesn't trigger `change:visible` event and redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### toggleVisible(...) ```ts toggleVisible(options?: Cell.SetOptions): this ``` Toggle the visibility of the node/edge. By default, it triggers the `change:visible` event and canvas redraw. When `options.silent` is `true`, it doesn't trigger the `change:visible` event and canvas redraw. | Name | Type | Required | Default | Description | |------------------|---------|:--------:|---------|-----------------------------------------------------------------| | options.silent | boolean | | `false` | When `true`, doesn't trigger `change:visible` event and redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### Business Data Business data associated with nodes/edges. For example, in practical use, we often store certain business data on the data property of nodes/edges. ```ts const rect = new Shape.Rect({ x: 40, y: 40, width: 100, height: 40, data: { bizID: 125, date: '20200630', price: 89.0, }, }) ``` #### get data Get the associated data. #### set data Set the associated data and trigger the `change:data` event and canvas redraw. #### getData() ```ts getData(): any ``` Get the associated data. #### setData(...) ```ts setData(data: any, options?: Cell.SetDataOptions): this ``` Set the associated business data. By default, it triggers the `change:data` event and canvas redraw. When `options.silent` is `true`, it doesn't trigger the `change:data` event and canvas redraw. | Name | Type | Required | Default | Description | |-------------------|---------|:--------:|---------|-----------------------------------------------------------------------------------------------| | data | any | ✓ | | | | options.overwrite | boolean | | `false` | When `true`, replaces existing values; otherwise, performs deep or shallow merge based on `options.deep`. | | options.deep | boolean | | `true` | Effective when `options.overwrite` is `false`. When `true`, performs deep merge; otherwise, shallow merge. | | options.silent | boolean | | `false` | When `true`, doesn't trigger `change:data` event and redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | By default, it performs a [deep merge](https://lodash.com/docs/4.17.15#merge) with the original data and triggers the `change:data` event and canvas redraw: ```ts cell.setData(data) ``` When `options.overwrite` is `true`, it replaces the old data: ```ts cell.setData(data, { overwrite: true }) ``` When `options.deep` is `false`, it performs a shallow merge with the original data: ```ts cell.setData(data, { deep: false }) ``` > **Note** The `setData` method uses shallow comparison to determine if the data has been updated, thus deciding whether to trigger node redraw. ```ts const obj = { name: 'x6', star: true } node.setData(obj) // This will trigger a node redraw obj.star = false node.setData(obj) // Note: At this point, no deep comparison is performed. The object is considered unchanged, so it won't trigger a node redraw node.setData({ ...obj, star: false, }) // This will trigger a node redraw ``` #### replaceData(...) ```ts replaceData(data: any, options: Cell.SetOptions = {}): this ``` Replace the original data with the specified data, equivalent to calling `setData(data, { ...options, overwrite: true })`. | Name | Type | Required | Default | Description | |------------------|---------|:--------:|---------|-------------------------------------------------------------------------------| | data | any | ✓ | | | | options.silent | boolean | | `false` | When `true`, does not trigger the `change:data` event and canvas redrawing. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### updateData(...) ```ts updateData(data: any, options: Cell.SetOptions = {}): this ``` Update data using shallow merge, equivalent to calling `setData(data, { ...options, deep: false })`. | Name | Type | Required | Default | Description | |------------------|---------|:--------:|---------|-------------------------------------------------------------------------------| | data | any | ✓ | | | | options.silent | boolean | | `false` | When `true`, does not trigger the `change:data` event and canvas redrawing. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### removeData(...) ```ts removeData(options: Cell.SetOptions): this ``` Remove data. By default, it triggers the `change:data` event and canvas redrawing. When `options.silent` is `true`, it does not trigger the `change:data` event and canvas redrawing. | Name | Type | Required | Default | Description | |------------------|---------|----------|---------|-------------------------------------------------------------------------------| | options.silent | boolean | | `false` | When `true`, does not trigger the `change:data` event and canvas redrawing. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### Parent/Children Relationship #### get parent Get the parent node. #### getParent() ```ts getParent(): Cell | null ``` Get the parent node. Returns the parent node if it exists, otherwise returns `null`. #### setParent(...) ```ts setParent(parent: Cell | null, options?: Cell.SetOptions): this ``` Set the parent node. | Name | Type | Required | Default | Description | |------------------|--------------|:--------:|---------|------------------------------------------------------------------------------------| | parent | Cell \| null | ✓ | | Parent node or `null`. When `parent` is `null`, it removes the parent node. | | options.silent | boolean | | `false` | When `true`, does not trigger the `change:parent` event and canvas redrawing. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### getParentId() ```ts getParentId(): string | undefined ``` Get the ID of the parent node. Returns the parent node's ID if it exists, otherwise returns `undefined`. #### hasParent() ```ts hasParent(): boolean ``` Check if the node/edge has a parent node. #### get children Get all child nodes/edges. #### getChildren() ```ts getChildren(): Cell[] | null ``` Get all child nodes/edges. #### setChildren() ```ts setChildren(children: Cell[] | null, options?: Cell.SetOptions) ``` Set child nodes/edges. | Name | Type | Required | Default | Description | |------------------|----------------|:--------:|---------|----------------------------------------------------------------------------------------| | children | Cell[] \| null | ✓ | | Array of child nodes/edges or `null`. When `children` is `null`, it clears all children. | | options.silent | boolean | | `false` | When `true`, does not trigger the `change:children` event and canvas redrawing. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### isParentOf(...) ```ts isParentOf(child: Cell | null): boolean ``` Returns whether the current node is the parent of the specified Cell. | Name | Type | Required | Default | Description | |-------|--------------|:--------:|---------|-------------| | child | Cell \| null | ✓ | | | #### isChildOf(...) ```ts isChildOf(parent: Cell | null): boolean ``` Returns whether the current node/edge is a child of the specified node. | Name | Type | Required | Default | Description | |--------|--------------|:--------:|---------|-------------| | parent | Cell \| null | ✓ | | | #### eachChild(...) ```ts eachChild(iterator: (child: Cell, index: number, children: Cell[]) => void, context?: any): this ``` Iterate through child nodes. | Name | Type | Required | Default | Description | |----------|--------------------------------------------------------|:--------:|---------|----------------------------------------| | iterator | (child: Cell, index: number, children: Cell[]) => void | ✓ | | Iterator function. | | context | any | | | Execution context of iterator function. | #### filterChild(...) ```ts filterChild(iterator: (child: Cell, index: number, children: Cell[]) => boolean, context?: any): Cell[] ``` Filter child nodes. | Name | Type | Required | Default | Description | |----------|-----------------------------------------------------------|:--------:|---------|----------------------------------------| | iterator | (child: Cell, index: number, children: Cell[]) => boolean | ✓ | | Filter function. | | context | any | | | Execution context of filter function. | #### getChildCount() ```ts getChildCount(): number ``` Get the number of child nodes/edges. #### getChildIndex(...) ```ts getChildIndex(child: Cell): number ``` Get the index of a child node/edge. | Name | Type | Required | Default | Description | |-------|------|:--------:|---------|-------------| | child | Cell | ✓ | | | #### getChildAt(...) ```ts getChildAt(index: number): Cell | null ``` Get the child node/edge at the specified index. | Name | Type | Required | Default | Description | |-------|--------|:--------:|---------|-------------| | index | number | ✓ | | Index position. | #### getAncestors(...) ```ts getAncestors(options?: { deep?: boolean }): Cell[] ``` Get all ancestor nodes. | Name | Type | Required | Default | Description | |--------------|---------|:--------:|---------|-----------------------------------------------------------------------------------------------| | options.deep | boolean | | `true` | By default, recursively gets all ancestor nodes. Set to `false` to only return the parent node. | #### getDescendants(...) ```ts getDescendants(options?: Cell.GetDescendantsOptions): Cell[] ``` Get all descendant nodes. | Name | Type | Required | Default | Description | |----------------------|---------|:--------:|---------|-------------------------------------------------------------------------------------------------| | options.deep | boolean | | `true` | By default, recursively gets all descendant nodes. Set to `false` to only return child nodes/edges. | | options.breadthFirst | boolean | | `false` | By default, uses depth-first algorithm. Set to `true` to use breadth-first search algorithm. | Returns an array of descendant nodes/edges. #### isDescendantOf(...) ```ts isDescendantOf(ancestor: Cell | null, options?: { deep?: boolean }): boolean ``` Returns whether the current node/edge is a descendant of the specified node. | Name | Type | Required | Default | Description | |--------------|--------------|:--------:|---------|------------------------------------------------------------------------------------------------------------| | ancestor | Cell \| null | ✓ | | Specified node. | | options.deep | boolean | | `true` | By default, recursively checks all descendants of the specified node. Set to `false` to only check children. | #### isAncestorOf(...) ```ts isAncestorOf(descendant: Cell | null, options?: { deep?: boolean }): boolean ``` Returns whether the current node is an ancestor of the specified node/edge. | Name | Type | Required | Default | Description | |--------------|--------------|:--------:|---------|------------------------------------------------------------------------------------------------------------| | descendant | Cell \| null | ✓ | | Specified node/edge. | | options.deep | boolean | | `true` | By default, recursively checks all descendants of the specified node. Set to `false` to only check children. | #### getCommonAncestor(...) ```ts getCommonAncestor(...cells: (Cell | null | undefined)[]): Cell | null ``` Get the common ancestor nodes of the given nodes/edges. | Name | Type | Required | Default | Description | |----------|-------------------------------|:--------:|---------|---------------------| | ...cells | (Cell \| null \| undefined)[] | ✓ | | Specified nodes/edges. | #### addChild(...) ```ts addChild(child: Cell, options?: Cell.SetOptions): this ``` Adds the specified node/edge to the end of the current node's children. | Name | Type | Required | Default | Description | |------------------|---------|:--------:|---------|----------------------------------------------------------------------------| | child | Cell | ✓ | | The specified node/edge. | | options.silent | boolean | | `false` | When `true`, does not trigger the `change:children` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### removeChild(...) ```ts removeChild(child: Cell, options?: Cell.RemoveOptions): Cell | null ``` Removes the specified child node/edge. | Name | Type | Required | Default | Description | |------------------|---------|:--------:|---------|-----------------------------------------------------------------------------------------------------------| | child | Cell | ✓ | | The specified node/edge. | | options.deep | boolean | | `true` | By default, recursively removes all child nodes/edges. Set to `false` to only remove the current node/edge. | | options.silent | boolean | | `false` | When `true`, does not trigger the `change:children` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### remove(...) ```ts remove(options?: Cell.RemoveOptions): this ``` First removes the current node/edge from its parent node, then removes it from the canvas. ### Node and Edge Properties The basic options introduced above such as `markup`, `attrs`, `zIndex`, `data`, as well as node options like `size`, `position`, `angle`, `ports`, and edge options like `source`, `target`, `labels`, along with any additional key-value pairs provided when creating nodes/edges, are all called properties. ```ts const rect = new Shape.Rect({ x: 30, y: 30, width: 100, height: 40, attrs: {...}, data: {...}, zIndex: 10, sale: {...}, product: { id: '1234', name: 'apple', price: 3.99, }, }) ``` For example, in the code above, `attrs`, `data`, `zIndex` are standard properties, while `x` and `y` are a pair of [custom options](#custom-options) that are converted to the `position` property during node initialization. Similarly, `width` and `height` are another pair of [custom options](#custom-options) converted to the `size` property during node initialization. The remaining `sale` and `product` objects are non-standard properties. We've introduced some standard properties and methods to operate (get/set) these standard properties above. Now let's introduce a few more general methods that apply to both standard and non-standard properties. #### getProp(...) Gets the value of the specified property. ```ts getProp<T>(key: string, defaultValue?: T): T ``` | Name | Type | Required | Default | Description | |--------------|--------|:--------:|---------|------------------------------------------------------------------| | key | string | ✓ | | Property name. | | defaultValue | T | | - | Default value, returned when the specified property doesn't exist. | ```ts // Get standard properties const zIndex = rect.getProp<number>('zIndex') const position = rect.getProp<{ x: number; y: number }>('position') // Get non-standard properties const product = rect.getProp('product') ``` #### setProp(...) Sets the specified property. By default, it triggers the corresponding `change:xxx` event and canvas redraw. When `options.silent` is `true`, it doesn't trigger. ```ts // Set a specific property setProp(key: string, value: any, options?: Cell.SetOptions): this // Batch set properties, deeply merging the provided properties with the original properties. Note that using this method will trigger propHooks calls setProp(props: Partial<Properties>, options?: Cell.SetOptions): this ``` | Name | Type | Required | Default | Description | |------------------|-----------------------|:--------:|---------|---------------------------------------------------------------------------------------------------------------| | key | string | ✓ | | Property name. | | value | any | ✓ | | Property value. | | props | `Partial<Properties>` | ✓ | | Property key-value pairs, which will be [deeply merged](https://lodash.com/docs/4.17.15#merge) with existing properties. | | options.silent | boolean | | `false` | When `true`, does not trigger the `change:markup` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ```ts // Set a single property: rect.setProp('size', { width: 100, height: 30 }) rect.setProp('zIndex', 10) // Set multiple properties simultaneously rect.setProp({ size: { width: 100, height: 30, }, zIndex: 10, }) ``` #### removeProp(...) Removes the property at the specified path. By default, it triggers the corresponding `change:xxx` event and canvas redraw. When `options.silent` is `true`, it doesn't trigger. ```ts removeProp(path: string | string[], options?: Cell.SetOptions): this ``` | Name | Type | Required | Default | Description | |------------------|--------------------|:--------:|---------|----------------------------------------------------------------------------| | path | string \| string[] | ✓ | | Property path. | | options.silent | boolean | | `false` | When `true`, does not trigger the `change:markup` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ```ts rect.removeProp('zIndex') rect.removeProp('product/id') ``` #### prop(...) This method integrates the above methods and provides four function signatures, making it a very practical method. ```ts prop(): Properties // Get all properties. prop<T>(path: string | string[]): T // Get the property value at the specified path. prop(path: string | string[], value: any, options?: Cell.SetOptions): this // Set the property value at the specified path, deeply merging with existing properties on the path. prop(props: Partial<Properties>, options?: Cell.SetOptions): this // Set properties, deeply merging with existing properties. ``` ```ts // Get properties: rect.prop() rect.prop('zIndex') rect.prop('product/price') // Set properties: rect.prop('zIndex', 10) rect.prop('product/price', 5.99) rect.prop({ product: { id: '234', name: 'banana', price: 3.99, }, }) ``` #### hasChanged(...) ```ts hasChanged(key: string | undefined | null): boolean ``` Returns whether the specified property or all properties have changed. | Name | Type | Required | Default | Description | |------|-----------------------------|:--------:|---------|----------------------------------------------------------------| | key | string \| undefined \| null | | | Property name. When omitted, it checks all properties. | #### previous(...) ```ts previous<T>(name: string): T | undefined ``` After a specified property has changed, get the property value before the change. | Name | Type | Required | Default | Description | |------|--------|:--------:|---------|----------------| | key | string | ✓ | | Property name. | ### Tools #### addTools(...) ```ts addTools( items: Cell.ToolItem | Cell.ToolItem[], options?: Cell.AddToolOptions, ): this addTools( items: Cell.ToolItem | Cell.ToolItem[], name: string, options?: Cell.AddToolOptions, ): this ``` Add tools. | Name | Type | Required | Default | Description | |------------------|----------------------------------|:--------:|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | items | Cell.ToolItem \| Cell.ToolItem[] | | | Tools defined in [NodeTool](/en/api/registry/node-tool#presets) or [EdgeTool](/en/api/registry/edge-tool#presets). | | name | string | | `null` | Define an alias for this group of tools, which can be used as a parameter for `hasTools(name)` | | options.reset | boolean | | `false` | Whether to clear the tool set. By default, tools are appended to the tool set. | | options.local | boolean | | `false` | Whether the tool is rendered in the node/edge container. By default, it's `false`, and all tools are rendered under `x6-graph-svg-decorator`. Only takes effect when `options.reset` is `true` | | options.silent | boolean | | `false` | When `true`, does not trigger the `change:tools` event and tool redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### getTools() ```ts getTools(): Cell.Tools | null ``` Get the tool set. #### removeTools(...) ```ts removeTools(options?: Cell.SetOptions): this ``` Remove all tools. | Name | Type | Required | Default | Description | |------------------|---------|:--------:|---------|-------------------------------------------------------------------------| | options.silent | boolean | | `false` | When `true`, does not trigger the `change:tools` event and tool redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### hasTool(...) ```ts hasTool(name: string): boolean ``` Check if a tool with the specified name exists. #### removeTool(...) ```ts removeTool(name: string, options?: Cell.SetOptions): this removeTool(index: number, options?: Cell.SetOptions): this ``` Removes a tool with the specified name. | Name | Type | Required | Default | Description | |------------------|------------------|:--------:|---------|----------------------------------------------------------------------------------| | nameOrIndex | string \| number | ✓ | | The name or index of the tool. | | options.silent | boolean | | `false` | If `true`, doesn't trigger the `change:tools` event and tool redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ## config(...) ```ts config<C extends Cell.Config = Cell.Config>(presets: C): void ``` Sets the default values for node/edge options. | Name | Type | Required | Default | Description | |-------------------|------------------------|:--------:|---------|-------------------------------------------------------------------------------------------------------| | options.propHooks | Cell.PropHooks\<M, C\> | | | Custom options. | | options.attrHooks | Attr.Definitions | | | Custom attribute key-value pairs. Key is the name of the custom attribute, Value is the custom attribute object (including methods for attribute checking, applying attributes, etc.). | | options...others | object | | | Other options, properties of nodes/edges. | ### Default Option Values This method is very friendly for custom nodes/edges, making it convenient for us to set some preset options for our nodes/edges. For example, when defining a rectangle node, we specified the default Markup, default size, and default style for it. ```ts Shape.Rect.config({ width: 80, height: 40, markup: ..., attrs: ..., }) ``` Our code for creating rectangles can be very simple: ```ts const rect = graph.addNode({ x: 100, y: 100, attrs: { label: { text: 'rect', }, }, }) ``` Each call to `config(presets)` performs a [deep merge](https://lodash.com/docs/4.17.15#merge) with the current preset values. For example, the following code modifies the default border color of the rectangle to red and the default text color to blue, respectively. The final effect is the combination of both: ```ts // Only modify the default border color Shape.Rect.config({ attrs: { body: { stroke: 'red', }, }, }) // Only modify the default text color Shape.Rect.config({ attrs: { label: { fill: 'blue', // Override the 'red' defined above stroke: '#000', }, }, }) ``` ### Custom Options When creating a rectangle, we can use `label` to set the label text of the rectangle: ```ts const rect = graph.addNode({ x: 100, y: 100, label: 'rect', }) ``` We didn't define the `label` option for the rectangle, so how is this `label` applied to `attrs/label/text`? This is where the `propHooks` hook comes in. We can define `propHooks` hooks to consume these non-standard options. Let's look at the implementation details of the `label` option hook: ```ts Shape.Rect.config({ // Apply 'label' to the 'attrs/text/text' property through the hook propHooks(metadata) { const { label, ...others } = metadata if (label) { ObjectExt.setByPath(others, 'attrs/text/text', label) } return others }, }) ``` Through the `propHooks` hook, we can easily extend some custom options. For example, we can define certain styles as options for the node, which not only reduces nesting but also makes the code for creating nodes more semantic. Look at the following code, which defines custom `rx` and `ry` options for rectangles: ```ts 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: ```ts const rect = graph.addNode({ x: 100, y: 100, rx: 5, ry: 10, label: 'rect', }) ``` ### Custom Attributes Custom attributes refer to non-standard SVG/HTML attributes, such as built-in system attributes like `refWidth`, `refHeight`, `sourceMarker`, `targetMarker`, etc. These attributes are globally shared, and we can use the `attrHooks` hook to define **exclusive** custom attributes for nodes/edges. For example: ```ts import { Shape, Color } from '@antv/x6' Shape.Rect.config({ attrHooks: { fill: { set(val) { return Color.invert(val) // Automatically invert the fill color }, }, theme: { set(val) { // Set both fill color and border color simultaneously return { fill: val, stroke: Color.invert(val), } }, }, }, }) ``` We can use the `fill` and `theme` attributes defined above like this: ```ts const rect = graph.addNode({ x: 100, y: 100, rx: 5, ry: 10, label: 'rect', attrs: { body: { theme: '#f5f5f5', }, label: { fill: '#fff', }, }, }) ``` ## Static Methods ### generateId We provide a static method `generateId` on the Cell base class to quickly implement custom node/edge IDs. ```ts generateId(metadata: Cell.Metadata): string ``` For example: ```ts import { Cell } from '@antv/x6' let sid = 0 Cell.generateId = ({ shape }) => { return `${shape}_${sid++}` } ``` #### Node - Canonical page: https://x6.antv.antgroup.com/en/api/model/node/ - Markdown source: https://x6.antv.antgroup.com/en/api/model/node.md - Description: Node --- title: "Node" description: "Node" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/model/node/" version: "3.x" --- Node is the base class for all nodes, inheriting from [Cell](/en/api/model/cell), and defines the common properties and methods for nodes. ## Attributes In addition to inheriting from Cell [attributes](/en/api/model/cell#properties), the following attributes are also supported. | Option | Type | Default Value | Required | Description | | --- | --- | --- | :-: | --- | | size | `{ width: number; height: number }` | `{ width: 1, height: 1 }` | | Node size. | | position | `{ x: number; y: number }` | - | | Node position. | | angle | number | - | | Node rotation angle. | | ports | object | - | | Connection ports. | | portMarkup | Markup | object | | DOM structure of the connection ports. | | portLabelMarkup | Markup | object | | DOM structure of the connection port labels. | ### Size The size of the node, which is an object containing `width` and `height` properties, can be retrieved and set using the [`size(...)`](#size-1) method. ### Position The position of the node, which is an object containing `x` and `y` properties, can be retrieved and set using the [`position(...)`](#position-1) method. ### Angle The rotation angle of the node, with the rotation center being the center of the node, can be retrieved and set using the [`rotate(...)`](#rotate) method. ### Ports Connection ports are fixed connection points on the node. Many graph applications have connection ports, and some applications also categorize them into input and output ports. The `ports` option is a complex object that can be used as follows: ```ts const node = new Node({ ports: { group: { ... }, // Connection port group definition items: [ ... ], // Connection ports } }) ``` Or ```ts const node = new Node({ ports: [ ... ], // Connection ports }) ``` Typically, we group connection ports with the same behavior and appearance into the same group and set the grouping through the `group` option, which is an object `{ [groupName: string]: PortGroupMetadata }`, where the group name is the key and the value is the default options for each group of connection ports. The supported options are as follows: ```ts interface PortGroupMetadata { /** * Connection port DOM structure definition. */ markup?: Markup /** * Attributes and styles. */ attrs?: Attr.CellAttrs /** * DOM hierarchy of the connection ports, the higher the value, the higher the hierarchy. */ zIndex?: number | 'auto' /** * Layout of the connection ports in the group. */ position?: | [number, number] // Absolute positioning | string // Name of the connection port layout method | { // Name and parameters of the connection port layout method name: string args?: object } /** * Connection port label. */ label?: { markup?: Markup position?: { // Layout of the connection port label name: string // Layout name args?: object // Layout parameters } } } ``` For example: ```ts const node = new Node({ ports: { group: { group1: { markup: { tagName: 'circle' }, attrs: { }, zIndex: 1, position: { name: 'top', args: {}, }, }, group2: { ... }, group3: { ... }, }, items: [ ... ], } }) ``` Another option, `items`, is an array `PortMetadata[]`, where each item represents a connection port. The supported options for connection ports are as follows: ```ts interface PortMetadata { /** * Unique ID of the connection port, automatically generated by default. */ id?: string /** * Group name, specifying a group will inherit the connection port options from the group. */ group?: string /** * Provides parameters for the layout algorithm specified in the group for the designated connection port. * We cannot specify a layout algorithm for a single connection port, but we can provide different parameters for the layout algorithm specified in the group. */ args?: object /** * DOM element and structure definition of the connection port. Specifying this option will override the default options provided by the group. */ markup?: Markup /** * Element attribute styles. Specifying this option will override the default options provided by the group. */ attrs?: Attr.CellAttrs /** * DOM hierarchy of the connection port, the higher the value, the higher the hierarchy. Specifying this option will override the default options provided by the group. */ zIndex?: number | 'auto' /** * Connection port label. Specifying this option will override the default options provided by the group. */ label?: { markup?: Markup position?: { // Layout of the connection port label name: string // Layout name args?: object // Layout parameters } } } ``` For example: ```ts const node = new Node({ ports: { group: { ... }, items: [ { id: 'port1', group: 'group1', ... }, { id: 'port2', group: 'group1', ... }, { id: 'port3', group: 'group2', ... }, ], } }) ``` For more details, please refer to the [Configure Connection Ports](#connection-ports) documentation. ### Port Markup The DOM structure of the connection ports. When neither `ports.groups` nor `ports.items` specifies `markup` for the corresponding connection port, this default option is used to render the connection port, with the default value being: ```ts { tagName: 'circle', selector: 'circle', attrs: { r: 10, fill: '#fff', stroke: '#000', }, } ``` This indicates that the connection port is rendered as a circle with a radius of `10px`. ### Port Label Markup The DOM structure of the connection port labels. When neither `ports.groups` nor `ports.items` specifies `markup` for the corresponding connection port label, this default option is used to render the connection port label, with the default value being: ```ts { tagName: 'text', selector: 'text', attrs: { fill: '#000000', }, } ``` ## Methods ### General #### isNode() ```ts isNode(): true ``` Determines if it is a node; this method always returns `true`. #### getBBox(...) ```ts getBBox(options: { deep?: boolean }): Rectangle ``` Gets the bounding box of the node. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | options.deep | boolean | | `false` | When `true`, includes the bounding box of all child nodes and edges; defaults to `false`. | ```ts const rect1 = node.getBBox() const rect2 = node.getBBox({ deep: true }) ``` ### Size #### size(...) ```ts /** * Gets the size of the node. */ size(): Size /** * Sets the size of the node. */ size(size: Size, options?: Node.ResizeOptions): this /** * Sets the size of the node. */ size(width: number, height: number, options?: Node.ResizeOptions): this ``` Gets the size of the node. ```ts const size = node.size() console.log(size.width, size.height) ``` The parameters and usage for setting the node size are the same as the [`resize`](#resize) method. #### resize(...) Changes the size of the node. Depending on the rotation angle and `options.direction`, both the position and size of the node may change. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | width | number | | | Node width. | | height | number | | | Node height. | | options.direction | Direction | | `'bottom-right'` | The direction in which to change the size; defaults to fixing the top-left corner and resizing towards the bottom-right. | | options.silent | boolean | | `false` | When `true`, does not trigger `'change:size'` and `'change:position'` events and does not redraw the canvas. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | Supports resizing the node in 8 directions, with the default being `'bottom-right'`, which means fixing the top-left corner and resizing towards the bottom-right. - top - right - bottom - left - top-left - top-right - bottom-left - bottom-right ```ts node.resize(100, 40) // Fixing the bottom-right corner, resizing towards the top-left node.resize(100, 40, { direction: 'top-left' }) // Do not trigger events and redraw node.resize(100, 40, { silent: true }) ``` #### scale(...) ```ts scale( sx: number, sy: number, origin?: Point.PointLike, options?: Node.SetOptions, ): this ``` Scales the node. Depending on the scaling center and scaling ratio, both the size and position of the node may change. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | sx | number | ✓ | | Scaling ratio in the X direction. | | sy | number | ✓ | | Scaling ratio in the Y direction. | | origin | Point.PointLike \| null | | - | Scaling center, defaults to the center of the node. | | options.silent | boolean | | `false` | When `true`, does not trigger `'change:size'` and `'change:position'` events and does not redraw the canvas. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ```ts node.scale(1.5, 1.5) // Custom scaling center node.scale(1.5, 1.5, { x: 100, y: 30 }) // Do not trigger events and redraw node.scale(1.5, 1.5, null, { silent: true }) ``` #### fit(...) ```ts fit(options?: Node.FitEmbedsOptions): this ``` Automatically adjusts the size and position of the node based on the size and position of child nodes and edges, ensuring that all child nodes and edges are within the bounding box of the node. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | options.padding | number \| `{ top: number; right: number; bottom: number; left: number }` | | `0` | Padding. | | options.deep | boolean | | `false` | Whether to include all descendant nodes and edges; defaults to only including direct child nodes and edges. | | options.silent | boolean | | `false` | When `true`, does not trigger events and redraw the canvas. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ```ts node.fit() node.fit({ padding: 10 }) node.fit({ padding: { top: 20, bottom: 20, left: 10, right: 10 } }) ``` ### Position #### position(...) ```ts /** * Gets the position of the node. */ position(options?: Node.GetPositionOptions): Point.PointLike /** * Sets the position of the node. */ position(x: number, y: number, options?: Node.SetPositionOptions): this ``` Gets the position of the node. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | options.relative | boolean | | `false` | Whether to return the relative position to the parent node; defaults to `false`, indicating that the absolute position relative to the canvas is returned. | ```ts const pos = rect.position() console.log(pos.x, pos.y) const relativePos = child.position({ relative: true }) console.log(relativePos.x, relativePos.y) ``` Sets the position of the node. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | x | number | ✓ | | Absolute or relative X-axis coordinate of the node. | | y | number | ✓ | | Absolute or relative Y-axis coordinate of the node. | | options.relative | boolean | | `false` | Whether the provided coordinates are relative coordinates. When `true`, indicates that the provided coordinates are relative to the parent node; defaults to `false`, indicating that the provided coordinates are absolute coordinates relative to the canvas. | | options.deep | boolean | | `false` | Whether to also change the position of child nodes/edges. | | options.silent | boolean | | `false` | When `true`, does not trigger `'change:position'` events and does not redraw the canvas. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | By default, absolute coordinates are used. When `options.relative` is `true`, relative coordinates are used. ```ts // Move the node to the canvas position [30, 30] node.position(30, 30) // Move the child node to the relative position [30, 30] at the top-left corner of the parent node child.position(30, 30, { relative: true }) ``` When `options.deep` is `true`, child nodes and edges will also be moved simultaneously. ```ts parent.position(30, 30, { deep: true }) ``` When `options.silent` is `true`, it does not trigger `'change:position'` events and does not redraw the canvas. ```ts node.position(30, 30, { silent: true }) ``` Other custom key-value pairs can be used in event callbacks. ```ts node.position(30, 30, { otherKey: 'otherValue', ... }) ``` #### translate(...) ```ts translate(tx?: number, ty?: number, options?: Node.TranslateOptions): this ``` Translates the node along with its child nodes and edges. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | tx | number | | `0` | The offset of the node in the X direction. | | ty | number | | `0` | The offset of the node in the Y direction. | | options.restrict | Rectangle.RectangleLike | | `undefined` | Restricts the movable range of the node to the specified rectangular area. | | options.transition | boolean \| KeyframeEffectOptions | | `false` | Whether to use animation or specify an animation option. | | options.silent | boolean | | `false` | When `true`, does not trigger `'change:position'` events and does not redraw the canvas. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | When the specified `tx` and `ty` are `undefined`, it indicates that the corresponding direction's translation amount is `0`. ```ts node.translate(30, 30) node.translate(30) // Move only in the X direction node.translate(undefined, 30) // Move only in the Y direction ``` We can restrict the movement of the node within a specified rectangle `{x: number; y: number; width: number; height: number}` using the `options.restrict` option. For example, we can restrict the movement of child nodes within the bounding box of the parent node: ```ts child.translate(30, 30, { restrict: child.getParent().getBBox(), }) ``` When `options.transition` is `true` or an animation option is specified, it indicates that animation should be used to translate the node. ```ts // Translate the node using the default animation node.translate(30, 30, { transition: true, }) // Custom animation options node.translate(30, 30, { transition: { duration: 2000, }, }) ``` When `options.silent` is true, it does not trigger `'change:position'` events and does not redraw the canvas. ```ts node.translate(30, 30, { silent: true }) ``` Other custom key-value pairs can be used in event callbacks. ```ts node.translate(30, 30, { otherKey: 'otherValue', ... }) ``` ### Rotation Angle #### getAngle() ```ts getAngle(): number ``` Gets the rotation angle of the node. ```ts if (node.getAngle() !== 0) { // do something } ``` #### rotate(...) ```ts rotate( deg: number, absolute?: boolean, origin?: Point.PointLike, options?: Node.RotateOptions, ): this ``` Rotates the node. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | deg | number | ✓ | | Rotation degree. | | options.absolute | boolean | | `false` | When `true`, indicates that the given degree is the absolute degree after rotation; defaults to `false`, indicating that the node rotates the given degree based on the current rotation angle. | | options.center | Point.PointLike | | `undefined` | Defaults to rotating around the center of the node; when `options.center` is given, it indicates rotating around the specified center. | | options.silent | boolean | | `false` | When `true`, does not trigger `'change:angle'` events and does not redraw the canvas. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | By default, it is a relative rotation, meaning that the node rotates the given degree based on the current rotation angle. When `options.absolute` is `true`, it indicates absolute rotation, meaning that the given degree is the angle of the node after rotation. ```ts // Relative rotation, the node rotates 30 degrees based on the current rotation angle node.rotate(30) // Absolute rotation, the angle of the node after rotation is 30 degrees node.rotate(30, { absolute: true }) ``` By default, it rotates around the center of the node. You can specify a rotation center using the `options.center` option. ```ts node.rotate(30, { center: { x: 10, y: 10 } }) ``` By default, it triggers `'change:angle'` events and redraws the canvas. When `options.silent` is `true`, it does not trigger `'change:angle'` events and does not redraw the canvas. ```ts node.rotate(30, { silent: true }) ``` Other custom key-value pairs can be used in event callbacks. ```ts node.rotate(30, { otherKey: 'otherValue', ... }) ``` ### Connection Ports Connection ports are fixed connection points on the node. Many graph applications have connection ports, and some applications also categorize them into input and output ports. In the above, we introduced the data structure of connection ports. Here, we will continue to introduce some methods for operating connection ports on the node. #### addPort(...) ```ts addPort(port: PortMetadata, options?: Node.SetOptions): this ``` Adds a single connection port. The connection port is added to the end of the connection port array. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | port | [PortMetadata](#ports) | ✓ | | Connection port. | | options.silent | boolean | | `false` | When `true`, does not trigger `'change:ports'` events and does not redraw the canvas. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### addPorts(...) ```ts addPorts(ports: PortMetadata[], options?: Node.SetOptions) ``` Adds multiple connection ports. The connection ports are added to the end of the connection port array. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | port | [PortMetadata](#ports)[] | ✓ | | Array of connection ports. | | options.silent | boolean | | `false` | When `true`, does not trigger `'change:ports'` events and does not redraw the canvas. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### insertPort(...) ```ts insertPort(index: number, port: PortMetadata, options?: Node.SetOptions): this ``` Adds a connection port at the specified position. Note that the `port` parameter needs to include the `id` property. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | index | number | ✓ | | Position of the connection port. | | port | [PortMetadata](#ports) | ✓ | | Connection port. | | options.silent | boolean | | `false` | When `true`, does not trigger `'change:ports'` events and does not redraw the canvas. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### hasPort(...) ```ts hasPort(portId: string): boolean ``` Checks if the specified connection port exists. | Name | Type | Required | Default Value | Description | | ------ | ------ | :--: | ------ | ----------- | | portId | string | ✓ | | Connection port ID. | ```ts if (node.hasPort('port1')) { // do something } ``` #### hasPorts() ```ts hasPorts(): boolean ``` Checks if the node contains connection ports. ```ts if (node.hasPorts()) { // do something } ``` #### getPort(...) ```ts getPort(portId: string): PortMetadata ``` Gets the connection port by ID. | Name | Type | Required | Default Value | Description | | ------ | ------ | :--: | ------ | ----------- | | portId | string | ✓ | | Connection port ID. | #### getPortAt(...) ```ts getPortAt(index: number): PortMetadata | null ``` Gets the connection port at the specified index. | Name | Type | Required | Default Value | Description | | ----- | ------ | :--: | ------ | ------------ | | index | number | ✓ | | Connection port index. | #### getPorts() ```ts getPorts(): PortMetadata[] ``` Gets all connection ports. #### getPortsByGroup(...) ```ts getPortsByGroup(groupName: string): PortMetadata[] ``` Gets all connection ports under the specified group. | Name | Type | Required | Default Value | Description | | --------- | ------ | :--: | ------ | ---------- | | groupName | string | ✓ | | Group name. | #### removePort(...) ```ts /** * Removes the specified connection port. */ removePort(port: PortMetadata, options?: Node.SetOptions): this /** * Removes the connection port with the specified ID. */ removePort(portId: string, options?: Node.SetOptions): this ``` Removes the specified connection port. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | port | PortMetadata | ✓ | | Connection port. | | options.silent | boolean | | `false` | When `true`, does not trigger `'change:ports'` events and does not redraw the canvas. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | Removes the connection port with the specified ID. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | portId | string | ✓ | | Connection port ID. | | options.silent | boolean | | `false` | When `true`, does not trigger `'change:ports'` events and does not redraw the canvas. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### removePortAt(...) ```ts removePortAt(index: number, options?: Node.SetOptions): this ``` Removes the connection port at the specified index. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | index | number | ✓ | | Connection port index. | | options.silent | boolean | | `false` | When `true`, does not trigger `'change:ports'` events and does not redraw the canvas. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### removePorts(...) ```ts /** * Removes all connection ports. */ removePorts(options?: Node.SetOptions): this /** * Removes the specified connection ports. */ removePorts(ports: (PortMetadata | string)[], options?: Node.SetOptions): this ``` Removes the specified multiple connection ports. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | ports | (PortMetadata \| string)[] | | | Array of connection ports to be removed. | | options.silent | boolean | | `false` | When `true`, does not trigger `'change:ports'` events and does not redraw the canvas. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### getPortIndex(...) ```ts getPortIndex(port: PortMetadata | string): number ``` Obtain the index position of the connection port. #### getPortProp(...) ```ts getPortProp<T>(portId: string, path?: string | string[]): any ``` Retrieves the property value at the specified path of the port. When the path is empty, it returns the complete port. | Name | Type | Required | Default Value | Description | |----------|---------------------|:--------:|---------------|---------------------------------| | portId | string | ✓ | | Port ID. | | path | string \| string[] | | | Property path. When `path` is of type `string`, the path is a string separated by `'/'`. When `path` is of type `string[]`, the path is an array of keys that make up the port object path. | ```ts node.getPortProp('port1') node.getPortProp('port1', 'attrs/circle') node.getPortProp('port1', ['attrs', 'circle']) ``` #### setPortProp(...) ```ts setPortProp( portId: string, path: string | string[], value: any, options?: Node.SetOptions, ): this ``` Sets the property of the port based on the path. | Name | Type | Required | Default Value | Description | |----------|---------------------|:--------:|---------------|---------------------------------| | portId | string | ✓ | | Port ID. | | path | string \| string[] | ✓ | | Property path. When `path` is of type `string`, the path is a string separated by `'/'`. When `path` is of type `string[]`, the path is an array of keys that make up the port object path. | | value | any | ✓ | | Property value. | | options.silent | boolean | | `false` | If `true`, does not trigger the `'change:ports'` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ```ts node.setPortProp('port1', 'attrs/circle', { fill: '#ffffff', stroke: '#000000', }) node.setPortProp('port1', ['attrs', 'circle'], { fill: '#ffffff', stroke: '#000000', }) ``` ```ts setPortProp( portId: string, value: DeepPartial<PortMetadata>, options?: Node.SetOptions, ): this ``` Sets the properties of the port, merging the provided property options with the current values using [deep merge](https://lodash.com/docs/4.17.15#merge). | Name | Type | Required | Default Value | Description | |----------|-------------------------------|:--------:|---------------|---------------------------------| | portId | string | ✓ | | Port ID. | | value | DeepPartial\<PortMetadata\> | ✓ | | Port options. | | options.silent | boolean | | `false` | If `true`, does not trigger the `'change:ports'` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ```ts node.setPortProp('port1', { attrs: { circle: { fill: '#ffffff', stroke: '#000000', }, }, }) ``` #### removePortProp(...) ```ts removePortProp(portId: string, options?: Node.SetOptions): this ``` Removes the options of the specified port. | Name | Type | Required | Default Value | Description | |----------|---------------------|:--------:|---------------|---------------------------------| | portId | string | ✓ | | Port ID. | | options.silent | boolean | | `false` | If `true`, does not trigger the `'change:ports'` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ```ts removePortProp(portId: string, path: string | string[], options?: Node.SetOptions): this ``` Removes the options of the specified port at the specified path. | Name | Type | Required | Default Value | Description | |----------|---------------------|:--------:|---------------|---------------------------------| | portId | string | ✓ | | Port ID. | | path | string \| string[] | ✓ | | Property path. When `path` is of type `string`, the path is a string separated by `'/'`. When `path` is of type `string[]`, the path is an array of keys that make up the port object path. | | options.silent | boolean | | `false` | If `true`, does not trigger the `'change:ports'` event and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### portProp(...) ```ts portProp(portId: string): PortMetadata portProp<T>(portId: string, path: string | string[]): T portProp( portId: string, path: string | string[], value: any, options?: Node.SetOptions, ): this portProp( portId: string, value: DeepPartial<PortMetadata>, options?: Node.SetOptions, ): this ``` This method is a combination of [`getPortProp`](#getportprop) and [`setPortProp`](#setportprop), with parameter options and usage consistent with these two methods. #### Model - Canonical page: https://x6.antv.antgroup.com/en/api/mvc/model/ - Markdown source: https://x6.antv.antgroup.com/en/api/mvc/model.md - Description: Model --- title: "Model" description: "Model" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/mvc/model/" version: "3.x" --- ## Configuration ### model The model corresponding to the canvas, by default creates a new model. ## Methods ### isNode(...) ```ts isNode(cell: Cell): cell is Node ``` Returns whether the specified Cell is a node. | Name | Type | Required | Default | Description | |------|------|:--------:|---------|-------------| | cell | Cell | ✓ | | The specified Cell. | ### isEdge(...) ```ts isEdge(cell: Cell): cell is Edge ``` Returns whether the specified Cell is an edge. | Name | Type | Required | Default | Description | |------|------|:--------:|---------|-------------| | cell | Cell | ✓ | | The specified Cell. | ### createNode(...) ```ts createNode(metadata: Node.Metadata): Node ``` Creates a node. | Name | Type | Required | Default | Description | |----------|---------------|:--------:|---------|-------------| | metadata | Node.Metadata | ✓ | | Node metadata. | ### addNode(...) ```ts addNode(metadata: Node.Metadata, options?: AddOptions): Node addNode(node: Node, options?: AddOptions): Node ``` Adds a node to the canvas, returns the added node. | Name | Type | Required | Default | Description | |------------------|-----------------------|:--------:|---------|-------------| | node | Node.Metadata \| Node | ✓ | | Node metadata or node instance. | | options.silent | boolean | | `false` | If `true`, does not trigger `'node:added'` and `'cell:added'` events and canvas redraw. | | options.sort | boolean | | `true` | Whether to sort by `zIndex`. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### addNodes(...) ```ts addNodes(nodes: (Node.Metadata | Node)[], options?: AddOptions): Graph ``` Adds multiple nodes to the canvas, returns the graph. When adding nodes in batch, it's recommended to use this method for better performance compared to multiple addNode calls. | Name | Type | Required | Default | Description | |------------------|---------------------------|:--------:|---------|-------------| | nodes | (Node.Metadata \| Node)[] | ✓ | | Array of node metadata or node instances. | | options.silent | boolean | | `false` | If `true`, does not trigger `'node:added'` and `'cell:added'` events and canvas redraw. | | options.sort | boolean | | `true` | Whether to sort by `zIndex`. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### removeNode(...) ```ts removeNode(nodeId: string, options?: RemoveOptions): Node | null removeNode(node: Node, options?: RemoveOptions): Node | null ``` Removes a node, returns the removed node. | Name | Type | Required | Default | Description | |------------------|----------------|:--------:|---------|-------------| | node | string \| Node | ✓ | | Node ID or node instance. | | options.silent | boolean | | `false` | If `true`, does not trigger `'node:removed'` and `'cell:removed'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### createEdge(...) ```ts createEdge(metadata: Edge.Metadata): Edge ``` Creates an edge. | Name | Type | Required | Default | Description | |----------|---------------|:--------:|---------|-------------| | metadata | Edge.Metadata | ✓ | | Edge metadata. | ### addEdge(...) ```ts addEdge(metadata: Edge.Metadata, options?: AddOptions): Edge addEdge(edge:Edge, options?: AddOptions): Edge ``` Adds an edge to the canvas, returns the added edge. | Name | Type | Required | Default | Description | |------------------|-----------------------|:--------:|---------|-------------| | edge | Edge.Metadata \| Edge | ✓ | | Edge metadata or edge instance. | | options.silent | boolean | | `false` | If `true`, does not trigger `'edge:added'` and `'cell:added'` events and canvas redraw. | | options.sort | boolean | | `true` | Whether to sort by `zIndex`. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### addEdges(...) ```ts addEdges(edges: (Edge.Metadata | Edge)[], options?: AddOptions): Graph ``` Adds multiple edges to the canvas, returns the graph. | Name | Type | Required | Default | Description | |------------------|---------------------------|:--------:|---------|-------------| | edges | (Edge.Metadata \| Edge)[] | ✓ | | Array of edge metadata or edge instances. | | options.silent | boolean | | `false` | If `true`, does not trigger `'edge:added'` and `'cell:added'` events and canvas redraw. | | options.sort | boolean | | `true` | Whether to sort by `zIndex`. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### removeEdge(...) ```ts removeEdge(edgeId: string, options?: RemoveOptions): Edge | null removeEdge(edge: Edge, options?: RemoveOptions): Edge | null ``` Removes an edge, returns the removed edge. | Name | Type | Required | Default | Description | |------------------|----------------|:--------:|---------|-------------| | edge | string \| Edge | ✓ | | Edge ID or edge instance. | | options.silent | boolean | | `false` | If `true`, does not trigger `'edge:removed'` and `'cell:removed'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### addCell(...) ```ts addCell(cell: Cell | Cell[], options?: AddOptions): this ``` Adds a node or edge to the canvas. | Name | Type | Required | Default | Description | |------------------|----------------|:--------:|---------|-------------| | cell | Cell \| Cell[] | ✓ | | Node instance or edge instance, supports passing an array to add multiple nodes or edges at once. | | options.silent | boolean | | `false` | If `true`, does not trigger `'cell:added'`, `'node:added'`, and `'edge:added'` events and canvas redraw. | | options.sort | boolean | | `true` | Whether to sort by `zIndex`. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### removeCell(...) ```ts removeCell(cellId: string, options?: RemoveOptions): Cell | null removeCell(cell: Cell, options?: RemoveOptions): Cell | null ``` Removes a node or edge, returns the removed node or edge. | Name | Type | Required | Default | Description | |------------------|----------------|:--------:|---------|-------------| | cell | string \| Cell | ✓ | | Node/edge ID or node/edge instance. | | options.silent | boolean | | `false` | If `true`, does not trigger `'cell:removed'`, `'node:removed'`, and `'edge:removed'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### removeCells(...) ```ts removeCells(cells: (Cell | string)[], options?: RemoveOptions): Cell[] ``` Removes multiple nodes/edges, returns an array of removed nodes or edges. | Name | Type | Required | Default | Description | |------------------|--------------------|:--------:|---------|-------------| | cell | (string \| Cell)[] | ✓ | | Array of node/edge IDs or node/edge instances. | | options.silent | boolean | | `false` | If `true`, does not trigger `'cell:removed'`, `'node:removed'`, and `'edge:removed'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### removeConnectedEdges(...) ```ts removeConnectedEdges(cell: Cell | string, options?: RemoveOptions): Edge[] ``` Removes edges connected to the node/edge, returns an array of removed edges. | Name | Type | Required | Default | Description | |------------------|----------------|:--------:|---------|-------------| | cell | string \| Cell | ✓ | | Node/edge ID or node/edge. | | options.silent | boolean | | `false` | If `true`, does not trigger `'cell:removed'` and `'edge:removed'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### disconnectConnectedEdges(...) ```ts disconnectConnectedEdges(cell: Cell | string, options?: Edge.SetOptions): this ``` Sets the source and target of edges connected to the node/edge to the origin `{x: 0, y: 0}`, effectively disconnecting them. | Name | Type | Required | Default | Description | |------------------|----------------|:--------:|---------|-------------| | cell | string \| Cell | ✓ | | Node/edge ID or node/edge. | | options.silent | boolean | | `false` | If `true`, does not trigger `'edge:change:source'` and `'edge:change:target'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### clearCells(...) ```ts clearCells(options?: SetOptions): this ``` Clears the canvas. | Name | Type | Required | Default | Description | |------------------|---------|:--------:|---------|-------------| | options.silent | boolean | | `false` | If `true`, does not trigger `'cell:removed'`, `'node:removed'`, and `'edge:removed'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### resetCells(...) ```ts resetCells(cells: Cell[], options?: SetOptions): this ``` Clears the canvas and adds the specified nodes/edges. | Name | Type | Required | Default | Description | |------------------|---------|:--------:|---------|-------------| | cell | Cell[] | ✓ | | Array of nodes/edges. | | options.silent | boolean | | `false` | If `true`, does not trigger `'cell:added'`, `'node:added'`, `'edge:added'`, `'cell:removed'`, `'node:removed'`, and `'edge:removed'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### hasCell(...) ```ts hasCell(cellId: string): boolean hasCell(cell: Cell): boolean ``` Returns whether the canvas contains the specified node/edge. | Name | Type | Required | Default | Description | |------|----------------|:--------:|---------|-------------| | cell | string \| Cell | ✓ | | Node/edge ID or node/edge. | ### getCellById(...) ```ts getCellById(id: string) ``` Gets a node/edge by its ID. | Name | Type | Required | Default | Description | |------|--------|:--------:|---------|-------------| | id | string | ✓ | | ID of the node/edge. | ### updateCellId(...) ```ts updateCellId(cell: Cell, newId: string) ``` Updates the ID of a node or edge, returns the newly created node/edge. | Name | Type | Required | Default | Description | |-------|--------|:--------:|---------|-------------| | cell | Cell | ✓ | | Node/edge. | | newId | string | ✓ | | New ID. | ### getCells() ```ts getCells(): Cell[] ``` Returns all nodes and edges in the canvas. ### getCellCount() ```ts getCellCount(): number ``` Returns the count of all nodes and edges in the canvas. ### getNodes() ```ts getNodes(): Node[] ``` Returns all nodes in the canvas. ### getEdges() ```ts getEdges(): Edge[] ``` Returns all edges in the canvas. ### getOutgoingEdges(...) ```ts getOutgoingEdges(cell: Cell | string): Edge[] | null ``` Gets the outgoing edges connected to the node/edge, i.e., edges whose source is the specified node/edge. | Name | Type | Required | Default | Description | |------|----------------|:--------:|---------|-------------| | cell | string \| Cell | ✓ | | Node/edge ID or node/edge. | ### getIncomingEdges(...) ```ts getIncomingEdges(cell: Cell | string): Edge[] | null ``` Gets the incoming edges connected to the node/edge, i.e., edges whose target is the specified node/edge. | Name | Type | Required | Default | Description | |------|----------------|:--------:|---------|-------------| | cell | string \| Cell | ✓ | | Node/edge ID or node/edge. | ### getConnectedEdges(...) ```ts getConnectedEdges(cell: Cell | string, options?: GetConnectedEdgesOptions): Edge[] ``` Get the edges connected to a node/edge. | Name | Type | Required | Default | Description | |------------------|----------------|:--------:|---------|---------------------------------------------------------------------------------------------------------------------------| | cell | string \| Cell | ✓ | | Node/Edge ID or Node/Edge. | | options.incoming | boolean | | - | Whether to include incoming edges. By default, returns all incoming and outgoing edges. When `incoming` is `true`, only returns incoming edges. | | options.outgoing | boolean | | - | Whether to include outgoing edges. By default, returns all incoming and outgoing edges. When `outgoing` is `true`, only returns outgoing edges. | | options.deep | boolean | | `false` | Whether to recursively get edges for all child nodes/edges. When `true`, it will also return edges connected to all descendant nodes/edges. | | options.enclosed | boolean | | `false` | Whether to include edges connecting descendant nodes. | | options.indirect | boolean | | `false` | Whether to include indirectly connected edges, i.e., edges connected to input or output edges. | ```ts const edges = graph.getConnectedEdges(node) // Returns incoming and outgoing edges const edges = graph.getConnectedEdges(node, { incoming: true, outgoing: true }) // Returns incoming and outgoing edges const edges = graph.getConnectedEdges(node, { incoming: true }) // Returns incoming edges const edges = graph.getConnectedEdges(node, { incoming: true, outgoing: false, }) // Returns incoming edges const edges = graph.getConnectedEdges(node, { outgoing: true }) // Returns outgoing edges const edges = graph.getConnectedEdges(node, { incoming: false, outgoing: true, }) // Returns outgoing edges const edges = graph.getConnectedEdges(node, { deep: true }) // Returns incoming and outgoing edges, including those connected to all descendant nodes/edges const edges = graph.getConnectedEdges(node, { deep: true, incoming: true }) // Returns incoming edges, including those connected to all descendant nodes/edges const edges = graph.getConnectedEdges(node, { deep: true, enclosed: true }) // Returns incoming and outgoing edges, including edges connecting descendant nodes/edges const edges = graph.getConnectedEdges(node, { indirect: true }) // Returns incoming and outgoing edges, including indirectly connected edges ``` ### getRootNodes() ```ts getRootNodes(): Node[] ``` Get all root nodes, i.e., nodes without incoming edges. ### isRootNode(...) ```ts isRootNode(cell: Cell | string): boolean ``` Returns whether the specified node is a root node. | Name | Type | Required | Default | Description | |------|----------------|:--------:|---------|----------------------------| | cell | string \| Cell | ✓ | | Node/Edge ID or Node/Edge. | ### getLeafNodes() ```ts getLeafNodes(): Node[] ``` Returns all leaf nodes, i.e., nodes without outgoing edges. ### isLeafNode(...) ```ts isLeafNode(cell: Cell | string): boolean ``` Returns whether the specified node is a leaf node. | Name | Type | Required | Default | Description | |------|----------------|:--------:|---------|----------------------------| | cell | string \| Cell | ✓ | | Node/Edge ID or Node/Edge. | ### getNeighbors(...) ```ts getNeighbors(cell: Cell, options?: GetNeighborsOptions): Cell[] ``` Get neighboring nodes. | Name | Type | Required | Default | Description | |------------------|---------|:--------:|---------|----------------------------------------------------------------------------------------------------------------------------------------------| | cell | Cell | ✓ | | Node/Edge. | | options.incoming | boolean | | - | Whether to include incoming neighboring nodes. By default, includes both incoming and outgoing nodes. When `incoming` is `true`, only returns incoming nodes. | | options.outgoing | boolean | | - | Whether to include outgoing neighboring nodes. By default, includes both incoming and outgoing nodes. When `outgoing` is `true`, only returns outgoing nodes. | | options.deep | boolean | | `false` | Whether to recursively get all child nodes/edges. When `true`, it will also return neighboring nodes of all descendant nodes/edges. | | options.indirect | boolean | | `false` | Whether to include indirectly connected neighboring nodes, i.e., neighbors connected through multiple edges (edge-to-edge connections). | ### isNeighbor(...) ```ts isNeighbor(cell1: Cell, cell2: Cell, options?: GetNeighborsOptions): boolean ``` Returns whether `cell2` is a neighbor of `cell1`. The `options` are the same as those in the [`getNeighbors(...)`](#getneighbors) method. ### getPredecessors(...) ```ts getPredecessors(cell: Cell, options?: GetPredecessorsOptions): Cell[] ``` Returns the predecessor nodes of a node, i.e., nodes connected from the root node to the specified node. | Name | Type | Required | Default | Description | |----------------------|-------------------------------------------------------|:--------:|---------|---------------------------------------------------------------------------------------------------------------| | cell | Cell | ✓ | | Node/Edge. | | options.breadthFirst | boolean | | `false` | Whether to use breadth-first search algorithm. By default, uses depth-first search algorithm. | | options.deep | boolean | | `false` | Whether to recursively get all child nodes/edges. When `true`, it will also return predecessors of all descendant nodes/edges. | | options.distance | number \| number[] \| ((distance: number) => boolean) | | - | Get predecessors at a specified distance. The number of edges between nodes is considered as 1 distance unit. | ### isPredecessor(...) ```ts isPredecessor(cell1: Cell, cell2: Cell, options?: GetPredecessorsOptions): boolean ``` Returns whether `cell2` is a predecessor of `cell1`. The `options` are the same as those in the [`getPredecessors(...)`](#getpredecessors) method. ### getSuccessors(...) ```ts getSuccessors(cell: Cell, options?: GetPredecessorsOptions): Cell[] ``` Get all successor nodes, i.e., nodes connected from the specified node to leaf nodes. The `options` are the same as those in the [`getPredecessors(...)`](#getpredecessors) method. ### isSuccessor(...) ```ts isSuccessor(cell1: Cell, cell2: Cell, options?: GetPredecessorsOptions): boolean ``` Returns whether `cell2` is a successor of `cell1`. The `options` are the same as those in the [`getPredecessors(...)`](#getpredecessors) method. ### getCommonAncestor(...) ```ts getCommonAncestor(...cells: {Cell | Cell[])[]): Cell | null ``` Get the common ancestor node of the specified nodes. ### getSubGraph(...) ```ts getSubGraph(cells: Cell[], options?: GetSubgraphOptions): Cell[] ``` Returns a subgraph consisting of the specified nodes and edges. It traverses the given `cells` array, including the source and target nodes when encountering an edge; when encountering a node, it includes the edge if both nodes connected by the edge are in the `cells` array. | Name | Type | Required | Default | Description | |--------------|---------|:--------:|---------|------------------------------------------------| | cells | Cell[] | ✓ | | Array of nodes/edges. | | options.deep | boolean | | `false` | Whether to recursively get all child nodes/edges. | ### cloneCells(...) ```ts cloneCells(cells: Cell[]): { [oldCellId: string]: Cell } ``` Clone cells, returning a key-value pair of old node/edge IDs and cloned nodes/edges. ### cloneSubGraph(...) ```ts cloneSubGraph(cells: Cell[], options?: GetSubgraphOptions): { [oldCellId: string]: Cell } ``` Get and clone a subgraph. The `options` are the same as those in the [`getSubGraph(...)`](#getsubgraph) method. ### getNodesFromPoint(...) ```ts getNodesFromPoint(x: number, y: number): Node[] getNodesFromPoint(p: Point.PointLike): Node[] ``` Returns nodes at the specified position, i.e., nodes whose rectangular area contains the specified point. ### getNodesInArea(...) ```ts getNodesInArea( x: number, y: number, w: number, h: number, options?: Model.GetCellsInAreaOptions, ): Node[] getNodesInArea( rect: Rectangle.RectangleLike, options?: Model.GetCellsInAreaOptions, ): Node[] ``` Returns nodes in the specified rectangular area. When `options.strict` is `true`, it requires the node's rectangular area to be completely contained within the specified rectangle; otherwise, intersection is sufficient. ### getNodesUnderNode(...) ```ts getNodesUnderNode( node: Node, options?: { by?: 'bbox' | Rectangle.KeyPoint }, ): Node[] ``` Returns nodes at the position of the specified node. The `options.by` option specifies the method of retrieval, including: - `null` or `bbox`: Returns nodes that intersect with the specified node's rectangular area - `Rectangle.KeyPoint`: Returns nodes that contain a specific key point of the rectangle, where `Rectangle.KeyPoint` can be one of: - `"center"` - `"origin"` - `"corner"` - `"topLeft"` - `"topCenter"` - `"topRight"` - `"bottomLeft"` - `"bottomCenter"` - `"bottomRight"` - `"leftMiddle"` - `"rightMiddle"` ### searchCell(...) ```ts searchCell(cell: Cell, iterator: SearchIterator, options?: SearchOptions): this ``` Traverse starting from the specified node/edge. | Name | Type | Required | Default | Description | |----------------------|---------------------------------------|:--------:|---------|----------------------------------------------------------------------------------------------------------------------------------------------| | cell | Cell | ✓ | | Node/Edge. | | iterator | (cell: Cell, distance: number) => any | ✓ | | Traversal method. | | options.breadthFirst | boolean | | `false` | Whether to use breadth-first search algorithm. By default, uses depth-first search algorithm. | | options.incoming | boolean | | - | Whether to traverse incoming neighboring nodes. By default, traverses both incoming and outgoing nodes. When `incoming` is `true`, only traverses incoming nodes. | | options.outgoing | boolean | | - | Whether to traverse outgoing neighboring nodes. By default, traverses both incoming and outgoing nodes. When `outgoing` is `true`, only traverses outgoing nodes. | | options.deep | boolean | | `false` | Whether to recursively traverse all child nodes/edges. When `true`, it will also traverse neighboring nodes of all descendant nodes/edges. | | options.indirect | boolean | | `false` | Whether to traverse indirectly connected neighboring nodes, i.e., neighbors connected through multiple edges (edge-to-edge connections). | ### getShortestPath(...) ```ts getShortestPath( source: Cell | string, target: Cell | string, options?: GetShortestPathOptions, ): string[] ``` Get the shortest path between nodes, returning the node IDs on the shortest path. | Name | Type | Required | Default | Description | |------------------|----------------------------------|:--------:|----------------|---------------------------------------------------------------------------------------------------------------| | source | Cell \| string | ✓ | | Start node/edge. | | target | Cell \| string | ✓ | | End node/edge. | | options.directed | boolean | | `false` | Whether to consider directionality. When `true`, the path must follow the direction from start node to end node. | | options.weight | (u: string, v: string) => number | | `(u, v) => 1` | Distance weight algorithm. `u` and `v` are adjacent nodes, default distance is `1`. | ### getAllCellsBBox(...) ```ts getAllCellsBBox(): Rectangle | null ``` Returns the rectangular area of all nodes and edges on the canvas. ### getCellsBBox(...) ```ts getCellsBBox(cells: Cell[], options?: Cell.GetCellsBBoxOptions): Rectangle | null ``` Returns the rectangular area formed by the specified nodes and edges. | Name | Type | Required | Default | Description | |--------------|---------|:--------:|---------|----------------------------------------------------| | cells | Cell[] | ✓ | | Array of nodes and edges. | | options.deep | boolean | | `false` | Whether to include all descendant nodes and edges. | ### toJSON(...) ```ts toJSON(options?: ToJSONOptions): object ``` Export nodes and edges in the graph, returning an object with a `{ cells: [] }` structure, where the `cells` array stores nodes and edges in rendering order. | Name | Type | Required | Default | Description | |--------------|------|:--------:|---------|-------------------------------------------------------------------------------------------------------------------------------| | options.deep | diff | | `false` | Whether to export differential data of nodes and edges (parts that differ from the [default configuration](/en/api/model/cell#default-options) of nodes and edges). | ### parseJSON(...) Convert specified data to nodes and edges. Supports an array of node/edge metadata, returning created nodes and edges in array order. ```ts parseJSON(cells: (Node.Metadata | Edge.Metadata)[]): (Node | Edge)[] ``` Or provide an object containing `cells`, `nodes`, `edges`, creating and returning nodes and edges in the order of `[...cells, ...nodes, ...edges]`. ```ts parseJSON({ cells?: (Node.Metadata | Edge.Metadata)[], nodes?: Node.Metadata[], edges?: Edge.Metadata[], }): (Node | Edge)[] ``` ### fromJSON(...) Render nodes and edges according to the specified JSON data. Supports an array of node/edge metadata, rendering nodes and edges in array order. ```ts fromJSON(data: (Node.Metadata | Edge.Metadata)[], options?: FromJSONOptions): this ``` Or provide an object containing `cells`, `nodes`, `edges`, rendering in the order of `[...cells, ...nodes, ...edges]`. ```ts fromJSON( data: { cells?: (Node.Metadata | Edge.Metadata)[], nodes?: Node.Metadata[], edges?: Edge.Metadata[], }, options?: FromJSONOptions, ): this ``` When `options.silent` is `true`, it does not trigger `cell:added`, `node:added`, and `edge:added` events or canvas redrawing, when `options.diff` is `true`, will incremental updates by `id`. #### Grid - Canonical page: https://x6.antv.antgroup.com/en/api/graph/grid/ - Markdown source: https://x6.antv.antgroup.com/en/api/graph/grid.md - Description: Grid --- title: "Grid" description: "Grid" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/graph/grid/" version: "3.x" --- The grid is the smallest unit of rendering/moving nodes. The default grid size is `10px`. When rendering nodes, it means aligning to the grid with `10` as the minimum unit, such as a node with a position of `{ x: 24, y: 38 }` will be rendered at `{ x: 20, y: 40 }` on the canvas. When moving nodes, it means the minimum distance of each move is `10px`. ## Demo ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Settings } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.graph = new Graph({ container: this.container, grid: { visible: true, }, background: { color: '#F2F7FA', }, mousewheel: { enabled: true, minScale: 0.2, maxScale: 4, }, }) const source = this.graph.addNode({ x: 40, y: 40, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ x: 200, y: 180, width: 100, height: 40, label: 'Grid', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph.centerContent() } onGridChanged = (options: any) => { this.graph.drawGrid(options) } onGridSizeChanged = (size: number) => { this.graph.setGridSize(size) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="grid-app"> <div className="app-side"> <Settings onChange={this.onGridChanged} onGridSizeChange={this.onGridSizeChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Configuration ### size You can set the grid size when creating the canvas through the following configuration. ```ts const graph = new Graph({ grid: 10, }) // Equivalent to const graph = new Graph({ grid: { size: 10, }, }) ``` ### type The grid is invisible by default. You can enable grid drawing when creating the canvas through the following configuration. ```ts const graph = new Graph({ grid: true, // Grid size is 10px and draw the grid }) // Equivalent to const graph = new Graph({ grid: { size: 10, // Grid size is 10px visible: true, // Draw the grid, default is dot type }, }) ``` We have four built-in grid types, which can be specified through the `type` option, with a default value of `dot`. You can also configure the grid style through the `args` option. #### dot (default) Dot grid. ```ts new Graph({ container: this.container, grid: { visible: true, type: 'dot', args: { color: '#a0a0a0', // Grid point color thickness: 1, // Grid point size }, }, }) ``` #### fixedDot Fixed dot grid with a fixed point size. When the canvas zoom ratio is less than `1`, the grid point size scales with the canvas zoom ratio. When the canvas zoom ratio is greater than `1`, the grid point size is the given `thickness` value. ```ts new Graph({ container: this.container, grid: { visible: true, size: 10, type: 'fixedDot', args: { color: '#a0a0a0', // Grid point color thickness: 2, // Grid point size }, }, }) graph.scale(10, 10) ``` #### mesh Mesh grid. ```ts new Graph({ container: this.container, grid: { visible: true, type: 'mesh', args: { color: '#ddd', // Grid line color thickness: 1, // Grid line width }, }, }) ``` #### doubleMesh Double mesh grid. ```ts const graph = new Graph({ grid: { size: 10, visible: true, type: 'doubleMesh', args: [ { color: '#eee', // Main grid line color thickness: 1, // Main grid line width }, { color: '#ddd', // Secondary grid line color thickness: 1, // Secondary grid line width factor: 4, // Main and secondary grid line interval }, ], }, }) ``` ## Methods ### getGridSize() ```ts getGridSize(): number ``` Get the grid size. ### setGridSize() ```ts setGridSize(gridSize: number): this ``` Set the grid size. ### showGrid() ```ts showGrid(): this ``` Show the grid. ### hideGrid() ```ts hideGrid(): this ``` Hide the grid. ### clearGrid() ```ts clearGrid(): this ``` Clear the grid. ### drawGrid(...) ```ts drawGrid(options?: DrawGridOptions): this ``` Redraw the grid. | Name | Type | Required | Default | Description | | --- | --- | :-: | --- | --- | | type | string | | `dot` | Grid type. For details, please refer to [here](/en/api/graph/grid). | | args | object | | - | Grid parameters corresponding to the grid type. | ## Custom Grid Here's an example of registering a red dot grid: ```ts Graph.registerGrid('red-dot', { color: 'red', thickness: 1, markup: 'rect', update(elem, options) { const width = options.thickness * options.sx const height = options.thickness * options.sy Dom.attr(elem, { width, height, rx: width, ry: height, fill: options.color, }) }, }) const graph = new Graph({ grid: { type: 'red-dot', }, }) ``` ### docs/demos/api/grid/playground/index.less ```less .grid-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/grid/playground/settings.tsx ```tsx import React from 'react' import { Input, Select, Slider, Card, Row, Col } from 'antd' export interface Props { onGridSizeChange: (size: number) => void onChange: (res: any) => void } export interface State { type: string size: number color: string thickness: number colorSecond: string thicknessSecond: number factor: number } export class Settings extends React.Component<Props, State> { state: State = { type: 'dot', size: 10, color: '#aaaaaa', thickness: 1, colorSecond: '#888888', thicknessSecond: 3, factor: 4, } notifyChange() { if (this.state.type === 'doubleMesh') { this.props.onChange({ type: this.state.type, args: [ { color: this.state.color, thickness: this.state.thickness, }, { color: this.state.colorSecond, thickness: this.state.thicknessSecond, factor: this.state.factor, }, ], }) } else { this.props.onChange({ type: this.state.type, args: [ { color: this.state.color, thickness: this.state.thickness, }, ], }) } } onTypeChanged = (type: string) => { this.setState({ type }, () => { this.notifyChange() }) } onSizeChanged = (size: number) => { this.setState({ size }, () => { this.props.onGridSizeChange(this.state.size) }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onSecondaryColorChanged = (e: any) => { this.setState({ colorSecond: e.target.value }, () => { this.notifyChange() }) } onThicknessChanged = (thickness: number) => { this.setState({ thickness }, () => { this.notifyChange() }) } onSecondaryThicknessChanged = (thicknessSecond: number) => { this.setState({ thicknessSecond }, () => { this.notifyChange() }) } onFactorChanged = (factor: number) => { this.setState({ factor }, () => { this.notifyChange() }) } render() { return ( <Card title="Grid Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={8}>Grid Type</Col> <Col span={13}> <Select style={{ width: '100%' }} value={this.state.type} onChange={this.onTypeChanged} > <Select.Option value="dot">Dot</Select.Option> <Select.Option value="fixedDot">Fixed Dot</Select.Option> <Select.Option value="mesh">Mesh</Select.Option> <Select.Option value="doubleMesh">Double Mesh</Select.Option> </Select> </Col> </Row> <Row align="middle"> <Col span={8}>Grid Size</Col> <Col span={13}> <Slider min={1} max={20} step={1} value={this.state.size} onChange={this.onSizeChanged} /> </Col> <Col span={1}> <div className="slider-value">{this.state.size}</div> </Col> </Row> {this.state.type === 'doubleMesh' ? ( <React.Fragment> <Row align="middle"> <Col span={8}>Primary Color</Col> <Col span={13}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={8}>Primary Thickness</Col> <Col span={13}> <Slider min={0.5} max={10} step={0.5} value={this.state.thickness} onChange={this.onThicknessChanged} /> </Col> <Col span={1}> <div className="slider-value"> {this.state.thickness.toFixed(1)} </div> </Col> </Row> <Row align="middle"> <Col span={8}>Secondary Color</Col> <Col span={13}> <Input type="color" value={this.state.colorSecond} style={{ width: '100%' }} onChange={this.onSecondaryColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={8}>Secondary Thickness</Col> <Col span={13}> <Slider min={0.5} max={10} step={0.5} value={this.state.thicknessSecond} onChange={this.onSecondaryThicknessChanged} /> </Col> <Col span={1}> <div className="slider-value"> {this.state.thicknessSecond.toFixed(1)} </div> </Col> </Row> <Row align="middle"> <Col span={8}>Scale Factor</Col> <Col span={13}> <Slider min={1} max={10} step={1} value={this.state.factor} onChange={this.onFactorChanged} /> </Col> <Col span={1}> <div className="slider-value">{this.state.factor}</div> </Col> </Row> </React.Fragment> ) : ( <React.Fragment> <Row align="middle"> <Col span={8}>Grid Color</Col> <Col span={13}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={8}>Thickness</Col> <Col span={13}> <Slider min={0.5} max={10} step={0.5} value={this.state.thickness} onChange={this.onThicknessChanged} /> </Col> <Col span={1}> <div className="slider-value"> {this.state.thickness.toFixed(1)} </div> </Col> </Row> </React.Fragment> )} </Card> ) } } ``` #### Edge - Canonical page: https://x6.antv.antgroup.com/en/api/model/edge/ - Markdown source: https://x6.antv.antgroup.com/en/api/model/edge.md - Description: Edge --- title: "Edge" description: "Edge" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/model/edge/" version: "3.x" --- Edge is the base class for edges, inheriting from [Cell](/en/api/model/cell), and defines the common properties and methods for edges. ## Attributes In addition to inheriting from Cell [attributes](/en/api/model/cell#properties), the following attributes are also supported. | Option | Type | Default Value | Required | Description | |-------------|--------------------------|---------------|:--------:|--------------------------------------------------| | source | TerminalData | | | Starting point or source node, connection point information. | | target | TerminalData | | | End point or target node, connection point information. | | vertices | Point.PointLike[] | | | Path points. | | router | RouterData | | | Routing. | | connector | ConnectorData | | | Connector. | | labels | Label[] \| string[] | | | Labels. | | defaultLabel| Label | | | Default label. | ### Source and Target Setting the starting/ending point or source/target node of an edge can be categorized into the following situations: - **Connecting to a point on the canvas** ```ts const edge = graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 180, y: 80 }, }) ``` - **Connecting to nodes/edges** ```ts const edge = graph.addEdge({ source: { cell: 'source-cell-id' }, target: { cell: 'target-cell-id' }, }) ``` - **Connecting to connection points on nodes** ```ts const edge = graph.addEdge({ source: { cell: 'source-cell-id', port: 'port-id' }, target: { cell: 'target-cell-id', port: 'port-id' }, }) ``` - **Connecting to specific elements on nodes** ```ts const edge = graph.addEdge({ source: { cell: 'source-cell-id', selector: 'some-selector' }, target: { cell: 'target-cell-id', selector: 'some-selector' }, }) ``` Additionally, the edge's [anchor points](/en/api/registry/node-anchor) and [connection points](/en/api/registry/connection-point) options together determine the starting and ending points of the edge. - Starting Point: A reference line is drawn from the first path point or the center of the target node (if there are no path points) to the anchor point of the source node. Then, based on the intersection calculation method specified by the connectionPoint, the intersection of the reference line and the shape is calculated, which becomes the starting point of the edge. - Ending Point: A reference line is drawn from the last path point or the center of the source node (if there are no path points) to the anchor point of the target node. Then, based on the intersection calculation method specified by the connectionPoint, the intersection of the reference line and the shape is calculated, which becomes the ending point of the edge. When creating an edge, you can specify anchor points and connection points for `source` and `target` separately. - **Specifying Anchor Points** ```ts const edge = graph.addEdge({ source: { cell: 'source-id', anchor: { name: 'midSide', args: { dx: 10, }, }, }, target: { cell: 'target-id', anchor: 'orth', // Can be simplified when there are no parameters }, }) ``` - **Specifying Connection Points** ```ts const edge = graph.addEdge({ source: { cell: 'source-id', connectionPoint: { name: 'boundary', args: { sticky: true, }, }, }, target: { cell: 'target-id', connectionPoint: 'boundary', // Can be simplified when there are no parameters }, }) ``` ### Vertices The path points `vertices` is an array of points. The edge starts from the starting point, passes through the path points in order, and finally reaches the end point. ```ts const edge = graph.addEdge({ source, target, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], }) ``` ### Router The router `router` will further process the `vertices`, adding additional points if necessary, and return the processed points (excluding the starting and ending points of the edge). For example, after processing with the [`orth`](/en/api/registry/router#orth) router, each link segment of the edge is either horizontal or vertical. We provide the following default routers. | Router Name | Description | |-------------|--------------------------------------------------| | normal | [Default router](/en/api/registry/router#normal), returns the path points as is. | | orth | [Orthogonal router](/en/api/registry/router#orth), consists of horizontal or vertical orthogonal segments. | | oneSide | [Restricted orthogonal router](/en/api/registry/router#oneside), consists of three restricted horizontal or vertical orthogonal segments. | | manhattan | [Smart orthogonal router](/en/api/registry/router#manhattan), consists of horizontal or vertical orthogonal segments and automatically avoids other nodes (obstacles) along the path. | | metro | [Smart subway line router](/en/api/registry/router#metro), consists of horizontal or vertical orthogonal segments and diagonal segments, similar to a subway map, and automatically avoids other nodes (obstacles) along the path. | | er | [Entity-relationship router](/en/api/registry/router#er), consists of `Z` shaped diagonal segments. | You can specify the router name `name` and router parameters `args` like this: ```ts const edge = graph.addEdge({ source, target, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], router: { name: 'orth', args: { padding: 10, }, }, }) ``` When there are no router parameters `args`, it can also be simplified to: ```ts const edge = graph.addEdge({ source, target, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], router: 'orth', }) ``` In addition to the built-in routers mentioned above, we can also create custom routers and register them for use. For more details, please refer to the [Custom Router](/en/api/registry/router#registry) tutorial. ### Connector The connector processes the starting point, the points returned by the router, and the ending point into the `d` attribute of the `<path>` element, which determines the style of the edge rendered on the canvas. We provide the following default connectors. | Connector Name | Description | |----------------|--------------------------------------------------| | normal | [Simple connector](/en/api/registry/connector#normal), connects the starting point, routing points, and ending point with straight lines. | | smooth | [Smooth connector](/en/api/registry/connector#smooth), connects the starting point, routing points, and ending point with cubic Bezier curves. | | rounded | [Rounded connector](/en/api/registry/connector#rounded), connects the starting point, routing points, and ending point with straight lines and uses arcs to link at the segment connections (rounded corners). | | jumpover | [Jump line connector](/en/api/registry/connector#jumpover), connects the starting point, routing points, and ending point with straight lines and uses jump line symbols at the intersections of edges. | You can specify the connector name `name` and connector parameters `args` like this: ```ts const edge = graph.addEdge({ source, target, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], connector: { name: 'rounded', args: { radius: 20, }, }, }) ``` When there are no connector parameters `args`, it can also be simplified to: ```ts const edge = graph.addEdge({ source, target, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], connector: 'rounded', }) ``` In addition to the built-in connectors mentioned above, we can also create custom connectors and register them for use. For more details, please refer to the [Custom Connector](/en/api/registry/connector#registry) tutorial. ### Labels and Default Label Due to the flexibility of label configuration, we provide a separate tutorial to explain how to use labels. For details, please refer to the [Using Labels](/en/api/model/labels) tutorial. ## Methods ### General #### isEdge() ```ts isEdge(): true ``` Determines if it is an edge; this method always returns `true`. #### getBBox() ```ts getBBox(): Rectangle ``` Returns the bounding box of the edge. #### getPolyline() ```ts getPolyline(): Polyline ``` Returns the line segments composed of endpoints and path points. #### hasLoop(...) ```ts hasLoop(options: { deep?: boolean }): boolean ``` Checks if it contains a loop link. | Name | Type | Required | Default Value | Description | |--------------|---------|:--------:|---------------|---------------------------| | options.deep | boolean | | `false` | Whether to perform nested checks. | - When `options.deep` is `false`, it indicates that it is a loop connection only if the starting node and the ending node are the same node. - When `options.deep` is `true`, it indicates that it is a loop connection if the starting node and the ending node are the same node or if there is a parent-child nesting relationship between the starting node and the ending node. ### Link Terminal #### getSource() ```ts getSource(): Edge.TerminalData ``` Gets the starting node/start point information of the edge. #### getSourceCell() ```ts getSourceCell(): Cell | null ``` Gets the starting node/edge of the edge; returns `null` if not connected to a node/edge. #### getSourceNode() ```ts getSourceNode(): Node | null ``` Gets the starting node of the edge; returns `null` if not connected to a node. #### getSourceCellId() ```ts getSourceCellId(): string | null ``` Gets the ID of the starting node/edge of the edge; returns `null` if not connected to a node/edge. #### getSourcePortId() ```ts getSourcePortId(): string | null ``` Gets the ID of the starting connection point; returns `null` if not connected to a connection point. #### getSourcePoint() ```ts getSourcePoint(): Point.PointLike | null ``` Gets the starting point linked to the canvas; returns `null` when the edge is connected to a node/edge. #### setSource(...) ```ts /** * Link to a node. */ setSource( node: Node, args?: Edge.SetCellTerminalArgs, options?: Edge.SetOptions, ): this /** * Link to an edge. */ setSource( edge: Edge, args?: Edge.SetEdgeTerminalArgs, options?: Edge.SetOptions, ): this /** * Link to a point on the canvas. */ setSource( point: Point | Point.PointLike, args?: Edge.SetTerminalCommonArgs, options?: Edge.SetOptions, ): this /** * Set the starting point or starting node/edge of the edge. */ setSource(args: Edge.TerminalData, options?: Edge.SetOptions): this ``` #### getTarget() ```ts getTarget(): Edge.TerminalData ``` Gets the ending node/end point information of the edge. #### getTargetCell() ```ts getTargetCell(): Cell | null ``` Gets the ending node/edge of the edge; returns `null` if not connected to a node/edge. #### getTargetNode() ```ts getTargetNode(): Node | null ``` Gets the ending node of the edge; returns `null` if not connected to a node. #### getTargetCellId() ```ts getTargetCellId(): string | null ``` Gets the ID of the ending node/edge of the edge; returns `null` if not connected to a node/edge. #### getTargetPortId() ```ts getTargetPortId(): string | null ``` Gets the ID of the ending connection point; returns `null` if not connected to a connection point. #### getTargetPoint() ```ts getTargetPoint(): Point.PointLike | null ``` Gets the ending point linked to the canvas; returns `null` when the edge is connected to a node/edge. #### setTarget() ```ts /** * Link to a node. */ setTarget( edge: Node, args?: Edge.SetCellTerminalArgs, options?: Edge.SetOptions, ): this /** * Link to an edge. */ setTarget( edge: Edge, args?: Edge.SetEdgeTerminalArgs, options?: Edge.SetOptions, ): this /** * Link to a point on the canvas. */ setTarget( point: Point | Point.PointLike, args?: Edge.SetTerminalCommonArgs, options?: Edge.SetOptions, ): this /** * Set the ending point or ending node/edge of the edge. */ setTarget(args: Edge.TerminalData, options?: Edge.SetOptions): this ``` #### disconnect(...) ```ts disconnect(options?: Edge.SetOptions) ``` Removes the link information of the edge, setting both the starting and ending points to the origin of the canvas `{ x:0, y:0 }`. | Name | Type | Required | Default Value | Description | |------|------|:--------:|---------------|-------------| | options.silent | boolean | | `false` | If `true`, does not trigger `'change:source'` and `'change:target'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### Path Points Vertice #### getVertices() ```ts getVertices(): Point.PointLike[] ``` Gets the path points; returns an empty array if there are no path points. #### setVertices(...) ```ts setVertices( vertices: Point.PointLike | Point.PointLike[], options?: Edge.SetOptions, ): this ``` Sets the path points. | Name | Type | Required | Default Value | Description | |------|--------------------------|:--------:|---------------|-------------------| | vertices | Point.PointLike \| Point.PointLike[] | ✓ | | Path points. | | options.silent | boolean | | `false` | If `true`, does not trigger `'change:vertices'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### insertVertex(...) ```ts insertVertex( vertice: Point.PointLike, index?: number, options?: Edge.SetOptions, ): this ``` Inserts a path point at the specified position. | Name | Type | Required | Default Value | Description | |------|----------------|:--------:|---------------|-------------------| | vertice | Point.PointLike | ✓ | | Path point. | | index | number | | | Insertion position, defaults to the end of the path point array. | | options.silent | boolean | | `false` | If `true`, does not trigger `'change:vertices'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### appendVertex(...) ```ts appendVertex(vertex: Point.PointLike, options?: Edge.SetOptions): this ``` Inserts a path point at the end of the path point array. | Name | Type | Required | Default Value | Description | |------|----------------|:--------:|---------------|-------------------| | vertex | Point.PointLike | ✓ | | Path point. | | options.silent | boolean | | `false` | If `true`, does not trigger `'change:vertices'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### getVertexAt(...) ```ts getVertexAt(index: number): Point.PointLike | null ``` Gets the path point at the specified index. | Name | Type | Required | Default Value | Description | |-------|--------|:--------:|---------------|-------------------| | index | number | ✓ | | Index position. | #### setVertexAt(...) ```ts setVertexAt( index: number, vertice: Point.PointLike, options?: Edge.SetOptions, ): this ``` Sets the path point at the specified index. | Name | Type | Required | Default Value | Description | |------|----------------|:--------:|---------------|-------------------| | index | number | ✓ | | Index position. | | vertice | Point.PointLike | ✓ | | Path point. | | options.silent | boolean | | `false` | If `true`, does not trigger `'change:vertices'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### removeVertexAt(...) ```ts removeVertexAt(index: number, options?: Edge.SetOptions): this ``` Removes the path point at the specified index. | Name | Type | Required | Default Value | Description | |------|--------|:--------:|---------------|-------------------| | index | number | ✓ | | Index position. | | options.silent | boolean | | `false` | If `true`, does not trigger `'change:vertices'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### Router #### getRouter() ```ts getRouter(): Edge.RouterData ``` Gets the router. #### setRouter(...) ```ts setRouter(name: string, args?: KeyValue, options?: Edge.SetOptions): this setRouter(router: Edge.RouterData, options?: Edge.SetOptions): this ``` Sets the router. | Name | Type | Required | Default Value | Description | |------|----------------|:--------:|---------------|-------------------| | name | string | ✓ | | Router name. | | args | KeyValue | | | Router parameters. | | router | Edge.RouterData | ✓ | | Router. | | options.silent | boolean | | `false` | If `true`, does not trigger `'change:router'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### removeRouter(...) ```ts removeRouter(options?: Edge.SetOptions): this ``` Removes the router. | Name | Type | Required | Default Value | Description | |------|--------|:--------:|---------------|-------------------| | options.silent | boolean | | `false` | If `true`, does not trigger `'change:router'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### Connector #### getConnector() ```ts getConnector(): Edge.ConnectorData ``` Gets the connector. #### setConnector(...) ```ts setConnector(name: string, args?: KeyValue, options?: Edge.SetOptions): this setConnector(connector: Edge.ConnectorData, options?: Edge.SetOptions): this ``` Sets the connector. | Name | Type | Required | Default Value | Description | |------|----------------|:--------:|---------------|-------------------| | name | string | ✓ | | Connector name. | | args | KeyValue | | | Connector parameters. | | connector | Edge.ConnectorData | ✓ | | Connector. | | options.silent | boolean | | `false` | If `true`, does not trigger `'change:connector'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### removeConnector(...) ```ts removeConnector(options?: Edge.SetOptions): this ``` Removes the connector. | Name | Type | Required | Default Value | Description | |------|--------|:--------:|---------------|-------------------| | options.silent | boolean | | `false` | If `true`, does not trigger `'change:connector'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | ### Label #### getDefaultLabel() ```ts getDefaultLabel(): Edge.Label ``` Gets the default label. #### getLabels() ```ts getLabels(): Edge.Label[] ``` Gets all labels. #### setLabels(...) ```ts setLabels( labels: Edge.Label | Edge.Label[] | string | string[], options?: Edge.SetOptions, ): this ``` Sets the labels. | Name | Type | Required | Default Value | Description | |------|----------------------------------------|:--------:|---------------|-------------------| | labels | Edge.Label \| Edge.Label[] \| string \| string[] | ✓ | | Labels or array of labels. | | options.silent | boolean | | `false` | If `true`, does not trigger `'change:labels'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### insertLabel(...) ```ts insertLabel( label: Edge.Label | string, index?: number, options?: Edge.SetOptions, ): this ``` Inserts a label at the specified position. | Name | Type | Required | Default Value | Description | |------|--------------------------|:--------:|---------------|-------------------| | label | Edge.Label \| string | ✓ | | Label. | | index | number | | | Insertion position, defaults to the end of the label array. | | options.silent | boolean | | `false` | If `true`, does not trigger `'change:labels'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### appendLabel(...) ```ts appendLabel(label: Edge.Label | string, options?: Edge.SetOptions): this ``` Inserts a label at the end of the label array. | Name | Type | Required | Default Value | Description | |------|--------------------------|:--------:|---------------|-------------------| | label | Edge.Label \| string | ✓ | | Label. | | options.silent | boolean | | `false` | If `true`, does not trigger `'change:labels'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### getLabelAt(...) ```ts getLabelAt(index: number): Edge.Label | null ``` Gets the label at the specified position. | Name | Type | Required | Default Value | Description | |-------|--------|:--------:|---------------|-------------------| | index | number | ✓ | | Index position. | #### setLabelAt(...) ```ts setLabelAt( index: number, label: Edge.Label | string, options?: Edge.SetOptions, ): this ``` Sets the label at the specified position. | Name | Type | Required | Default Value | Description | |------|--------------------------|:--------:|---------------|-------------------| | index | number | ✓ | | Index position. | | label | Edge.Label \| string | ✓ | | Label. | | options.silent | boolean | | `false` | If `true`, does not trigger `'change:labels'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### removeLabelAt(...) ```ts removeLabelAt(index: number, options?: Edge.SetOptions): this ``` Removes the label at the specified position. | Name | Type | Required | Default Value | Description | |------|--------|:--------:|---------------|-------------------| | index | number | ✓ | | Index position. | | options.silent | boolean | | `false` | If `true`, does not trigger `'change:labels'` events and canvas redraw. | | options...others | object | | | Other custom key-value pairs that can be used in event callbacks. | #### Background - Canonical page: https://x6.antv.antgroup.com/en/api/graph/background/ - Markdown source: https://x6.antv.antgroup.com/en/api/graph/background.md - Description: Background --- title: "Background" description: "Background" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/graph/background/" version: "3.x" --- The background is used to specify the background color or background image of the canvas, supporting [watermark background](#repeat). The background layer is at the bottom of the DOM layer. ## Demo ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Settings } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA' }, }) const source = this.graph.addNode({ x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ x: 160, y: 180, width: 100, height: 40, label: 'Grid', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph.centerContent() } onBackgroundChanged = (options: Graph.BackgroundManager.Options) => { this.graph.drawBackground(options) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="background-app"> <div className="app-side"> <Settings onChange={this.onBackgroundChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Configuration ### color The background color, supporting all [CSS background-color](https://developer.mozilla.org/en-US/docs/Web/CSS/background-color) property values, such as: - `red` - `#f5f5f5` - `rgba(255, 255, 128, 0.5)` - `hsla(50, 33%, 25%, 0.75)` - `radial-gradient(ellipse at center, red, green)` ### image The URL address of the background image. The default value is `undefined`, indicating no background image. ### position The position of the background image, supporting all [CSS background-position](https://developer.mozilla.org/en-US/docs/Web/CSS/background-position) property values, with a default value of `'center'`. ### size The size of the background image, supporting all [CSS background-size](https://developer.mozilla.org/en-US/docs/Web/CSS/background-size) property values, with a default value of `'auto auto'`. ### repeat The repeat mode of the background image, supporting all [CSS background-repeat](https://developer.mozilla.org/en-US/docs/Web/CSS/background-repeat) property values, with a default value of `'no-repeat'`. Additionally, the following predefined values are supported: - `watermark`: Watermark effect. - `flip-x`: Flip the background image horizontally. - `flip-y`: Flip the background image vertically. - `flip-xy`: Flip the background image both horizontally and vertically. ### opacity The opacity of the background, with a value range of `[0, 1]`, and a default value of `1`. ### quality The quality of the background image, with a value range of `[0, 1]`, and a default value of `1`. ### angle The rotation angle of the watermark, only valid when [repeat](#repeat) is `'watermark'`, with a default value of `20`. ## Methods ### drawBackground(...) ```ts drawBackground(options?: Options): this ``` Redraw the background. | Name | Type | Required | Default Value | Description | | ---------------- | ------ | :------: | ------------- | ------------------ | | options.color | string | | - | Background color. | | options.image | string | | - | Background image address. | | options.position | string | | - | Background image position. | | options.size | string | | - | Background image size. | | options.repeat | string | | - | Background image repeat mode. | | options.opacity | string | | - | Background image opacity. | ### updateBackground() ```ts updateBackground(): this ``` Update the background. ### clearBackground() ```ts clearBackground(): this ``` Clear the background. ## Custom Image Repeat Mode In addition to the predefined values supported by [repeat](#repeat), you can also customize the image repeat mode. ```ts function watermark(img, options) { const width = img.width const height = img.height const canvas = document.createElement('canvas') canvas.width = width * 3 canvas.height = height * 3 const ctx = canvas.getContext('2d')! const angle = options.angle != null ? -options.angle : -20 const radians = Angle.toRad(angle) const stepX = canvas.width / 4 const stepY = canvas.height / 4 for (let i = 0; i < 4; i += 1) { for (let j = 0; j < 4; j += 1) { if ((i + j) % 2 > 0) { ctx.setTransform(1, 0, 0, 1, (2 * i - 1) * stepX, (2 * j - 1) * stepY) ctx.rotate(radians) ctx.drawImage(img, -width / 2, -height / 2, width, height) } } } return canvas } Graph.registerBackground('watermark', watermark) ``` ### docs/demos/api/background/playground/index.less ```less .background-app { display: flex; height: 388px; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; user-select: none; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/background/playground/settings.tsx ```tsx import React from 'react' import { Graph } from '@antv/x6' import { Input, Checkbox, Select, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (res: Graph.BackgroundManager.Options) => void } export interface State { color: string showImage: boolean repeat: string position: string size: string opacity: number angle?: number } const bgImageDataURL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAYAAADDhn8LAAAKQWlDQ1BJQ0MgUHJvZmlsZQAASA2dlndUU9kWh8+9N73QEiIgJfQaegkg0jtIFQRRiUmAUAKGhCZ2RAVGFBEpVmRUwAFHhyJjRRQLg4Ji1wnyEFDGwVFEReXdjGsJ7601896a/cdZ39nnt9fZZ+9917oAUPyCBMJ0WAGANKFYFO7rwVwSE8vE9wIYEAEOWAHA4WZmBEf4RALU/L09mZmoSMaz9u4ugGS72yy/UCZz1v9/kSI3QyQGAApF1TY8fiYX5QKUU7PFGTL/BMr0lSkyhjEyFqEJoqwi48SvbPan5iu7yZiXJuShGlnOGbw0noy7UN6aJeGjjAShXJgl4GejfAdlvVRJmgDl9yjT0/icTAAwFJlfzOcmoWyJMkUUGe6J8gIACJTEObxyDov5OWieAHimZ+SKBIlJYqYR15hp5ejIZvrxs1P5YjErlMNN4Yh4TM/0tAyOMBeAr2+WRQElWW2ZaJHtrRzt7VnW5mj5v9nfHn5T/T3IevtV8Sbsz55BjJ5Z32zsrC+9FgD2JFqbHbO+lVUAtG0GQOXhrE/vIADyBQC03pzzHoZsXpLE4gwnC4vs7GxzAZ9rLivoN/ufgm/Kv4Y595nL7vtWO6YXP4EjSRUzZUXlpqemS0TMzAwOl89k/fcQ/+PAOWnNycMsnJ/AF/GF6FVR6JQJhIlou4U8gViQLmQKhH/V4X8YNicHGX6daxRodV8AfYU5ULhJB8hvPQBDIwMkbj96An3rWxAxCsi+vGitka9zjzJ6/uf6Hwtcim7hTEEiU+b2DI9kciWiLBmj34RswQISkAd0oAo0gS4wAixgDRyAM3AD3iAAhIBIEAOWAy5IAmlABLJBPtgACkEx2AF2g2pwANSBetAEToI2cAZcBFfADXALDIBHQAqGwUswAd6BaQiC8BAVokGqkBakD5lC1hAbWgh5Q0FQOBQDxUOJkBCSQPnQJqgYKoOqoUNQPfQjdBq6CF2D+qAH0CA0Bv0BfYQRmALTYQ3YALaA2bA7HAhHwsvgRHgVnAcXwNvhSrgWPg63whfhG/AALIVfwpMIQMgIA9FGWAgb8URCkFgkAREha5EipAKpRZqQDqQbuY1IkXHkAwaHoWGYGBbGGeOHWYzhYlZh1mJKMNWYY5hWTBfmNmYQM4H5gqVi1bGmWCesP3YJNhGbjS3EVmCPYFuwl7ED2GHsOxwOx8AZ4hxwfrgYXDJuNa4Etw/XjLuA68MN4SbxeLwq3hTvgg/Bc/BifCG+Cn8cfx7fjx/GvyeQCVoEa4IPIZYgJGwkVBAaCOcI/YQRwjRRgahPdCKGEHnEXGIpsY7YQbxJHCZOkxRJhiQXUiQpmbSBVElqIl0mPSa9IZPJOmRHchhZQF5PriSfIF8lD5I/UJQoJhRPShxFQtlOOUq5QHlAeUOlUg2obtRYqpi6nVpPvUR9Sn0vR5Mzl/OX48mtk6uRa5Xrl3slT5TXl3eXXy6fJ18hf0r+pvy4AlHBQMFTgaOwVqFG4bTCPYVJRZqilWKIYppiiWKD4jXFUSW8koGStxJPqUDpsNIlpSEaQtOledK4tE20Otpl2jAdRzek+9OT6cX0H+i99AllJWVb5SjlHOUa5bPKUgbCMGD4M1IZpYyTjLuMj/M05rnP48/bNq9pXv+8KZX5Km4qfJUilWaVAZWPqkxVb9UU1Z2qbapP1DBqJmphatlq+9Uuq43Pp893ns+dXzT/5PyH6rC6iXq4+mr1w+o96pMamhq+GhkaVRqXNMY1GZpumsma5ZrnNMe0aFoLtQRa5VrntV4wlZnuzFRmJbOLOaGtru2nLdE+pN2rPa1jqLNYZ6NOs84TXZIuWzdBt1y3U3dCT0svWC9fr1HvoT5Rn62fpL9Hv1t/ysDQINpgi0GbwaihiqG/YZ5ho+FjI6qRq9Eqo1qjO8Y4Y7ZxivE+41smsImdSZJJjclNU9jU3lRgus+0zwxr5mgmNKs1u8eisNxZWaxG1qA5wzzIfKN5m/krCz2LWIudFt0WXyztLFMt6ywfWSlZBVhttOqw+sPaxJprXWN9x4Zq42Ozzqbd5rWtqS3fdr/tfTuaXbDdFrtOu8/2DvYi+yb7MQc9h3iHvQ732HR2KLuEfdUR6+jhuM7xjOMHJ3snsdNJp9+dWc4pzg3OowsMF/AX1C0YctFx4bgccpEuZC6MX3hwodRV25XjWuv6zE3Xjed2xG3E3dg92f24+ysPSw+RR4vHlKeT5xrPC16Il69XkVevt5L3Yu9q76c+Oj6JPo0+E752vqt9L/hh/QL9dvrd89fw5/rX+08EOASsCegKpARGBFYHPgsyCRIFdQTDwQHBu4IfL9JfJFzUFgJC/EN2hTwJNQxdFfpzGC4sNKwm7Hm4VXh+eHcELWJFREPEu0iPyNLIR4uNFksWd0bJR8VF1UdNRXtFl0VLl1gsWbPkRoxajCCmPRYfGxV7JHZyqffS3UuH4+ziCuPuLjNclrPs2nK15anLz66QX8FZcSoeGx8d3xD/iRPCqeVMrvRfuXflBNeTu4f7kufGK+eN8V34ZfyRBJeEsoTRRJfEXYljSa5JFUnjAk9BteB1sl/ygeSplJCUoykzqdGpzWmEtPi000IlYYqwK10zPSe9L8M0ozBDuspp1e5VE6JA0ZFMKHNZZruYjv5M9UiMJJslg1kLs2qy3mdHZZ/KUcwR5vTkmuRuyx3J88n7fjVmNXd1Z752/ob8wTXuaw6thdauXNu5Tnddwbrh9b7rj20gbUjZ8MtGy41lG99uit7UUaBRsL5gaLPv5sZCuUJR4b0tzlsObMVsFWzt3WazrWrblyJe0fViy+KK4k8l3JLr31l9V/ndzPaE7b2l9qX7d+B2CHfc3em681iZYlle2dCu4F2t5czyovK3u1fsvlZhW3FgD2mPZI+0MqiyvUqvakfVp+qk6oEaj5rmvep7t+2d2sfb17/fbX/TAY0DxQc+HhQcvH/I91BrrUFtxWHc4azDz+ui6rq/Z39ff0TtSPGRz0eFR6XHwo911TvU1zeoN5Q2wo2SxrHjccdv/eD1Q3sTq+lQM6O5+AQ4ITnx4sf4H++eDDzZeYp9qukn/Z/2ttBailqh1tzWibakNml7THvf6YDTnR3OHS0/m/989Iz2mZqzymdLz5HOFZybOZ93fvJCxoXxi4kXhzpXdD66tOTSna6wrt7LgZevXvG5cqnbvfv8VZerZ645XTt9nX297Yb9jdYeu56WX+x+aem172296XCz/ZbjrY6+BX3n+l37L972un3ljv+dGwOLBvruLr57/17cPel93v3RB6kPXj/Mejj9aP1j7OOiJwpPKp6qP6391fjXZqm99Oyg12DPs4hnj4a4Qy//lfmvT8MFz6nPK0a0RupHrUfPjPmM3Xqx9MXwy4yX0+OFvyn+tveV0auffnf7vWdiycTwa9HrmT9K3qi+OfrW9m3nZOjk03dp76anit6rvj/2gf2h+2P0x5Hp7E/4T5WfjT93fAn88ngmbWbm3/eE8/syOll+AAAH1ElEQVR4Ae2daW/USBCGOxAg4SYh3AJxRERCfOL//4N8JBIJIEDc95EQjnC+vVtWxXjamY5W3lBPS+x43F12++l6XX1ldmJxcfFnIkEAAp0EdnWe5SQEIJAJIBAcAQIFAgikAIcsCCAQfAACBQIIpACHLAggEHwAAgUCCKQAhywIIBB8AAIFAgikAIcsCCAQfAACBQIIpACHLAggEHwAAgUCCKQAhywIIBB8AAIFAgikAIcsCCAQfAACBQIIpACHLAggEHwAAgUCCKQAhywIIBB8AAIFAgikAIcsCCAQfAACBQIIpACHLAggEHwAAgUCCKQAhywIIBB8AAIFAgikAIcsCCAQfAACBQIIpACHLAggEHwAAgUCCKQAhywIIBB8AAIFAgikAIcsCCAQfAACBQIIpACHLAggEHwAAgUCCKQAhywITEZH8O3bt/T8+fP05cuXdPLkyXTgwIHoSHh+RyB8BHn06FF6/PhxevXqVVpZWUk/f/J/xXb+Ef4wvEDW1tYaJ9jY2MiRpDnBQXgC4QVy4sSJxgmOHDmSpqammu8cQCD8GEQCOXz4cPr69Ws6dOgQHgGBTQTCC0Q0FDWIHJv8gi//EgjfxcITIFAiED6CrK+v51ksQdq7d2+6cOHCH7w00/Xp06d8/vTp02lycjI9e/YsvX//Pkee48ePp9nZ2Zz/8uXL9ObNm6Tr7tu3L506dSrNzMz8cU07oes+efIkra6u5ilmlZ+YmEhPnz7NRaanp9O5c+es+KZP2archw8fksqpbrt3787XU0Hd//z585ts/BfV9d27d+njx4+5rMZguv+uXbw3jVN4gWgd5O3bt5nHqG6WHNBmu+REckqtmyjpU0KRuCSKBw8e5PP6j2bF7ty5k+bn59OxY8ea83ag/OXl5Tz+0TmNg3QvObrVSWW6kuqtaWmrh2xVx7Nnzza2pTUdTW3rnyXZS6QSy5UrV7JILS/yJ6+KMVv/4cOH2SkVRXySEEwc7TewFiK7kqKQHNOS3v7fv39Pilh9yYtUZcexlQhNHIpWmpyQvZKEqWuT/iGwuZWh0ktAwrh69Wo6ePBgdiQJRklvejnaxYsXcxfs3r17zZv88+fPnddV98aSumGXLl3K4rt582bvgqWc3JJsL1++nCOZokpfev36dVNE91T3UJFjaWkpn9ei6ZkzZ5oykQ+IIGO2/tzcXBaHzHTsk76rmyYR+bwfP374YvlYgrJxjU7IIRV5NJYojVlUVt0rObQldckUCY4ePdo7Va26aIykpPtZ10/dMd1bSYJWd5H0mxEQxiOgsYYlCUGOacl3u/yx5ftP37XSNcw5VaY0dlB+yVaRrZQkTHXjlCRm3x30dbAxV+laEfIQyJit7B1Kpl4gPs8fd91CUcBSW2iafSqltq2/l3fyrmv4Qb8Xu8r67/4eXdeJco4xyEAtbW9x3d4GyFYV7/B2zn+WbNvX8nY69o6vMdDi4mJTxG/U9OWaAgEPEMhAje6d3EchVWccgYxr23b8rvGR6uAjjb5HTQhkoJb3b3r/5lZ12t/bVfSi6CvbtvXi06zbqNmqPXv2tE1DfkcgAzW7F0j7Le6jS1f1vG27bPtabXs/eaDraOGTNJoAAhnN5j/N8Y7advJ2N6hdEW+rsooiFlXa1+qz9fmauXrx4kU+pelfmwL2ZaIdM4s1UIv7Loyc2raMqDp9axBeIBKHX4j0x12P5meqdB/fRdPaihYJ9a9PaF3X/hvPIZCBWlWO6qdzbfFOXSTbhzWqalq/8AKzlXE5u1+d77KXuGytpH0v7Smz1DddbOX+9k+6WAO2sPr/1qXR/iu90RUB/ELgqOpp1Vy7cZW0d0p2mnnqiz4qr60lthB4//79vKKvCGbikjj279+vouETEWRAF9BfM9qskt7+igTq5tjW+VLVZGvjDtkqAmk3rrbe9yWVMQFoDGM/WmF22l5v17ZzUT8RyIAtLye1zY2qhpyy7+9HrLrajqINijYekdA0ZbuVgbVmrxYWFvIMlheCum2jtubbfaN9TvxeSeV3bgZudUUAbVxs743aSrXathq/3L59O5tKRNeuXSteRuMQ3Vvi8AP4olGgTMYg/4PG1lvcujxbqY6c2sYQihw26JatBGPJum/2vetTZfo2R3bZRTmHQHZgS2vccOvWrabmN27caPZz+elZ6341BTkYmwBjkLGRDW+grpCPDjb7pOhhx6qln0YevtY7swZEkJ3Zbnkwbusfd+/ezdPFmqr1U8SaCiZtjwARZHv8BrPWr5X4xUJN8XpxaKpWP4hH2h4BIsj2+A1mLXFcv349r2Ho79O1UKgulQb7ihxbWQ8ZrPI76MYIZAc1VruqGoR3/Y5Xuxzf6wnQxapnh2UAAggkQCPziPUEEEg9OywDEEAgARqZR6wngEDq2WEZgAACCdDIPGI9AQRSzw7LAAQQSIBG5hHrCSCQenZYBiCAQAI0Mo9YTwCB1LPDMgABBBKgkXnEegIIpJ4dlgEIIJAAjcwj1hNAIPXssAxAAIEEaGQesZ4AAqlnh2UAAggkQCPziPUEEEg9OywDEEAgARqZR6wngEDq2WEZgAACCdDIPGI9AQRSzw7LAAQQSIBG5hHrCSCQenZYBiCAQAI0Mo9YTwCB1LPDMgABBBKgkXnEegIIpJ4dlgEIIJAAjcwj1hP4BWQ+g7ufR9NrAAAAAElFTkSuQmCC' export class Settings extends React.Component<Props, State> { state: State = { color: '#F2F7FA', showImage: false, repeat: 'no-repeat', position: 'center', size: 'auto auto', opacity: 1, angle: 20, } tryToJSON(val: string) { try { return JSON.parse(val) } catch (error) { return val } } notifyChange() { const { showImage, size, position, ...state } = this.state this.props.onChange({ ...state, image: showImage ? bgImageDataURL : undefined, size: this.tryToJSON(size), position: this.tryToJSON(position), }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onShowImageChanged = (e: any) => { this.setState({ showImage: e.target.checked }, () => { this.notifyChange() }) } onRepeatChanged = (repeat: string) => { this.setState({ repeat }, () => { this.notifyChange() }) } onPositionChanged = (position: string) => { this.setState({ position }, () => { this.notifyChange() }) } onSizeChanged = (size: string) => { this.setState({ size }, () => { this.notifyChange() }) } onOpacityChanged = (opacity: number) => { this.setState({ opacity }, () => { this.notifyChange() }) } onWaterAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } render() { return ( <Card title="Background Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>Color</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={18} offset={6}> <Checkbox checked={this.state.showImage} onChange={this.onShowImageChanged} > Show Background Image </Checkbox> </Col> </Row> {this.state.showImage && ( <React.Fragment> <Row align="middle"> <Col span={6}>Repeat</Col> <Col span={14}> <Select style={{ width: '100%' }} value={this.state.repeat} onChange={this.onRepeatChanged} > <Select.Option value="no-repeat">no repeat</Select.Option> <Select.Option value="repeat">repeat</Select.Option> <Select.Option value="repeat-x">repeat x</Select.Option> <Select.Option value="repeat-y">repeat y</Select.Option> <Select.Option value="round">round</Select.Option> <Select.Option value="space">space</Select.Option> <Select.Option value="flipX">flip x</Select.Option> <Select.Option value="flipY">flip y</Select.Option> <Select.Option value="flipXY">flip xy</Select.Option> <Select.Option value="watermark">watermark</Select.Option> </Select> </Col> </Row> {this.state.repeat === 'watermark' && ( <Row align="middle"> <Col span={16} offset={6} style={{ fontSize: 12 }}> Watermark Angle </Col> <Col span={14} offset={6}> <Slider min={0} max={360} step={1} value={this.state.angle} onChange={this.onWaterAngleChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.angle}</div> </Col> </Row> )} <Row align="middle"> <Col span={6}>Position</Col> <Col span={14}> <Select style={{ width: '100%' }} value={this.state.position} onChange={this.onPositionChanged} > <Select.Option value="center">center</Select.Option> <Select.Option value="left">left</Select.Option> <Select.Option value="right">right</Select.Option> <Select.Option value="top">top</Select.Option> <Select.Option value="bottom">bottom</Select.Option> <Select.Option value="50px 50px">50px 50px</Select.Option> <Select.Option value={JSON.stringify({ x: 50, y: 50 })}> {`{ x: 50, y: 50 }`} </Select.Option> </Select> </Col> </Row> <Row align="middle"> <Col span={6}>Size</Col> <Col span={14}> <Select style={{ width: '100%' }} value={this.state.size} onChange={this.onSizeChanged} > <Select.Option value="auto auto">auto auto</Select.Option> <Select.Option value="cover">cover</Select.Option> <Select.Option value="contain">contain</Select.Option> <Select.Option value="30px 30px">30px 30px</Select.Option> <Select.Option value="100% 100%">100% 100%</Select.Option> <Select.Option value={JSON.stringify({ width: 50, height: 50 })} > {`{width: 50, height: 50 }`} </Select.Option> </Select> </Col> </Row> <Row align="middle"> <Col span={6}>Opacity</Col> <Col span={14}> <Slider min={0.05} max={1} step={0.05} value={this.state.opacity} onChange={this.onOpacityChanged} /> </Col> <Col span={2}> <div className="slider-value"> {this.state.opacity.toFixed(2)} </div> </Col> </Row> </React.Fragment> )} </Card> ) } } ``` #### Labels - Canonical page: https://x6.antv.antgroup.com/en/api/model/labels/ - Markdown source: https://x6.antv.antgroup.com/en/api/model/labels.md - Description: Labels --- title: "Labels" description: "Labels" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/model/labels/" version: "3.x" --- The label configuration for edges in X6 is very flexible, so we dedicate a separate section to detail how to use edge labels. Before we begin, let's briefly understand a few methods for manipulating labels on an Edge instance. | Method Signature | Description | |-------------------------------------------------------------------|---------------------| | [edge.getLabels()](/en/api/model/edge#getlabels) | Get all labels. | | [edge.setLabels(...)](/en/api/model/edge#setlabels) | Set labels. | | [edge.insertLabel(...)](/en/api/model/edge#insertlabel) | Insert a label at a specified position. | | [edge.appendLabel(...)](/en/api/model/edge#appendlabel) | Append a label at the end. | | [edge.setLabelAt(...)](/en/api/model/edge#setlabelat) | Set a label at a specified position. | | [edge.getLabelAt(...)](/en/api/model/edge#getlabelat) | Get a label at a specified position. | | [edge.removeLabelAt(...)](/en/api/model/edge#removelabelat) | Remove a label at a specified position. | ## Label Definition A label includes label markup, label position, label style, etc. The complete definition is as follows. ```ts interface Label { markup?: Markup attrs?: Attr.CellAttrs position?: | number | { distance: number offset?: | number | { x?: number y?: number } angle?: number options?: { absoluteDistance?: boolean reverseDistance?: boolean absoluteOffset?: boolean keepGradient?: boolean ensureLegibility?: boolean } } } ``` - `markup`: Label markup. - `attrs`: Label style. - `position`: Label position. When its value is a `number`, it is equivalent to setting the value of `position.distance`. - `distance`: [Label position](#position). - `offset`: [Label offset](#offset). - `angle`: [Label rotation](#rotation). ## Default Label When creating an Edge, you can set the default label using the [defaultLabel option](/en/tutorial/basic/edge#defaultlabel), with the default value as follows: ```ts { markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'text', selector: 'label', }, ], attrs: { text: { fill: '#000', fontSize: 14, textAnchor: 'middle', textVerticalAnchor: 'middle', pointerEvents: 'none', }, rect: { ref: 'label', fill: '#fff', rx: 3, ry: 3, refWidth: 1, refHeight: 1, refX: 0, refY: 0, }, }, position: { distance: 0.5, }, } ``` This default label includes a `<text>` element (representing the label text) and a `<rect>` element (representing the label background), which is centered by default and has a white rounded background. Since all custom labels will be [merged](https://lodash.com/docs/4.17.15#merge) with this default label, we can simply provide a text attribute for a label as shown below. ```ts edge.appendLabel({ attrs: { text: { text: 'Hello Label', }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ source: { x: 125, y: 125 }, target: { x: 320, y: 125 }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) edge.appendLabel({ attrs: { text: { text: 'Hello Label', }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="append-label-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Label Position ### Position We can specify the label's position using the `position.distance` option of the Label, with a default value of `0.5`, indicating that the label is positioned at the center of the edge's length. Depending on the value, the calculation of the label's position can be categorized into three cases. - When it is between `[0, 1]`, it indicates that the label is positioned at a relative length (proportion) from the starting point along the length direction. - A positive number indicates that the label is positioned at a distance from the starting point along the edge's length. - A negative number indicates that the label is positioned at a distance from the endpoint along the length direction. ```ts edge.appendLabel({ attrs: { text: { text: '0.25', }, }, position: { distance: 0.25, }, }) edge.appendLabel({ attrs: { text: { text: '150', }, }, position: { distance: 150, }, }) edge.appendLabel({ attrs: { text: { text: '-100', }, }, position: { distance: -100, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ source: { x: 30, y: 80 }, target: { x: 430, y: 80 }, vertices: [{ x: 230, y: 160 }], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) edge.appendLabel({ attrs: { text: { text: '0.25', }, }, position: { distance: 0.25, }, }) edge.appendLabel({ attrs: { text: { text: '150', }, }, position: { distance: 150, }, }) edge.appendLabel({ attrs: { text: { text: '-100', }, }, position: { distance: -100, }, }) let oscillateToggle = false function contract() { edge.animate( { 'source/x': 130, 'source/y': 80, 'target/x': 330, 'target/y': 80, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = true } function oscillate() { edge.animate( { 'source/x': 30, 'source/y': 160, 'vertices/0/x': 230, 'vertices/0/y': 80, 'target/x': 430, 'target/y': 160, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = false } edge.on('animation:finish', () => { if (oscillateToggle) { oscillate() } else { contract() } }) contract() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="label-position"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### Offset We can set the label's offset using the `position.offset` option of the Label, with a default value of `0`, indicating no offset. Depending on the value, the calculation of the label's offset can be categorized into three cases. - A positive number indicates an **absolute offset downwards perpendicular to the edge**. - A negative number indicates an **absolute offset upwards perpendicular to the edge**. - A coordinate object `{x: number; y: number }` indicates an **absolute offset in both `x` and `y` directions**. ```ts edge.appendLabel({ attrs: { text: { text: 'offset: 40', }, }, position: { distance: 0.66, offset: 40, }, }) edge.appendLabel({ attrs: { text: { text: 'offset: -40', }, }, position: { distance: 0.66, offset: -40, }, }) edge.appendLabel({ attrs: { text: { text: 'offset: { x: -40, y: 80 }', }, }, position: { distance: 0.66, offset: { x: -40, y: 80, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ markup: [ { tagName: 'path', selector: 'line', }, { tagName: 'path', selector: 'offsetLabelPositiveConnector', }, { tagName: 'path', selector: 'offsetLabelNegativeConnector', }, { tagName: 'path', selector: 'offsetLabelAbsoluteConnector', }, { tagName: 'text', selector: 'offsetLabelMarker', }, ], source: { x: 30, y: 80 }, target: { x: 430, y: 80 }, vertices: [{ x: 230, y: 160 }], }) edge.attr({ line: { connection: true, fill: 'none', stroke: '#8f8f8f', strokeWidth: 1, }, offsetLabelMarker: { atConnectionRatio: 0.66, textAnchor: 'middle', textVerticalAnchor: 'middle', text: '*', fill: 'red', stroke: 'black', strokeWidth: 1, fontSize: 32, fontWeight: 'bold', }, offsetLabelPositiveConnector: { atConnectionRatio: 0.66, d: 'M 0 0 0 80', stroke: 'black', strokeDasharray: '5 5', }, offsetLabelNegativeConnector: { atConnectionRatio: 0.66, d: 'M 0 0 0 -80', stroke: 'black', strokeDasharray: '5 5', }, offsetLabelAbsoluteConnector: { atConnectionRatioIgnoreGradient: 0.66, d: 'M 0 0 -40 80', stroke: 'black', strokeDasharray: '5 5', }, }) edge.appendLabel({ attrs: { text: { text: 'offset: 40', }, }, position: { distance: 0.66, offset: 40, }, }) edge.appendLabel({ attrs: { text: { text: 'offset: -40', }, }, position: { distance: 0.66, offset: -40, }, }) edge.appendLabel({ attrs: { text: { text: 'offset: { x: -40, y: 80 }', }, }, position: { distance: 0.66, offset: { x: -40, y: 80, }, }, }) let oscillateToggle = false function contract() { edge.animate( { 'source/x': 130, 'source/y': 80, 'target/x': 330, 'target/y': 80, 'labels/0/position/offset': 80, 'labels/1/position/offset': -80, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = true } function oscillate() { edge.animate( { 'source/x': 30, 'source/y': 160, 'vertices/0/x': 230, 'vertices/0/y': 80, 'target/x': 430, 'target/y': 160, 'labels/0/position/offset': 80, 'labels/1/position/offset': -80, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = false } edge.on('animation:finish', () => { if (oscillateToggle) { oscillate() } else { contract() } }) contract() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="label-offset-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### Rotation We can set the label's rotation angle in the **clockwise direction** using the `position.angle` option of the Label, with a default value of `0`, indicating no rotation. **Options** - When `position.options.keepGradient` is `true`, the initial rotation angle of the label is the angle of the edge at the label's position, and subsequent `position.angle` settings are relative to that initial angle. - When `position.options.ensureLegibility` is `true`, an additional 180° rotation will be applied to the label if necessary to ensure the label text is more readable. ```ts edge.appendLabel({ attrs: { text: { text: '70°\nkeepGradient', }, }, position: { distance: 0.05, angle: 70, options: { keepGradient: true, }, }, }) edge.appendLabel({ attrs: { text: { text: '0°\nkeepGradient', }, }, position: { distance: 0.3, options: { keepGradient: true, }, }, }) edge.appendLabel({ attrs: { text: { text: '45°', }, }, position: { distance: 0.8, angle: 45, }, }) edge.appendLabel({ attrs: { text: { text: '135°', }, }, position: { distance: 0.9, angle: 135, }, }) edge.appendLabel({ attrs: { text: { text: '270°\nkeepGradient', }, }, position: { distance: 0.66, offset: 80, angle: 270, options: { keepGradient: true, }, }, }) edge.appendLabel({ attrs: { text: { text: '270°\nkeepGradient\nensureLegibility', }, }, position: { distance: 0.66, offset: -80, angle: 270, options: { keepGradient: true, ensureLegibility: true, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ markup: [ { tagName: 'path', selector: 'line', }, { tagName: 'path', selector: 'offsetLabelPositiveConnector', }, { tagName: 'path', selector: 'offsetLabelNegativeConnector', }, { tagName: 'path', selector: 'offsetLabelAbsoluteConnector', }, { tagName: 'text', selector: 'offsetLabelMarker', }, ], source: { x: 30, y: 120 }, target: { x: 430, y: 120 }, vertices: [{ x: 230, y: 200 }], }) edge.attr({ line: { connection: true, fill: 'none', stroke: '#8f8f8f', strokeWidth: 1, }, offsetLabelMarker: { atConnectionRatio: 0.66, textAnchor: 'middle', textVerticalAnchor: 'middle', text: '*', fill: 'red', stroke: 'black', strokeWidth: 1, fontSize: 32, fontWeight: 'bold', }, offsetLabelPositiveConnector: { atConnectionRatio: 0.66, d: 'M 0 0 0 80', stroke: 'black', strokeDasharray: '5 5', }, offsetLabelNegativeConnector: { atConnectionRatio: 0.66, d: 'M 0 0 0 -80', stroke: 'black', strokeDasharray: '5 5', }, }) edge.appendLabel({ attrs: { text: { text: '70°\nkeepGradient', }, }, position: { distance: 0.05, angle: 70, options: { keepGradient: true, }, }, }) edge.appendLabel({ attrs: { text: { text: '0°\nkeepGradient', }, }, position: { distance: 0.3, options: { keepGradient: true, }, }, }) edge.appendLabel({ attrs: { text: { text: '45°', }, }, position: { distance: 0.8, angle: 45, }, }) edge.appendLabel({ attrs: { text: { text: '135°', }, }, position: { distance: 0.9, angle: 135, }, }) edge.appendLabel({ attrs: { text: { text: '270°\nkeepGradient', }, }, position: { distance: 0.66, offset: 80, angle: 270, options: { keepGradient: true, }, }, }) edge.appendLabel({ attrs: { text: { text: '270°\nkeepGradient\nensureLegibility', }, }, position: { distance: 0.66, offset: -80, angle: 270, options: { keepGradient: true, ensureLegibility: true, }, }, }) let oscillateToggle = false function contract() { edge.animate( { 'source/x': 130, 'source/y': 120, 'target/x': 330, 'target/y': 120, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = true } function oscillate() { edge.animate( { 'source/x': 30, 'source/y': 200, 'vertices/0/x': 230, 'vertices/0/y': 120, 'target/x': 430, 'target/y': 200, }, { delay: 1000, duration: 2000, direction: 'alternate', iterations: 2, easing: 'linear', fill: 'none', }, ) oscillateToggle = false } edge.on('animation:finish', () => { if (oscillateToggle) { oscillate() } else { contract() } }) contract() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="label-rotate-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Label Style We can customize the label style using the `markup` and `attrs` options, supporting customization in two dimensions. **Method 1**: Globally override the default label definition when creating an Edge, affecting all labels. ```ts const edge = graph.addEdge({ source: { x: 100, y: 40 }, target: { x: 400, y: 40 }, defaultLabel: { markup: [ { tagName: 'ellipse', selector: 'bg', }, { tagName: 'text', selector: 'txt', }, ], attrs: { txt: { fill: '#7c68fc', textAnchor: 'middle', textVerticalAnchor: 'middle', }, bg: { ref: 'txt', refRx: '70%', refRy: '80%', stroke: '#7c68fc', fill: 'white', strokeWidth: 2, }, }, }, }) edge.appendLabel({ attrs: { txt: { text: 'First', }, }, position: { distance: 0.3, }, }) edge.appendLabel({ attrs: { txt: { text: 'Second', }, }, position: { distance: 0.7, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 320, y: 40 }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, defaultLabel: { markup: [ { tagName: 'ellipse', selector: 'bg', }, { tagName: 'text', selector: 'txt', }, ], attrs: { txt: { fill: '#7c68fc', textAnchor: 'middle', textVerticalAnchor: 'middle', }, bg: { ref: 'txt', refRx: '70%', refRy: '80%', stroke: '#7c68fc', fill: 'white', strokeWidth: 2, }, }, }, }) edge.appendLabel({ attrs: { txt: { text: 'First', }, }, position: { distance: 0.3, }, }) edge.appendLabel({ attrs: { txt: { text: 'Second', }, }, position: { distance: 0.7, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="label-markup-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` **Method 2**: Override the default label definition when creating a single label, affecting only that label. ```ts edge.appendLabel({ markup: [ { tagName: 'circle', selector: 'body', }, { tagName: 'text', selector: 'label', }, { tagName: 'circle', selector: 'asteriskBody', }, { tagName: 'text', selector: 'asterisk', }, ], attrs: { label: { text: '½', fill: '#000', fontSize: 12, textAnchor: 'middle', textVerticalAnchor: 'middle', pointerEvents: 'none', }, body: { ref: 'label', fill: '#fff', stroke: '#000', strokeWidth: 1, refR: 1, refCx: 0, refCy: 0, }, asterisk: { ref: 'label', text: '*', fill: '#ff0000', fontSize: 8, textAnchor: 'middle', textVerticalAnchor: 'middle', pointerEvents: 'none', refX: 16.5, refY: -2, }, asteriskBody: { ref: 'asterisk', fill: '#fff', stroke: '#000', strokeWidth: 1, refR: 1, refCx: '50%', refCy: '50%', refX: 0, refY: 0, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 320, y: 40 }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) edge.appendLabel({ markup: [ { tagName: 'circle', selector: 'body', }, { tagName: 'text', selector: 'label', }, { tagName: 'circle', selector: 'asteriskBody', }, { tagName: 'text', selector: 'asterisk', }, ], attrs: { label: { text: '½', fill: '#000', fontSize: 12, textAnchor: 'middle', textVerticalAnchor: 'middle', pointerEvents: 'none', }, body: { ref: 'label', fill: '#fff', stroke: '#000', strokeWidth: 1, refR: 1, refCx: 0, refCy: 0, }, asterisk: { ref: 'label', text: '*', fill: '#ff0000', fontSize: 8, textAnchor: 'middle', textVerticalAnchor: 'middle', pointerEvents: 'none', refX: 16.5, refY: -2, }, asteriskBody: { ref: 'asterisk', fill: '#fff', stroke: '#000', strokeWidth: 1, refR: 1, refCx: '50%', refCy: '50%', refX: 0, refY: 0, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="label-attrs-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## String Labels When setting the [default label](#default-label) using the [`updateLabels`](#default-label) option, adding labels becomes very simple, as shown in the following code. ```ts // Specify label when creating a node const edge = graph.addEdge({ source, target, labels: [ { attrs: { label: { text: 'edge label' } }, }, ], }) // Reset labels edge.setLabels([ { attrs: { label: { text: 'edge label' } }, }, ]) // Append label edge.appendLabel({ attrs: { label: { text: 'edge label' } }, }) ``` The above code only sets the label text, but it looks complicated as we have to provide a deeply nested object `{ attrs: { label: { text: 'edge' } } }`. To simplify this, we provide a syntax sugar that allows direct input of string labels, making the above code even simpler. ```ts const edge = graph.addEdge({ source, target, labels: ['edge label'], }) edge.setLabels(['edge label']) edge.appendLabel('edge label') ``` This syntax sugar defines a static method `parseStringLabel` on `Edge`, which converts string labels into Label objects. The default implementation is as follows. ```ts function parseStringLabel(label: string): Label { return { attrs: { label: { text: label } }, } } ``` It is important to note that this syntax sugar only applies to the system's default labels. This means that if you redefine the default label's `markup` using the `defaultLabel` option, you will also need to rewrite the `parseStringLabel` method to ensure the usability of string labels. ```ts Edge.config({ defaultLabel: { markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'text', selector: 'my-label', // Here the default selector is modified. }, ], }, }) // You also need to redefine parseStringLabel to ensure the usability of string labels. Edge.parseStringLabel = (label: string) => { return { attrs: { 'my-label': { text: label } }, } } ``` ## Single Label Most edges only have at most one label, so we define a [custom option](/en/api/model/cell#custom-options) `label` for `Edge` to support passing a single label. ```ts graph.addEdge({ source, target, label: { attrs: { label: { text: 'edge label' } }, }, }) ``` When only setting the label text, you can also use the string form of a single label. ```ts graph.addEdge({ source, target, label: 'edge label', }) ``` ### docs/demos/api/label/append-label/index.less ```less .append-label-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 250px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/label/label-position/index.less ```less .label-position { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/label/label-offset/index.less ```less .label-offset-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/label/label-rotate/index.less ```less .label-rotate-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/label/label-markup/index.less ```less .label-markup-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 80px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/label/label-attrs/index.less ```less .label-attrs-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 80px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### Node Tools - Canonical page: https://x6.antv.antgroup.com/en/api/registry/node-tool/ - Markdown source: https://x6.antv.antgroup.com/en/api/registry/node-tool.md - Description: Node Tools --- title: "Node Tools" description: "Node Tools" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/registry/node-tool/" version: "3.x" --- Node tools are small components rendered on nodes that typically come with some interactive features, such as a delete button that removes the corresponding node when clicked. We can add or remove tools based on the following scenarios. ```ts // Add tools when creating a node graph.addNode({ ..., tools: [ { name: 'button-remove', args: { x: 10, y: 10 }, }, ], }) // Add tools after creating a node node.addTools([ { name: 'button-remove', args: { x: 10, y: 10 }, }, ]) // Remove tool graph.on("node:mouseleave", ({ node }) => { if (node.hasTool("button-remove")) { node.removeTool("button-remove"); } }); ``` X6 provides the following default tools for nodes: - [button](#button) Renders a button at a specified position, supporting custom click interactions for the button. - [button-remove](#button-remove) Renders a delete button at a specified position, which deletes the corresponding node when clicked. - [boundary](#boundary) Renders a rectangle surrounding the node based on the node's bounding box. Note that this tool only renders a rectangle and does not provide any interaction. - [node-editor](#node-editor) Provides text editing functionality for the node. ## Built-in Tools ### button Renders a button at a specified position, supporting custom click interactions. The configuration is as follows: | Parameter Name | Type | Default Value | Description | |--------------------|------------------------------------------------------------------------|---------------|---------------------------------------------------------------------------------------------------------| | x | number \| string | `0` | The X coordinate relative to the top-left corner of the node, with decimals and percentages indicating relative positions. | | y | number \| string | `0` | The Y coordinate relative to the top-left corner of the node, with decimals and percentages indicating relative positions. | | offset | number \| `{ x: number, y: number }` | `0` | The offset based on `x` and `y`. | | rotate | boolean | - | Whether to follow the rotation of the node. | | useCellGeometry | boolean | `true` | Whether to use geometric calculations to compute the element's bounding box. Enabling this improves performance; if accuracy issues arise, set it to `false`. | | markup | Markup.JSONMarkup | - | The Markup definition for rendering the button. | | onClick | `(args: {e: Dom.MouseDownEvent, cell: Cell, view: CellView }) => void` | - | Callback function for button clicks. | ```ts // Add button on mouse hover graph.on('node:mouseenter', ({ node }) => { node.addTools({ name: 'button', args: { markup: ..., x: 0, y: 0, offset: { x: 18, y: 18 }, onClick({ view }) { ... }, }, }) }) // Remove button on mouse leave graph.on('node:mouseleave', ({ node }) => { node.removeTools() // Remove all tools }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Color, NodeView } from '@antv/x6' 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({ x: 40, y: 40, width: 120, height: 48, label: 'source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, tools: [ { name: 'button', args: { markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 14, stroke: '#fe854f', 'stroke-width': 3, fill: 'white', cursor: 'pointer', }, }, { tagName: 'text', textContent: 'Btn A', selector: 'icon', attrs: { fill: '#fe854f', 'font-size': 8, 'text-anchor': 'middle', 'pointer-events': 'none', y: '0.3em', }, }, ], x: '100%', y: '100%', offset: { x: -18, y: -18 }, onClick({ view }: { view: NodeView }) { const node = view.cell const fill = Color.randomHex() node.attr({ body: { fill, }, label: { fill: Color.invert(fill, true), }, }) }, }, }, ], }) const target = graph.addNode({ x: 160, y: 180, width: 120, height: 48, label: 'target', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.on('node:mouseenter', ({ node }) => { if (node === target) { node.addTools({ name: 'button', args: { markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 14, stroke: '#fe854f', 'stroke-width': 3, fill: 'white', cursor: 'pointer', }, }, { tagName: 'text', textContent: 'Btn B', selector: 'icon', attrs: { fill: '#fe854f', 'font-size': 8, 'text-anchor': 'middle', 'pointer-events': 'none', y: '0.3em', }, }, ], x: 0, y: 0, offset: { x: 18, y: 18 }, onClick({ view }: { view: NodeView }) { const node = view.cell const offset = parseInt( node.attr('body/strokeDashoffset') || '0', 10, ) node.attr({ body: { stroke: Color.randomHex(), strokeDasharray: '5, 1', strokeDashoffset: offset + 20, }, }) }, }, }) } }) graph.on('node:mouseleave', ({ cell }) => { if (cell === target) { cell.removeTools() } }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="button-tool-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### button-remove Renders a delete button at a specified position, which deletes the corresponding node when clicked. It is a special case of the `button` tool, so it supports all configurations of `button`. ```ts const source = graph.addNode({ ..., // Add a delete button that is always visible tools: [ { name: 'button-remove', args: { x: '100%', y: 0, offset: { x: -10, y: 10 }, }, }, ], }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' 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({ x: 40, y: 40, width: 100, height: 40, label: 'source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, tools: [ { name: 'button-remove', args: { x: '100%', y: 0, offset: { x: -10, y: 10 }, }, }, ], }) const target = graph.addNode({ x: 120, y: 160, width: 100, height: 40, label: 'target', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.on('node:mouseenter', ({ node }) => { if (node === target) { node.addTools({ name: 'button-remove', args: { x: 0, y: 0, offset: { x: 10, y: 10 }, }, }) } }) graph.on('node:mouseleave', ({ node }) => { if (node === target) { node.removeTools() } }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="button-remove-tool-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### boundary Renders a rectangle surrounding the node based on its bounding box. Note that this tool only renders a rectangle without any interaction. The configuration is as follows: | Parameter Name | Type | Default Value | Description | |--------------------|-------------|---------------|---------------------------------------------------------------------------------------------------------| | tagName | string | `rect` | The shape used for rendering. | | rotate | boolean | - | Whether the shape follows the rotation of the node. | | padding | SideOptions | `10` | Margin. | | attrs | KeyValue | `object` | Shape attributes. | | useCellGeometry | boolean | `true` | Whether to use geometric calculations to compute the element's bounding box. Enabling this improves performance; if accuracy issues arise, set it to `false`. | The default values (default styles) for `attrs` are: ```ts { fill: 'none', stroke: '#333', 'stroke-width': 0.5, 'stroke-dasharray': '5, 5', 'pointer-events': 'none', } ``` The type definition for `SideOptions` is as follows: ```typescript type SideOptions = | number | { vertical?: number horizontal?: number left?: number top?: number right?: number bottom?: number } ``` The tool usage is as follows: ```ts const source = graph.addNode({ ..., tools: [ { name: 'boundary', args: { padding: 5, attrs: { fill: '#7c68fc', stroke: '#333', 'stroke-width': 1, 'fill-opacity': 0.2, }, }, }, ], }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' 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({ x: 40, y: 40, width: 100, height: 40, label: 'source', tools: [ { name: 'boundary', args: { padding: 5, attrs: { fill: '#7c68fc', stroke: '#333', 'stroke-width': 1, 'fill-opacity': 0.2, }, }, }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode({ x: 120, y: 160, width: 100, height: 40, label: 'target', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.on('node:mouseenter', ({ node }) => { if (node === target) { node.addTools({ name: 'boundary', args: { attrs: { fill: '#7c68fc', stroke: '#333', 'stroke-width': 1, 'fill-opacity': 0.2, }, }, }) } }) graph.on('node:mouseleave', ({ node }) => { if (node === target) { node.removeTools() } }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="boundary-tool-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### node-editor Provides text editing functionality on the node. The configuration is as follows: | Parameter Name | Type | Default Value | Description | |-------------------------------|-------------------------------------------------------------------------|----------------------------------|---------------------------------------------------------------| | x | number \| string | - | The X coordinate relative to the top-left corner of the node, with decimals and percentages indicating relative positions. | | y | number \| string | - | The Y coordinate relative to the top-left corner of the node, with decimals and percentages indicating relative positions. | | attrs/fontSize | string | `14` | Font size of the edited text. | | attrs/color | string | `#000` | Font color of the edited text. | | attrs/fontFamily | string | `Arial, helvetica, sans-serif` | Font of the edited text. | | attrs/backgroundColor | string | `#fff` | Background color of the editing area. | | getText | string \| `(this: CellView, args: {cell: Cell}) => string` | - | Method to get the original text; needs to customize `getText` method in custom `markup` scenarios. | | setText | string \| `(this: CellView, args: {cell: Cell, value: string}) => void` | - | Method to set new text; needs to customize `setText` method in custom `markup` scenarios. | > **Note** It is important to note that after version 2.8.0, there is no need to dynamically add tools in the double-click event, and event parameters do not need to be passed. ```ts // Usage before version 2.8.0 graph.on('node:dblclick', ({ node, e }) => { node.addTools({ name: 'node-editor', args: { event: e, }, }) }) // Usage after version 2.8.0 node.addTools({ name: 'node-editor', }) ``` It is also important to note that if a custom `markup` is defined in the node, it is often necessary to customize the `getText` and `setText` methods to correctly get and set the edited text. Both configurations support both function and string forms; the function form is easier to understand, while the string form is essentially the property path of the text to be retrieved or set. Generally, it is recommended to use the string form so that the graph data can be fully serialized (since functions cannot be serialized), otherwise, issues may arise with the text editing functionality after rendering the canvas, for example: ```typescript node.addTools({ name: 'node-editor', args: { getText: 'a/b', setText: 'c/d', }, }) ``` The above configuration indicates: - Get edited text: `node.attr('a/b')` - Set edited text: `node.attr('c/d', value)` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, connecting: { connector: 'smooth', }, }) const source = graph.addNode({ x: 40, y: 40, width: 100, height: 40, label: 'source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, tools: ['node-editor'], }) const target = graph.addNode({ x: 180, y: 200, width: 100, height: 40, label: 'target', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, tools: ['node-editor'], }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, labels: [ { position: 0.3, attrs: { label: { text: 'edge label 1', }, }, }, { position: 0.6, attrs: { label: { text: 'edge label 2', }, }, }, ], tools: ['edge-editor'], }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="node-editor-tool-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Custom Tools ### Method One Inherit from `ToolItem` to implement a tool class, which is more challenging and requires understanding of the [ToolItem](https://github.com/antvis/X6/blob/master/src/view/tool.ts) class. You can refer to the source code of the built-in tools above; this will not be elaborated here. ```ts Graph.registerNodeTool('button', Button) ``` ### Method Two Inherit from an already registered tool and modify the configuration based on the inheritance. We provide a static method `define` on the `ToolItem` base class to quickly implement inheritance and modify configurations. ```ts const MyButton = Button.define<Button.Options>({ name: 'my-btn', markup: ..., onClick({ view }) { ... }, }) Graph.registerNodeTool('my-btn', MyButton, true) ``` Additionally, we provide a quick inheritance implementation for the `Graph.registerNodeTool` method to specify default options: ```ts Graph.registerNodeTool('my-btn', { inherit: 'button', // Base class name, using the name of the already registered tool. markup: ..., onClick: ..., }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Color, CellView } from '@antv/x6' Graph.registerNodeTool( 'my-btn', { inherit: 'button', markup: [ { tagName: 'rect', selector: 'button', attrs: { width: 40, height: 20, rx: 4, ry: 4, fill: 'white', stroke: '#fe854f', 'stroke-width': 2, cursor: 'pointer', }, }, { tagName: 'text', selector: 'text', textContent: 'btn', attrs: { fill: '#fe854f', 'font-size': 10, 'text-anchor': 'middle', 'pointer-events': 'none', x: 20, y: 13, }, }, ], onClick({ view }: { view: CellView }) { const node = view.cell const fill = Color.randomHex() node.attr({ body: { fill, }, label: { fill: Color.invert(fill, true), }, }) }, }, 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({ x: 40, y: 40, width: 140, height: 48, label: 'source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, tools: [ { name: 'my-btn', args: { x: '100%', y: '100%', offset: { x: -44, y: -24 }, }, }, ], }) const target = graph.addNode({ x: 160, y: 160, width: 140, height: 48, label: 'target', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="custom-button-tool-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/api/node-tool/button/index.less ```less .button-tool-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/node-tool/button-remove/index.less ```less .button-remove-tool-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/node-tool/boundary/index.less ```less .boundary-tool-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/node-tool/node-editor/index.less ```less .node-editor-tool-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/node-tool/custom-button/index.less ```less .custom-button-tool-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### Panning - Canonical page: https://x6.antv.antgroup.com/en/api/graph/panning/ - Markdown source: https://x6.antv.antgroup.com/en/api/graph/panning.md - Description: Panning --- title: "Panning" description: "Panning" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/graph/panning/" version: "3.x" --- ## Demo ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.initGraph({ enabled: true, modifiers: [], eventTypes: ['leftMouseDown'], }) } initGraph(options: any) { if (this.graph) { this.graph.dispose() } this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, panning: { ...options, }, }) this.graph.addNode({ x: 320, y: 100, width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const source = this.graph.addNode({ x: 80, y: 50, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 240, y: 200, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph.centerContent() } onSettingChanged = (options: State) => { this.initGraph(options) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="panning-app"> <div className="app-side"> <Settings onChange={this.onSettingChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Configuration A regular canvas (without the `Scroller` plugin) can support panning by the `panning` option. ```ts const graph = new Graph({ panning: true, // If not provided, default is also true }) // Equivalent to const graph = new Graph({ panning: { enabled: true, }, }) ``` The supported options are as follows: ```ts interface Options { enabled?: boolean modifiers?: ModifierKey eventTypes?: ('leftMouseDown' | 'rightMouseDown' | 'mouseWheel' | 'mouseWheelDown')[] } ``` ### enabled Whether to enable canvas panning interaction. ### modifiers Dragging may conflict with other operations, so you can set the `modifiers` parameter to specify a modifier key that needs to be pressed along with the mouse click to trigger canvas panning. The type definition of `ModifierKey` is as follows: ```ts type ModifierKey = string | ('alt' | 'ctrl' | 'meta' | 'shift' | 'space')[] | null ``` It supports the following forms: - `alt` represents pressing the `alt` key. - `[alt, ctrl]` represents pressing either the `alt` or `ctrl` key. - `alt|ctrl` represents pressing either the `alt` or `ctrl` key. - `alt&ctrl` represents pressing both the `alt` and `ctrl` keys simultaneously. - `alt|ctrl&shift` represents pressing both the `alt` and `shift` keys simultaneously or pressing both the `ctrl` and `shift` keys simultaneously. ### eventTypes The interaction types that trigger canvas panning. It supports four forms or their combinations: - `leftMouseDown`: Dragging by pressing the left mouse button - `rightMouseDown`: Dragging by pressing the right mouse button - `mouseWheel`: Dragging by scrolling the mouse wheel - `mouseWheelDown`: Dragging by pressing the mouse wheel ## Methods ### isPannable() ```ts isPannable(): boolean ``` Returns whether canvas panning interaction is enabled. ### enablePanning() ```ts enablePanning(): this ``` Enables canvas panning. ### disablePanning() ```ts disablePanning(): this ``` Disables canvas panning. ### togglePanning(...) ```ts togglePanning(enabled?: boolean): this ``` Toggles the enabled state of canvas panning. The parameter is as follows: | Name | Type | Required | Default Value | Description | |---------|---------|:--------:|--------------|--------------------------------------------------| | enabled | boolean | | - | Whether to enable canvas panning, defaults to toggling the enabled state. | ### docs/demos/api/panning/playground/index.less ```less .panning-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } } ``` ### docs/demos/api/panning/playground/settings.tsx ```tsx import React from 'react' import { Checkbox, Switch, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { enabled: boolean modifiers: string[] eventTypes: string[] } export class Settings extends React.Component<Props, State> { state: State = { enabled: true, modifiers: [], eventTypes: ['leftMouseDown'], } notifyChange() { this.props.onChange({ ...this.state, }) } onEnabledChanged = (enabled: boolean) => { this.setState({ enabled }, () => { this.notifyChange() }) } onModifiersChange = (modifiers: string[]) => { this.setState({ modifiers }, () => { this.notifyChange() }) } onEventTypesChange = (eventTypes: string[]) => { this.setState({ eventTypes }, () => { this.notifyChange() }) } render() { return ( <Card title="Selection Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={14}>Enabled</Col> <Col span={10}> <Switch checked={this.state.enabled} onChange={this.onEnabledChanged} /> </Col> </Row> <Row align="middle"> <Col span={8}>Modifier Key</Col> <Col span={16}> <Checkbox.Group options={['alt', 'ctrl', 'shift']} value={this.state.modifiers} onChange={this.onModifiersChange} /> </Col> </Row> <Row align="middle"> <Col span={8}>Event Type</Col> <Col span={16}> <Checkbox.Group options={[ { label: '左键', value: 'leftMouseDown', }, { label: '右键', value: 'rightMouseDown', }, { label: '滚轮', value: 'mousewheel', }, ]} value={this.state.eventTypes} onChange={this.onEventTypesChange} /> </Col> </Row> </Card> ) } } ``` #### Arrow - Canonical page: https://x6.antv.antgroup.com/en/api/model/marker/ - Markdown source: https://x6.antv.antgroup.com/en/api/model/marker.md - Description: Arrow --- title: "Arrow" description: "Arrow" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/model/marker/" version: "3.x" --- In the previous [tutorial](/en/tutorial/basic/edge#using-arrows), we briefly introduced how to use the two special attributes [`sourceMarker`](/en/api/registry/attr#sourcemarker) and [`targetMarker`](/en/api/registry/attr#targetmarker) to specify the starting and ending arrows for edges. We demonstrated how to use built-in arrows and custom arrows. Next, we will first list the parameters for each built-in arrow, then provide a detailed explanation of how to use various SVG elements to customize arrows, and finally explain how to register custom arrows as built-in arrows. ## Built-in Arrows Built-in arrows allow for the parameterization of commonly used arrow types. When using built-in arrows, you only need to specify the arrow name `name` and the corresponding parameters. The fill color `fill` and stroke color `stroke` default to inheriting from the edge, but can be overridden by specifying the `fill` and `stroke` properties. ```ts edge.attr({ line: { sourceMarker: 'block', targetMarker: { name: 'ellipse', rx: 10, // x radius of the ellipse arrow ry: 6, // y radius of the ellipse arrow }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import * as React from 'react' import { Graph, Edge } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private edge: Edge componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.edge = this.graph.addEdge({ source: [80, 160], target: [480, 160], attrs: { line: { sourceMarker: 'block', targetMarker: 'block', stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } onMarkerChange = ({ type, ...args }: State) => { this.edge.attr({ line: { sourceMarker: { args, name: type }, targetMarker: { args, name: type }, }, } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="marker-native-app"> <div className="app-side"> <Settings onChange={this.onMarkerChange} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` Each built-in arrow has corresponding parameters, which will be introduced below. ### block Solid arrow. | Parameter Name | Type | Default Value | Description | |----------------|---------|---------------|---------------------------------------------------------------------------------------------| | size | Number | 10 | Size of the arrow. | | width | Number | size | Width of the arrow; can use `size` directly when width and height are the same. | | height | Number | size | Height of the arrow; can use `size` directly when width and height are the same. | | offset | Number | 0 | Absolute offset along the edge direction. | | open | Boolean | false | Non-closed arrow. | | ...attrs | Object | { } | Other parameters will be treated as attributes of the arrow `<path>` element, such as `‘fill’` and `'stroke'`. | ### classic Classic arrow. | Parameter Name | Type | Default Value | Description | |----------------|---------|---------------|---------------------------------------------------------------------------------------------| | size | Number | 10 | Size of the arrow. | | width | Number | size | Width of the arrow; can use `size` directly when width and height are the same. | | height | Number | size | Height of the arrow; can use `size` directly when width and height are the same. | | offset | Number | 0 | Absolute offset along the edge direction. | | ...attrs | Object | { } | Other parameters will be treated as attributes of the arrow `<path>` element, such as `‘fill’` and `'stroke'`. | ### diamond Diamond arrow. | Parameter Name | Type | Default Value | Description | |----------------|---------|---------------|---------------------------------------------------------------------------------------------| | size | Number | 10 | Size of the arrow. | | width | Number | size | Width of the arrow; can use `size` directly when width and height are the same. | | height | Number | size | Height of the arrow; can use `size` directly when width and height are the same. | | offset | Number | 0 | Absolute offset along the edge direction. | | ...attrs | Object | { } | Other parameters will be treated as attributes of the arrow `<path>` element, such as `‘fill’` and `'stroke'`. | ### cross Cross arrow. | Parameter Name | Type | Default Value | Description | |----------------|---------|---------------|---------------------------------------------------------------------------------------------| | size | Number | 10 | Size of the arrow. | | width | Number | size | Width of the arrow; can use `size` directly when width and height are the same. | | height | Number | size | Height of the arrow; can use `size` directly when width and height are the same. | | offset | Number | 0 | Absolute offset along the edge direction. | | ...attrs | Object | { } | Other parameters will be treated as attributes of the arrow `<path>` element, such as `‘fill’` and `'stroke'`. | ### async | Parameter Name | Type | Default Value | Description | |----------------|---------|---------------|---------------------------------------------------------------------------------------------| | width | Number | 10 | Width of the arrow. | | height | Number | 6 | Height of the arrow. | | offset | Number | 0 | Absolute offset along the edge direction. | | open | Boolean | false | Non-closed arrow. | | flip | Boolean | false | Whether to flip the arrow. | | ...attrs | Object | { } | Other parameters will be treated as attributes of the arrow `<path>` element, such as `‘fill’` and `'stroke'`. | ### path Custom arrow with [pathData](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d). | Parameter Name | Type | Default Value | Description | |----------------|--------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| | d | string | undefined | The [d attribute value](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) of the `<path>` element, applied to the `<path>` element after being standardized by `Util.normalizeMarker`. | | offsetX | Number | 0 | x-direction offset. | | offsetY | Number | 0 | y-direction offset. | | ...attrs | Object | { } | Other parameters will be treated as attributes of the arrow `<path>` element, such as `‘fill’` and `'stroke'`. | ```ts graph.addEdge({ source: { x: 100, y: 40 }, target: { x: 400, y: 40 }, attrs: { line: { stroke: '#31d0c6', sourceMarker: { name: 'path', d: 'M5.5,15.499,15.8,21.447,15.8,15.846,25.5,21.447,25.5,9.552,15.8,15.152,15.8,9.552z', }, targetMarker: { name: 'path', offsetX: 10, d: 'M4.834,4.834L4.833,4.833c-5.889,5.892-5.89,15.443,0.001,21.334s15.44,5.888,21.33-0.002c5.891-5.891,5.893-15.44,0.002-21.33C20.275-1.056,10.725-1.056,4.834,4.834zM25.459,5.542c0.833,0.836,1.523,1.757,2.104,2.726l-4.08,4.08c-0.418-1.062-1.053-2.06-1.912-2.918c-0.859-0.859-1.857-1.494-2.92-1.913l4.08-4.08C23.7,4.018,24.622,4.709,25.459,5.542zM10.139,20.862c-2.958-2.968-2.959-7.758-0.001-10.725c2.966-2.957,7.756-2.957,10.725,0c2.954,2.965,2.955,7.757-0.001,10.724C17.896,23.819,13.104,23.817,10.139,20.862zM5.542,25.459c-0.833-0.837-1.524-1.759-2.105-2.728l4.081-4.081c0.418,1.063,1.055,2.06,1.914,2.919c0.858,0.859,1.855,1.494,2.917,1.913l-4.081,4.081C7.299,26.982,6.379,26.292,5.542,25.459zM8.268,3.435l4.082,4.082C11.288,7.935,10.29,8.571,9.43,9.43c-0.858,0.859-1.494,1.855-1.912,2.918L3.436,8.267c0.58-0.969,1.271-1.89,2.105-2.727C6.377,4.707,7.299,4.016,8.268,3.435zM22.732,27.563l-4.082-4.082c1.062-0.418,2.061-1.053,2.919-1.912c0.859-0.859,1.495-1.857,1.913-2.92l4.082,4.082c-0.58,0.969-1.271,1.891-2.105,2.728C24.623,26.292,23.701,26.983,22.732,27.563z', }, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 320, y: 40 }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, sourceMarker: { name: 'path', d: 'M5.5,15.499,15.8,21.447,15.8,15.846,25.5,21.447,25.5,9.552,15.8,15.152,15.8,9.552z', }, targetMarker: { name: 'path', offsetX: 10, d: 'M4.834,4.834L4.833,4.833c-5.889,5.892-5.89,15.443,0.001,21.334s15.44,5.888,21.33-0.002c5.891-5.891,5.893-15.44,0.002-21.33C20.275-1.056,10.725-1.056,4.834,4.834zM25.459,5.542c0.833,0.836,1.523,1.757,2.104,2.726l-4.08,4.08c-0.418-1.062-1.053-2.06-1.912-2.918c-0.859-0.859-1.857-1.494-2.92-1.913l4.08-4.08C23.7,4.018,24.622,4.709,25.459,5.542zM10.139,20.862c-2.958-2.968-2.959-7.758-0.001-10.725c2.966-2.957,7.756-2.957,10.725,0c2.954,2.965,2.955,7.757-0.001,10.724C17.896,23.819,13.104,23.817,10.139,20.862zM5.542,25.459c-0.833-0.837-1.524-1.759-2.105-2.728l4.081-4.081c0.418,1.063,1.055,2.06,1.914,2.919c0.858,0.859,1.855,1.494,2.917,1.913l-4.081,4.081C7.299,26.982,6.379,26.292,5.542,25.459zM8.268,3.435l4.082,4.082C11.288,7.935,10.29,8.571,9.43,9.43c-0.858,0.859-1.494,1.855-1.912,2.918L3.436,8.267c0.58-0.969,1.271-1.89,2.105-2.727C6.377,4.707,7.299,4.016,8.268,3.435zM22.732,27.563l-4.082-4.082c1.062-0.418,2.061-1.053,2.919-1.912c0.859-0.859,1.495-1.857,1.913-2.92l4.082,4.082c-0.58,0.969-1.271,1.891-2.105,2.728C24.623,26.292,23.701,26.983,22.732,27.563z', }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="marker-path-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### circle Circular arrow. | Parameter Name | Type | Default Value | Description | |----------------|--------|---------------|----------------------------------------------------------------------------------------------| | r | Number | 5 | Circle radius. | | ...attrs | Object | { } | Other parameters will be treated as attributes of the arrow `<circle>` element, such as `‘fill’` and `'stroke'`. | ### circlePlus Circular plus arrow. | Parameter Name | Type | Default Value | Description | |----------------|--------|---------------|----------------------------------------------------------------------------------------------| | r | Number | 5 | Circle radius. | | ...attrs | Object | { } | Other parameters will be treated as attributes of the arrow `<path>` element (plus), such as `‘fill’` and `'stroke'`. | ### ellipse Elliptical arrow. | Parameter Name | Type | Default Value | Description | |----------------|--------|---------------|----------------------------------------------------------------------------------------------| | rx | Number | 5 | x-axis radius of the ellipse. | | ry | Number | 5 | y-axis radius of the ellipse. | | ...attrs | Object | { } | Other parameters will be treated as attributes of the arrow `<ellipse>` element, such as `‘fill’` and `'stroke'`. | ## Custom Arrows We can specify which SVG element to use for rendering the arrow through `tagName`. For example, below we specify the use of the `<path>` element to render the arrow. All options except `tagName` will be added as attributes to the created `<path>` element. The fill color `fill` and stroke color `stroke` default to inheriting from the edge, but can be overridden by specifying the `fill` and `stroke` properties. ```ts edge.attr({ line: { sourceMarker: { tagName: 'path', d: 'M 20 -10 0 0 20 10 Z', }, targetMarker: { tagName: 'path', fill: 'yellow', // Use custom fill color stroke: 'green', // Use custom stroke color strokeWidth: 2, d: 'M 20 -10 0 0 20 10 Z', }, }, }) ``` In the code above, it is worth noting that our starting and ending arrows use the same `'d'` attribute value because we automatically calculate the arrow direction. In simple terms, we only need to define an arrow that points **left towards the origin**. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ source: [228.84550125020417, 100.76702664502545], target: [416.2834258874138, 72.03741369165368], vertices: [{ x: 300, y: 150 }], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, sourceMarker: { tagName: 'path', d: 'M 20 -10 0 0 20 10 Z', }, targetMarker: { tagName: 'path', stroke: '#873bf4', strokeWidth: 2, fill: 'yellow', d: 'M 20 -10 0 0 20 10 Z', }, }, }, }) function hourHand() { const originX = 300 const originY = 150 const radius = 140 / 1.618 const steps = 60 const offset = (10 + 9.36 / 60) / 12 const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = 2 * Math.PI * (i / steps + offset) - Math.PI / 2 xs.push(originX + radius * Math.cos(theta)) ys.push(originY + radius * Math.sin(theta)) } edge.animate( { 'source/x': xs, 'source/y': ys, }, { delay: 1000, duration: 19000, easing: 'linear', fill: 'none', }, ) } function minuteHand() { const originX = 300 const originY = 150 const radius = 140 const steps = 60 const offset = 9.36 / 60 const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = 2 * Math.PI * (i / steps + offset) - Math.PI / 2 xs.push(originX + radius * Math.cos(theta)) ys.push(originY + radius * Math.sin(theta)) } edge.animate( { 'target/x': xs, 'target/y': ys, }, { delay: 1000, duration: 19000 / 12, iterations: 12, easing: 'linear', fill: 'none', }, ) } let currentAnimations = 0 function run() { currentAnimations = 2 hourHand() minuteHand() } run() edge.on('animation:finish', () => { currentAnimations -= 1 if (currentAnimations === 0) { run() } }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="marker-custom-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` Sometimes, the coordinates of the `d` attribute of the path element we obtain may not be standardized. If used directly as an arrow, it may result in positional deviation. Therefore, we provide the `normalizeMarker` utility method in the `Util` namespace to standardize the coordinates of `d`. **Method Signature** ```ts Registry.Marker.normalize(d: string, offset: { x?: number; y?: number }): string Registry.Marker.normalize(d: string, offsetX?: number, offsetY?: number): string ``` | Parameter Name | Type | Description | |----------------|----------------------------|------------------------------------| | d | string | | | offset | { x?: number; y?: number } | Offset relative to the origin | | offsetX | number | x-axis offset relative to the origin | | offsetY | number | y-axis offset relative to the origin | Comparing the starting and ending arrows below, it is clear that the starting arrow has a certain offset. After processing with `normalizeMarker`, the position of the ending arrow is corrected. ```ts const d = 'M4.834,4.834L4.833,4.833c-5.889,5.892-5.89,15.443,0.001,21.334s15.44,5.888,21.33-0.002c5.891-5.891,5.893-15.44,0.002-21.33C20.275-1.056,10.725-1.056,4.834,4.834zM25.459,5.542c0.833,0.836,1.523,1.757,2.104,2.726l-4.08,4.08c-0.418-1.062-1.053-2.06-1.912-2.918c-0.859-0.859-1.857-1.494-2.92-1.913l4.08-4.08C23.7,4.018,24.622,4.709,25.459,5.542zM10.139,20.862c-2.958-2.968-2.959-7.758-0.001-10.725c2.966-2.957,7.756-2.957,10.725,0c2.954,2.965,2.955,7.757-0.001,10.724C17.896,23.819,13.104,23.817,10.139,20.862zM5.542,25.459c-0.833-0.837-1.524-1.759-2.105-2.728l4.081-4.081c0.418,1.063,1.055,2.06,1.914,2.919c0.858,0.859,1.855,1.494,2.917,1.913l-4.081,4.081C7.299,26.982,6.379,26.292,5.542,25.459zM8.268,3.435l4.082,4.082C11.288,7.935,10.29,8.571,9.43,9.43c-0.858,0.859-1.494,1.855-1.912,2.918L3.436,8.267c0.58-0.969,1.271-1.89,2.105-2.727C6.377,4.707,7.299,4.016,8.268,3.435zM22.732,27.563l-4.082-4.082c1.062-0.418,2.061-1.053,2.919-1.912c0.859-0.859,1.495-1.857,1.913-2.92l4.082,4.082c-0.58,0.969-1.271,1.891-2.105,2.728C24.623,26.292,23.701,26.983,22.732,27.563z' graph.addEdge({ source: { x: 160, y: 40 }, target: { x: 420, y: 40 }, attrs: { line: { stroke: '#31d0c6', sourceMarker: { d, tagName: 'path', }, targetMarker: { tagName: 'path', d: Util.normalizeMarker(d), }, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Util } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const d = 'M4.834,4.834L4.833,4.833c-5.889,5.892-5.89,15.443,0.001,21.334s15.44,5.888,21.33-0.002c5.891-5.891,5.893-15.44,0.002-21.33C20.275-1.056,10.725-1.056,4.834,4.834zM25.459,5.542c0.833,0.836,1.523,1.757,2.104,2.726l-4.08,4.08c-0.418-1.062-1.053-2.06-1.912-2.918c-0.859-0.859-1.857-1.494-2.92-1.913l4.08-4.08C23.7,4.018,24.622,4.709,25.459,5.542zM10.139,20.862c-2.958-2.968-2.959-7.758-0.001-10.725c2.966-2.957,7.756-2.957,10.725,0c2.954,2.965,2.955,7.757-0.001,10.724C17.896,23.819,13.104,23.817,10.139,20.862zM5.542,25.459c-0.833-0.837-1.524-1.759-2.105-2.728l4.081-4.081c0.418,1.063,1.055,2.06,1.914,2.919c0.858,0.859,1.855,1.494,2.917,1.913l-4.081,4.081C7.299,26.982,6.379,26.292,5.542,25.459zM8.268,3.435l4.082,4.082C11.288,7.935,10.29,8.571,9.43,9.43c-0.858,0.859-1.494,1.855-1.912,2.918L3.436,8.267c0.58-0.969,1.271-1.89,2.105-2.727C6.377,4.707,7.299,4.016,8.268,3.435zM22.732,27.563l-4.082-4.082c1.062-0.418,2.061-1.053,2.919-1.912c0.859-0.859,1.495-1.857,1.913-2.92l4.082,4.082c-0.58,0.969-1.271,1.891-2.105,2.728C24.623,26.292,23.701,26.983,22.732,27.563z' graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 320, y: 40 }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, sourceMarker: { d, tagName: 'path', }, targetMarker: { tagName: 'path', d: Util.normalizeMarker(d), }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="marker-normalize-path-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` In addition to `<path>`, we can also use `<circle>`, `<image>`, `<ellipse>`, `<rect>`, `<polyline>`, `<polygon>`, and other elements to define arrows. You just need to specify the tag name through `tagName` and set other necessary attributes for the element. For example, using an image to customize an arrow is also simple. First, we set `tagName` to `image` and specify the image URL through the `xlink:href` attribute, and then adjust the `y` attribute to center the image. ```ts edge.attr({ line: { sourceMarker: { tagName: 'image', 'xlink:href': 'http://cdn3.iconfinder.com/data/icons/49handdrawing/24x24/left.png', width: 24, height: 24, y: -12, }, targetMarker: { tagName: 'image', 'xlink:href': 'http://cdn3.iconfinder.com/data/icons/49handdrawing/24x24/left.png', width: 24, height: 24, y: -12, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const marker = { tagName: 'image', // https://www.iconfinder.com/icons/15539/arrow_left_previos_icon 'xlink:href': 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFMklEQVRYR+2WaWxUVRiGnzt3lntnptPOdGba6ZRS7ALUsFY2g7SyhKWA0ALKHrBgWBRqQioQKJZdVpHdH5jKokGBgopoBCpbIkvYJIoIspUudGEYmHY6c8eMLCHIojVATLjJ+XfP9z75znve7wg84094xvo8B/jfd8AsQZMq2AP4a+On/9KBZqmyvPpnj+enUhhTG/HgnloB6GFU/8YJM40Gg/mjA0dHK7DiaQHEN0CYkzmke8ao+SOYmTk/MGvrj12A7540gDEE+neJi80ZO/0tZ7v+rwOVZMSkuTddvNIKOPWkAV4a1jBu/3sLummi6xmpqizh/O8VZI3e8UeB60ZroPhJAOiA6juFW5vlrV99k9lDY5BAFDm2+wyjx205dNLnG2AymRJdLtevwJl7QGQguEyACqgCKgDPvbAPMqEzCkY6NdpOB2u8fYHLgKZnXJ19+Wc+bQFBLpk9679k8rsfX7e+3MZrMBrD9+Xnr/W4XMcjjVJilD08tq4zMiY03GTTaWVtQPELJUVlyuWzhZeOFhYVXERZDRy5/xY0jYIBHRvEDewz/NWoUJuRqZPW7T10pXRzS4u5w8ylmd3a9G8D1X7Qacib9AknTonkfr6OJYsXUT/iJimpzTHHRgABwAWuIryuMnzVHnw1ApWlHvZvP8GGlQWVWyuuDQG2CREwPNFoHNa1S6OWKV2StInJDkJC/Cg1XorOlXPxTCUxDevgiLdzo+IqNdfKELwlrFpwmMjkMYyYNY0ZM6bx5tAEHFFq3L8c4dql01Rdr0AjmZBCLaj1JrSGUDSSjE6vpiD/HJPHrT27z+NuFTyCVxJFMXvMoLZpaekRhJo8uCt9+Lw1CHjRaDQoiPj8IKol1JIOQ5jEqnlHCU3sxdD3c8jJzmJAiwtY9T4EKQSjIw5DZCxanRFkAyjgdRXjdZVjtEaD3kBWxyks3nui310PqGB8rzrWKeOyUy3J7euhlqz4FQMBRSEQ8COogj4S8N6oxO8+zcoPjqB/oTudR2WyYtFcsnpL2BwOVDrDrXjz3+piTc1NqsqKCfi9mOomIVscqIUq5o1dw8SNB7LuN2GjeEHMGdYzKSN9UALRCTEIchwIOqrdZfg9blQ6CcF3gSW5uxCdnWiakc6OTWvITtdhDNHj94sEVEGjiqAIBEQBtU6PWjbi93sRUOG7fp4lU75m5s6zfwO4Y8zBaZGm3IG9bHWTW8iExaWgtTVBo9WhUmtwnd7C0tk7uWpuT6u+vfn+szWBpOqTQp1oPXaHCZvDhMlqRh9mxRBuRyOF4PepUNChxsPpfRtZNPs31l9yjX3ULIirhzBncIq9z2v9HMQmp2KMCaauSNnxPJbN2Myuy9HlSZ1TLMcOHjp78PDhhWEQHymKjZ2y2CDSoolwOnRiRIQOq02L3WnBEWPD5ykl/4vz7D+s1Pzgrmz/uGGk0sDbfeuZc9/IkEzNU1rjbDsV9+UCFr+Ty/Sd5XMFWS4ON5uTCwsLB9++f1rACsSroGkYNI4U1UkOvTYxVBYssloreKq07C0vziuBkY8DuBNa7doapeVDu6pf7NivDbb6HViW/SFTtl+Z44WJt9PuJuB7SCTrATvQUA/NvODzQR5Q9E8BgnWj6wvi8mGdQnr0Tjfw7Q6FCZuvzPfChNrOgdq8B3QWmD0+1ZJlCtEwaVvxspsw9mkC/KUlw/jeDvvC3cVXNxQqysCnDhAUlGCkXaVqcUFRRj3i7B/L9m888KBiDqAM8D5W6SE//FeA2ure3fcc4HkH/gSNF7Qt0mUkowAAAABJRU5ErkJggg==', width: 32, height: 32, x: -16, y: -16, } const edge = graph.addEdge({ source: [228.84550125020417, 100.76702664502545], target: [416.2834258874138, 72.03741369165368], vertices: [{ x: 300, y: 150 }], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, sourceMarker: { ...marker }, targetMarker: { ...marker }, }, }, }) function hourHand() { const originX = 300 const originY = 150 const radius = 140 / 1.618 const steps = 60 const offset = (10 + 9.36 / 60) / 12 const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = 2 * Math.PI * (i / steps + offset) - Math.PI / 2 xs.push(originX + radius * Math.cos(theta)) ys.push(originY + radius * Math.sin(theta)) } edge.animate( { 'source/x': xs, 'source/y': ys, }, { delay: 1000, duration: 19000, easing: 'linear', fill: 'none', }, ) } function minuteHand() { const originX = 300 const originY = 150 const radius = 140 const steps = 60 const offset = 9.36 / 60 const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = 2 * Math.PI * (i / steps + offset) - Math.PI / 2 xs.push(originX + radius * Math.cos(theta)) ys.push(originY + radius * Math.sin(theta)) } edge.animate( { 'target/x': xs, 'target/y': ys, }, { delay: 1000, duration: 19000 / 12, iterations: 12, easing: 'linear', fill: 'none', }, ) } let currentAnimations = 0 function run() { currentAnimations = 2 hourHand() minuteHand() } run() edge.on('animation:finish', () => { currentAnimations -= 1 if (currentAnimations === 0) { run() } }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="marker-image-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` It is important to note that when using `<circle>` and `<ellipse>` to customize arrows, you can set their `cx` attribute to the corresponding axis radius to avoid the arrow overflowing the edge boundary. Other elements can be adjusted using the `y` attribute to center the arrow vertically. ```ts edge.attr({ line: { sourceMarker: { tagName: 'ellipse', rx: 20, ry: 10, cx: 20, fill: 'rgba(255,0,0,0.3)', }, targetMarker: { tagName: 'circle', r: 12, cx: 12, fill: 'rgba(0,255,0,0.3)', }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const edge = graph.addEdge({ source: [228.84550125020417, 100.76702664502545], target: [416.2834258874138, 72.03741369165368], vertices: [{ x: 300, y: 150 }], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, sourceMarker: { tagName: 'ellipse', rx: 20, ry: 10, cx: 20, fill: 'rgba(255,0,0,0.3)', }, targetMarker: { tagName: 'circle', r: 12, cx: 12, fill: 'rgba(0,255,0,0.3)', }, }, }, }) function hourHand() { const originX = 300 const originY = 150 const radius = 140 / 1.618 const steps = 60 const offset = (10 + 9.36 / 60) / 12 const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = 2 * Math.PI * (i / steps + offset) - Math.PI / 2 xs.push(originX + radius * Math.cos(theta)) ys.push(originY + radius * Math.sin(theta)) } edge.animate( { 'source/x': xs, 'source/y': ys, }, { delay: 1000, duration: 19000, easing: 'linear', fill: 'none', }, ) } function minuteHand() { const originX = 300 const originY = 150 const radius = 140 const steps = 60 const offset = 9.36 / 60 const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = 2 * Math.PI * (i / steps + offset) - Math.PI / 2 xs.push(originX + radius * Math.cos(theta)) ys.push(originY + radius * Math.sin(theta)) } edge.animate( { 'target/x': xs, 'target/y': ys, }, { delay: 1000, duration: 19000 / 12, iterations: 12, easing: 'linear', fill: 'none', }, ) } let currentAnimations = 0 function run() { currentAnimations = 2 hourHand() minuteHand() } run() edge.on('animation:finish', () => { currentAnimations -= 1 if (currentAnimations === 0) { run() } }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="marker-tagname-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Registering Arrows Registration refers to the process of registering a method for generating arrows within the X6 system, which we call an arrow factory method. Once registered, the arrow can be used just like built-in arrows. When do we need to register arrows? - To unify the abstraction of classic arrows and extract key parameters, allowing parameters to influence the rendering of arrows for multi-scenario reuse, as described in the [Built-in Arrows](#built-in-arrows) section. - To provide a unified definition for complex arrows, allowing for a single definition to be reused in multiple places, such as arrows with complex properties and style configurations. - To enhance code readability through semantic arrow names and parameter names. For example, we can replace the `xlink:href` attribute of the `<image>` arrow with the parameter name `'imageUrl'` to make it more semantic. - And more... Before continuing, let's take a look at the definition of the arrow factory method. ```ts type Factory<T extends KeyValue = KeyValue> = (args: T) => Result type Result = Attr.SimpleAttrs & { tagName?: string children?: Result[] } ``` The arrow factory method has only one parameter `args`, which is passed in when configuring the arrow. For example: ```ts edge.attr({ line: { sourceMarker: 'block', targetMarker: { name: 'ellipse', rx: 10, ry: 6, }, }, }) ``` The above configuration is parsed to obtain the arrow name and parameters for both the starting and ending arrows. | Arrow Type | Arrow Name `name` | Arrow Parameters `args` | |----------------|-------------------|------------------------------| | sourceMarker | block | { } | | targetMarker | ellipse | { rx: 10, ry: 6 } | Internally, X6 finds the corresponding factory method by the arrow name and calls that method with the provided parameters `args`, returning the result. The structure of the result `Result` is the same as that of the custom arrow structure introduced above, specifying which SVG element to use for rendering the arrow through `tagName`, while the remaining key-value pairs are attached as attributes to that element, supporting nesting through `children`. Once we understand the principle of the factory method, we can register the factory method using the static method `registerMarker` provided by Graph. ``` Graph.registerMarker(name: string, factory: Factory, overwrite?: boolean) ``` | Parameter Name | Parameter Type | Default Value | Description | |----------------|----------------|---------------|-------------------------------------------------------------------| | name | String | | Arrow name. | | factory | Factory | | Arrow factory method. | | overwrite | Boolean | false | Whether to overwrite the old factory method when encountering a name conflict; set to `true` to overwrite, otherwise an error will be thrown. | Finally, let's register an image arrow. ```ts /** * Parameter Definition */ interface ImageMarkerArgs extends Attr.SimpleAttrs { imageUrl: string imageWidth?: number imageHeight?: number } Graph.registerMarker('image', (args: ImageMarkerArgs) => { const { imageUrl, imageWidth, imageHeight, ...attrs } = args return { ...attrs, // Return non-special parameters as is tagName: 'image', // Use <image> tag to render the arrow; other key-value pairs will be treated as attributes of that element. width: imageWidth, height: imageHeight, 'xlink:href': imageUrl, } }) ``` After registration, we can use the image arrow like this: ```ts edge.attr({ line: { sourceMarker: { name: 'image', imageUrl: 'http://cdn3.iconfinder.com/data/icons/49handdrawing/24x24/left.png', imageWidth: 24, imageHeight: 24, y: -12, }, targetMarker: { name: 'image', imageUrl: 'http://cdn3.iconfinder.com/data/icons/49handdrawing/24x24/left.png', imageWidth: 24, imageHeight: 24, y: -12, }, }, }) ``` ### docs/demos/api/marker/native/index.less ```less .marker-native-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/marker/native/settings.tsx ```tsx import React from 'react' import { Input, Switch, Checkbox, Select, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { type: string r?: number rx?: number ry?: number width?: number height?: number offset?: number custom?: boolean fill?: string stroke?: string open?: boolean flip?: boolean strokeWidth?: number } export class Settings extends React.Component<Props, State> { state: State = { type: 'block', r: 5, rx: 5, ry: 5, width: 10, height: 10, offset: 0, custom: false, strokeWidth: 1, } notifyChange() { const { custom, type, r, rx, ry, width, height, offset, open, flip, ...others } = this.state if (others.stroke == null) { delete others.strokeWidth } let state: State if (type === 'block') { state = { type, width, height, offset, open, ...others, } } else if (type === 'classic' || type === 'diamond' || type === 'cross') { state = { type, width, height, offset, ...others, } } else if (type === 'async') { state = { type, width, height, offset, open, flip, ...others, } } else if (type === 'circle' || type === 'circlePlus') { state = { type, r, ...others, } } else if (type === 'ellipse') { state = { type, rx, ry, ...others, } } this.props.onChange(state!) } onTypeChanged = (type: string) => { this.setState({ type }, () => { this.notifyChange() }) } onRChanged = (r: number) => { this.setState({ r }, () => { this.notifyChange() }) } onRxChanged = (rx: number) => { this.setState({ rx }, () => { this.notifyChange() }) } onRyChanged = (ry: number) => { this.setState({ ry }, () => { this.notifyChange() }) } onWidthChanged = (width: number) => { this.setState({ width }, () => { this.notifyChange() }) } onHeightChanged = (height: number) => { this.setState({ height }, () => { this.notifyChange() }) } onOffsetChanged = (offset: number) => { this.setState({ offset }, () => { this.notifyChange() }) } onCustomChanged = (e: any) => { this.setState({ custom: e.target.checked }, () => { this.notifyChange() }) } onFillChanged = (e: any) => { this.setState({ fill: e.target.value }, () => { this.notifyChange() }) } onStrokeChanged = (e: any) => { this.setState({ stroke: e.target.value }, () => { this.notifyChange() }) } onStrokeWidthChanged = (strokeWidth: number) => { this.setState({ strokeWidth }, () => { this.notifyChange() }) } onOpenChanged = (open: boolean) => { this.setState({ open }, () => { this.notifyChange() }) } onFlipChanged = (flip: boolean) => { this.setState({ flip }, () => { this.notifyChange() }) } render() { const type = this.state.type return ( <Card title="Marker Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>Type</Col> <Col span={14}> <Select style={{ width: '100%' }} value={type} onChange={this.onTypeChanged} > <Select.Option value="block">Block</Select.Option> <Select.Option value="classic">Classic</Select.Option> <Select.Option value="diamond">Diamond</Select.Option> <Select.Option value="cross">Cross</Select.Option> <Select.Option value="async">Async</Select.Option> <Select.Option value="circle">Circle</Select.Option> <Select.Option value="circlePlus">Circle Plus</Select.Option> <Select.Option value="ellipse">Ellipse</Select.Option> </Select> </Col> </Row> {(type === 'circle' || type === 'circlePlus') && ( <Row align="middle"> <Col span={6}>Radius</Col> <Col span={14}> <Slider min={0} max={50} step={1} value={this.state.r} onChange={this.onRChanged} /> </Col> <Col span={2} offset={1}> <div className="slider-value">{this.state.r}</div> </Col> </Row> )} {type === 'ellipse' && ( <React.Fragment> <Row align="middle"> <Col span={6}>Radius X</Col> <Col span={14}> <Slider min={0} max={50} step={1} value={this.state.rx} onChange={this.onRxChanged} /> </Col> <Col span={2} offset={1}> <div className="slider-value">{this.state.rx}</div> </Col> </Row> <Row align="middle"> <Col span={6}>Radius Y</Col> <Col span={14}> <Slider min={0} max={50} step={1} value={this.state.ry} onChange={this.onRyChanged} /> </Col> <Col span={2} offset={1}> <div className="slider-value">{this.state.ry}</div> </Col> </Row> </React.Fragment> )} {(type === 'block' || type === 'classic' || type === 'diamond' || type === 'async' || type === 'cross') && ( <React.Fragment> <Row align="middle"> <Col span={6}>Width</Col> <Col span={14}> <Slider min={0} max={50} step={1} value={this.state.width} onChange={this.onWidthChanged} /> </Col> <Col span={2} offset={1}> <div className="slider-value">{this.state.width}</div> </Col> </Row> <Row align="middle"> <Col span={6}>Height</Col> <Col span={14}> <Slider min={0} max={50} step={1} value={this.state.height} onChange={this.onHeightChanged} /> </Col> <Col span={2} offset={1}> <div className="slider-value">{this.state.height}</div> </Col> </Row> <Row align="middle"> <Col span={6}>Offset</Col> <Col span={14}> <Slider min={-50} max={50} step={1} value={this.state.offset} onChange={this.onOffsetChanged} /> </Col> <Col span={2} offset={1}> <div className="slider-value">{this.state.offset}</div> </Col> </Row> </React.Fragment> )} {(type === 'block' || type === 'async') && ( <Row align="middle"> <Col span={4}>Open</Col> <Col span={18}> <Switch checked={this.state.open} onChange={this.onOpenChanged} /> </Col> </Row> )} {type === 'async' && ( <Row align="middle"> <Col span={4}>Flip</Col> <Col span={18}> <Switch checked={this.state.flip} onChange={this.onFlipChanged} /> </Col> </Row> )} <Row align="middle" style={{ borderTop: '1px solid #e9e9e9', paddingTop: 12 }} > <Col> <div> <Checkbox checked={this.state.custom} onChange={this.onCustomChanged} > Custom fill and stroke color </Checkbox> </div> </Col> </Row> {this.state.custom && ( <React.Fragment> <Row align="middle"> <Col span={8}>Fill Color</Col> <Col span={12}> <Input type="color" value={this.state.fill} style={{ width: '100%' }} onChange={this.onFillChanged} /> </Col> </Row> <Row align="middle"> <Col span={8}>Stroke Color</Col> <Col span={12}> <Input type="color" value={this.state.stroke} style={{ width: '100%' }} onChange={this.onStrokeChanged} /> </Col> </Row> <Row align="middle"> <Col span={8}>Stroke Width</Col> <Col span={12}> <Slider min={0} max={5} step={1} value={this.state.strokeWidth} onChange={this.onStrokeWidthChanged} /> </Col> <Col span={2} offset={1}> <div className="slider-value">{this.state.strokeWidth}</div> </Col> </Row> </React.Fragment> )} </Card> ) } } ``` ### docs/demos/api/marker/path/index.less ```less .marker-path-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 80px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/marker/custom/index.less ```less .marker-custom-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/marker/normalize-path/index.less ```less .marker-normalize-path-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 100px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/marker/image/index.less ```less .marker-image-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/marker/tagname/index.less ```less .marker-tagname-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### Edge Tools - Canonical page: https://x6.antv.antgroup.com/en/api/registry/edge-tool/ - Markdown source: https://x6.antv.antgroup.com/en/api/registry/edge-tool.md - Description: Edge Tools --- title: "Edge Tools" description: "Edge Tools" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/registry/edge-tool/" version: "3.x" --- Since edges are typically narrow lines or curves, interacting with them can be inconvenient. To address this issue, we render a transparent `<path>` shape that matches the edge's shape but is wider, allowing users to interact with the edge more easily. Additionally, we can add small tools to edges to enhance their interactivity, such as vertex tools that allow path points to be moved, and segment tools that enable the movement of segments within the edge. **Scenario 1: Adding specified tools.** ```ts // Add tools when creating an edge graph.addEdge({ source, target, tools: [ { name: 'vertices' }, { name: 'button-remove', args: { distance: 20 }, }, ], }) // Add tools after creating an edge edge.addTools([ { name: 'vertices' }, { name: 'button-remove', args: { distance: 20 }, }, ]) ``` **Scenario 2: Dynamically adding/removing tools with the mouse.** ```ts graph.on('edge:mouseenter', ({ cell }) => { cell.addTools([ { name: 'vertices' }, { name: 'button-remove', args: { distance: 20 }, }, ]) }) graph.on('edge:mouseleave', ({ cell }) => { if (cell.hasTool('button-remove')) { cell.removeTool('button-remove') } }) ``` In X6, the following tools for edges are provided by default: - [vertices](#vertices) Vertex tool, renders a small dot at the position of the path point. Dragging the dot modifies the position of the path point, double-clicking the dot deletes the path point, and clicking on the edge adds a path point. - [segments](#segments) Segment tool. Renders a tool bar at the center of each segment of the edge, allowing the tool bar to be dragged to adjust the positions of the path points at both ends of the segment. - [boundary](#boundary) Renders a rectangle surrounding the edge based on its bounding box. Note that this tool only renders a rectangle without any interactivity. - [button](#button) Renders a button at a specified position, supporting custom click interactions for the button. - [button-remove](#button-remove) Renders a delete button at a specified position, which deletes the corresponding edge when clicked. - [source-arrowhead and target-arrowhead](#source-arrowhead-and-target-arrowhead) Renders a shape (default is an arrow) at the start or end of the edge, allowing the shape to be dragged to modify the start or end of the edge. - [edge-editor](#edge-editor) Provides text editing functionality on the edge. ## Built-in Tools ### vertices The path point tool renders a small dot at the position of the path point. You can drag the dot to modify the path point's position, double-click the dot to delete the path point, and click on the edge to add a new path point. The configuration is as follows: | Parameter Name | Type | Default Value | Description | |----------------------|---------------------------|---------------|-----------------------------------------------------------------------------------------------------------------------| | attrs | KeyValue | `object` | Attributes of the small dot. | | snapRadius | number | `20` | The snap radius during the movement of the path point. When the path point is within the radius of a neighboring path point, it will snap to that point. | | addable | boolean | `true` | Whether new path points can be added by clicking the mouse on the edge. | | removable | boolean | `true` | Whether path points can be removed by double-clicking. | | removeRedundancies | boolean | `true` | Whether to automatically remove redundant path points. | | stopPropagation | boolean | `true` | Whether to prevent mouse events on the tool from bubbling up to the edge view. If prevented, mouse interactions with the tool will not trigger the edge's `mousedown`, `mousemove`, and `mouseup` events. | | modifiers | `string \| ModifierKey[]` | - | Keys that must be pressed to add path points, which can resolve the overlap of interactions for adding path points and clicking on edges. | The default value (default style) for `attrs` is: ```ts { r: 6, fill: '#333', stroke: '#fff', cursor: 'move', 'stroke-width': 2, } ``` The tool is used as follows: ```ts // Add the small tool when creating an edge const edge1 = graph.addEdge({ ..., tools: [ { name: 'vertices', args: { attrs: { fill: '#666' }, }, }, ] }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, tools: [ { name: 'vertices', args: { attrs: { fill: '#8f8f8f' }, }, }, ], }) const edge2 = graph.addEdge({ source: { x: 40, y: 160 }, target: { x: 380, y: 160 }, vertices: [ { x: 40, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 160 }, ], connector: 'smooth', attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, }) graph.on('edge:mouseenter', ({ cell }) => { if (cell === edge2) { cell.addTools({ name: 'vertices', args: { attrs: { fill: '#8f8f8f' }, }, }) } }) graph.on('edge:mouseleave', ({ cell }) => { if (cell === edge2) { if (cell.hasTool('vertices')) { cell.removeTool('vertices') } } }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="edge-tool-vertices-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### segments The line segment tool renders a tool bar at the center of each line segment of the edge, allowing you to drag the tool bar to adjust the positions of the path points at both ends of the segment. The configuration is as follows: | Parameter Name | Type | Default Value | Description | |----------------------|---------|---------------|-----------------------------------------------------------------------------------------------------------------------| | attrs | object | `object` | Attributes of the element. | | precision | number | `0.5` | The tool is rendered only when the coordinate difference of the X or Y axis of the two endpoints of the segment is less than `precision`. The default `0.5` means the tool is rendered only for vertical and horizontal segments. | | threshold | number | `40` | The tool is rendered only when the segment length exceeds `threshold`. | | snapRadius | number | `10` | The snap radius during the adjustment of the segment. | | removeRedundancies | boolean | `true` | Whether to automatically remove redundant path points. | | stopPropagation | boolean | `true` | Whether to prevent mouse events on the tool from bubbling up to the edge view. If prevented, mouse interactions with the tool will not trigger the edge's `mousedown`, `mousemove`, and `mouseup` events. | The default value (default style) for `attrs` is: ```ts { width: 20, height: 8, x: -10, y: -4, rx: 4, ry: 4, fill: '#333', stroke: '#fff', 'stroke-width': 2, } ``` The tool is used as follows: ```ts graph.addEdge({ ..., tools: [{ name: 'segments', args: { snapRadius: 20, attrs: { fill: '#444', }, }, }] }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, tools: [ { name: 'segments', args: { snapRadius: 20, attrs: { fill: '#8f8f8f', }, }, }, ], }) graph.addEdge({ source: { x: 40, y: 160 }, target: { x: 380, y: 160 }, vertices: [ { x: 40, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 160 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, connector: 'smooth', tools: [ { name: 'segments', args: { snapRadius: 20, attrs: { fill: '#8f8f8f', }, }, }, ], }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="edge-tool-segments-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### boundary Renders a rectangle surrounding the edge based on its bounding box. Note that this tool only renders a rectangle and does not provide any interaction. The configuration is as follows: | Parameter Name | Type | Default Value | Description | |---------------------|----------|---------------|-----------------------------------------------------------------------------------------------------------------------| | tagName | string | `rect` | The type of shape to render. | | padding | number | `10` | Margin. | | attrs | KeyValue | `object` | Shape attributes. | | useCellGeometry | boolean | `true` | Whether to use geometric calculations to compute the element's bounding box. Enabling this will improve performance, but if there are accuracy issues, set it to `false`. | The default value (default style) for `attrs` is: ```js { fill: 'none', stroke: '#333', 'stroke-width': 0.5, 'stroke-dasharray': '5, 5', 'pointer-events': 'none', } ``` The tool is used as follows: ```ts graph.addEdge({ ..., tools: [ { name: 'boundary', args: { padding: 5, attrs: { fill: '#7c68fc', stroke: '#333', 'stroke-width': 0.5, 'fill-opacity': 0.2, }, }, }, ] }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, tools: ['boundary'], }) graph.addEdge({ source: { x: 40, y: 160 }, target: { x: 380, y: 160 }, vertices: [ { x: 40, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 160 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, connector: 'smooth', tools: [ { name: 'boundary', args: { padding: 5, useCellGeometry: false, attrs: { fill: '#7c68fc', stroke: '#333', strokeWidth: 0.5, fillOpacity: 0.2, }, }, }, ], }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="edge-tool-boundary-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### button Renders a button at a specified position, supporting custom click interactions. The configuration is as follows: | Parameter Name | Type | Default Value | Description | |----------------|------------------------------------------------------------------------|---------------|--------------------------------------| | distance | number \| string | `undefined` | Distance or ratio from the start point. | | offset | number \| Point.PointLike | `0` | Offset based on `distance`. | | rotate | boolean | `undefined` | Whether to rotate with the edge. | | markup | Markup.JSONMarkup | `undefined` | Markup definition for rendering the button. | | onClick | `(args: {e: Dom.MouseDownEvent, cell: Cell, view: CellView }) => void` | `undefined` | Callback function for button click. | The usage is as follows: ```ts graph.addEdge({ ..., tools: [ { name: 'button', args: { distance: -40, onClick({ view }: any) { // }, }, }, ], }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, EdgeView } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, tools: [ { name: 'button', args: { markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 18, stroke: '#fe854f', strokeWidth: 2, fill: 'white', cursor: 'pointer', }, }, { tagName: 'text', textContent: 'Btn B', selector: 'icon', attrs: { fill: '#fe854f', fontSize: 10, textAnchor: 'middle', pointerEvents: 'none', y: '0.3em', }, }, ], distance: -40, onClick({ view }: { view: EdgeView }) { const edge = view.cell const source = edge.getSource() const target = edge.getTarget() edge.setSource(target) edge.setTarget(source) }, }, }, ], }) graph.addEdge({ source: { x: 40, y: 160 }, target: { x: 380, y: 160 }, vertices: [ { x: 40, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 160 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, connector: 'smooth', tools: { name: 'button', args: { markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 18, stroke: '#fe854f', strokeWidth: 2, fill: 'white', cursor: 'pointer', }, }, { tagName: 'text', textContent: 'Btn A', selector: 'icon', attrs: { fill: '#fe854f', fontSize: 10, textAnchor: 'middle', pointerEvents: 'none', y: '0.3em', }, }, ], distance: -100, offset: { x: 0, y: 0 }, onClick({ view }: { view: EdgeView }) { const edge = view.cell const offset = parseInt( edge.attr('line/strokeDashoffset') || '0', 10, ) edge.attr({ line: { strokeDasharray: '5, 10', strokeDashoffset: offset + 20, }, }) }, }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="edge-tool-button-app "> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### button-remove Renders a delete button at a specified position, which deletes the corresponding edge when clicked. It is a special case of the `button` tool above, so it supports all configurations of `button`. The usage is as follows: ```ts graph.addEdge({ ..., tools: [ { name: 'button-remove', args: { distance: -40 }, }, ] }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, tools: [ { name: 'button-remove', args: { distance: -40 }, }, ], }) graph.addEdge({ source: { x: 40, y: 160 }, target: { x: 380, y: 160 }, vertices: [ { x: 40, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 160 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, connector: 'smooth', tools: [ { name: 'button-remove', args: { distance: -40 }, }, ], }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="edge-tool-button-remove-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### source-arrowhead and target-arrowhead Renders a shape (default is an arrow) at the start or end of the edge, allowing you to drag the shape to modify the start or end of the edge. The configuration is as follows: | Parameter Name | Type | Default Value | Description | |----------------|------------------|---------------|----------------------------| | tagName | string | `path` | The type of shape to render. | | attrs | Attr.SimpleAttrs | `object` | Attributes of the shape. | The default value for `source-arrowhead` attributes is: ```ts { d: 'M 10 -8 -10 0 10 8 Z', fill: '#333', stroke: '#fff', 'stroke-width': 2, cursor: 'move', } ``` The default value for `target-arrowhead` attributes is: ```ts { d: 'M -10 -8 10 0 -10 8 Z', fill: '#333', stroke: '#fff', 'stroke-width': 2, cursor: 'move', } ``` The tool is used as follows: ```ts graph.on('edge:mouseenter', ({ cell }) => { cell.addTools([ 'source-arrowhead', { name: 'target-arrowhead', args: { attrs: { fill: 'red', }, }, }, ]) }) graph.on('edge:mouseleave', ({ cell }) => { cell.removeTools() }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node-with-port', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { in: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, out: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const magnetAvailabilityHighlighter = { name: 'stroke', args: { padding: 3, attrs: { strokeWidth: 3, stroke: '#52c41a', }, }, } const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, highlighting: { magnetAvailable: magnetAvailabilityHighlighter, }, connecting: { snap: true, allowBlank: false, allowLoop: false, allowNode: false, highlight: true, createEdge() { return this.createEdge({ attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) }, validateMagnet({ magnet }) { return magnet.getAttribute('port-group') !== 'in' }, validateConnection({ sourceMagnet, targetMagnet }) { // 只能从输出链接桩创建连接 if ( !sourceMagnet || sourceMagnet.getAttribute('port-group') === 'in' ) { return false } // 只能连接到输入链接桩 if ( !targetMagnet || targetMagnet.getAttribute('port-group') !== 'in' ) { return false } return true }, }, }) const source = graph.addNode({ shape: 'custom-node-with-port', x: 40, y: 40, width: 100, height: 40, label: 'source', ports: [ { id: 'in-1', group: 'in' }, { id: 'in-2', group: 'in' }, { id: 'out-1', group: 'out' }, { id: 'out-2', group: 'out' }, ], }) const target = graph.addNode({ shape: 'custom-node-with-port', x: 140, y: 240, width: 100, height: 40, label: 'target', ports: [ { id: 'in-1', group: 'in' }, { id: 'in-2', group: 'in' }, { id: 'out-1', group: 'out' }, { id: 'out-2', group: 'out' }, ], }) graph.addNode({ shape: 'custom-node-with-port', x: 320, y: 120, width: 100, height: 40, label: 'hello', ports: [ { id: 'in-1', group: 'in' }, { id: 'in-2', group: 'in' }, { id: 'out-1', group: 'out' }, { id: 'out-2', group: 'out' }, ], }) graph.addEdge({ source: { cell: source.id, port: 'out-2' }, target: { cell: target.id, port: 'in-1' }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.on('edge:mouseenter', ({ cell }) => { cell.addTools([ { name: 'source-arrowhead', }, { name: 'target-arrowhead', args: { attrs: { fill: 'red', }, }, }, ]) }) graph.on('edge:mouseleave', ({ cell }) => { cell.removeTools() }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="edge-tool-arrowhead-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### edge-editor Provides text editing functionality on the edge. The configuration is as follows: | Parameter Name | Type | Default Value | Description | |-------------------------------|-------------------------------------------------------------------------|----------------------------------|------------------------------------------------------------------| | labelAddable | boolean | true | Whether to create a new label by clicking on a non-text position. | | attrs/fontSize | string | `14` | Font size for editing text. | | attrs/color | string | `#000` | Font color for editing text. | | attrs/fontFamily | string | `Arial, helvetica, sans-serif` | Font for editing text. | | attrs/backgroundColor | string | `#fff` | Background color for the editing area. | | getText | string \| `(this: CellView, args: {cell: Cell}) => string` | - | Method to get the original text; needs to be customized in custom `markup` scenarios. | | setText | string \| `(this: CellView, args: {cell: Cell, value: string}) => void` | - | Method to set new text; needs to be customized in custom `markup` scenarios. | > **Note** Note that after version 2.8.0, there is no need to dynamically add tools in the double-click event, so event parameters do not need to be passed. ```ts // Usage before version 2.8.0 graph.on('node:dblclick', ({ node, e }) => { edge.addTools({ name: 'edge-editor', args: { event: e, }, }) }) // Usage after version 2.8.0 edge.addTools({ name: 'edge-editor', }) ``` It is also important to note that if a custom `markup` is defined in the edge, it is often necessary to customize the `getText` and `setText` methods to correctly get and set the edited text. Both configurations support function and string forms; the function is straightforward, while the string is the property path of the text to get or set. Generally, it is recommended to use the string form so that the graph data can be fully serialized (since functions cannot be serialized), otherwise, issues may arise with the text editing functionality after rendering the canvas. For example: ```typescript edge.addTools({ name: 'edge-editor', args: { getText: 'a/b', setText: 'c/d', }, }) ``` The above configuration indicates: - Get edited text: `edge.attr('a/b')` - Set edited text: `edge.attr('c/d', value)` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, connecting: { connector: 'smooth', }, }) const source = graph.addNode({ x: 40, y: 40, width: 100, height: 40, label: 'source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, tools: ['node-editor'], }) const target = graph.addNode({ x: 180, y: 200, width: 100, height: 40, label: 'target', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, tools: ['node-editor'], }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, labels: [ { position: 0.3, attrs: { label: { text: 'edge label 1', }, }, }, { position: 0.6, attrs: { label: { text: 'edge label 2', }, }, }, ], tools: ['edge-editor'], }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="node-editor-tool-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Custom Tools ### Method One Inherit from `ToolItem` to implement a tool class, which is more complex and requires understanding of the [ToolItem](https://github.com/antvis/X6/blob/master/src/view/tool.ts) class. You can refer to the source code of the built-in tools above; this will not be elaborated here. ```ts Graph.registerEdgeTool('button', Button) ``` ### Method Two Inherit from an already registered tool and modify the configuration based on that. We provide a static method `define` on the `ToolItem` base class to quickly implement inheritance and modify configurations. ```ts import { Vertices } from '@antv/x6/es/registry/tool/vertices' const RedVertices = Vertices.define<Vertices.Options>({ attrs: { fill: 'red', }, }) Graph.registerEdgeTool('red-vertices', RedVertices, true) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerEdgeTool( 'red-vertices', { inherit: 'vertices', attrs: { fill: 'red', }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, tools: ['red-vertices'], }) const edge2 = graph.addEdge({ source: { x: 60, y: 140 }, target: { x: 380, y: 140 }, vertices: [ { x: 40, y: 180 }, { x: 200, y: 140 }, { x: 240, y: 180 }, ], connector: 'smooth', attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, }) graph.on('edge:mouseenter', ({ cell }) => { if (cell === edge2) { cell.addTools([{ name: 'red-vertices' }]) } }) graph.on('edge:mouseleave', ({ cell }) => { if (cell === edge2) { if (cell.hasTool('red-vertices')) { cell.removeTool('red-vertices') } } }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="edge-tool-custom-vertices-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` Additionally, we provide a quick way to inherit and specify default options for the `Graph.registerEdgeTool` method: ```ts Graph.registerEdgeTool('red-vertices', { inherit: 'vertices', // Base class name, using the name of the already registered tool. attrs: { // Other options, as default options for the inherited class. fill: 'red', }, }) ``` Using this method, we can quickly define and register a circular endpoint `circle-target-arrowhead`: ```ts Graph.registerEdgeTool('circle-target-arrowhead', { inherit: 'target-arrowhead', tagName: 'circle', attrs: { r: 18, fill: '#31d0c6', 'fill-opacity': 0.3, stroke: '#fe854f', 'stroke-width': 4, cursor: 'move', }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerEdgeTool('circle-target-arrowhead', { inherit: 'target-arrowhead', tagName: 'circle', attrs: { r: 18, fill: '#31d0c6', 'fill-opacity': 0.3, stroke: '#fe854f', 'stroke-width': 4, cursor: 'move', }, }) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 320, y: 40 }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: null, }, }, tools: 'circle-target-arrowhead', }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="edge-tool-custom-arrowhead-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/api/edge-tool/vertices/index.less ```less .edge-tool-vertices-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/edge-tool/segments/index.less ```less .edge-tool-segments-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/edge-tool/boundary/index.less ```less .edge-tool-boundary-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/edge-tool/button/index.less ```less .edge-tool-button-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/edge-tool/button-remove/index.less ```less .edge-tool-button-remove-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/edge-tool/arrowhead/index.less ```less .edge-tool-arrowhead-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/node-tool/node-editor/index.less ```less .node-editor-tool-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/edge-tool/custom-vertices/index.less ```less .edge-tool-custom-vertices-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/edge-tool/custom-arrowhead/index.less ```less .edge-tool-custom-arrowhead-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### Mousewheel - Canonical page: https://x6.antv.antgroup.com/en/api/graph/mousewheel/ - Markdown source: https://x6.antv.antgroup.com/en/api/graph/mousewheel.md - Description: Mousewheel --- title: "Mousewheel" description: "Mousewheel" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/graph/mousewheel/" version: "3.x" --- ## Demonstration > Hold down the `Command` key and use the mouse wheel to zoom in/out of the canvas. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.initGraph({ zoomAtMousePosition: true, }) } initGraph(options: State) { if (this.graph) { this.graph.dispose() } this.graph = new Graph({ container: this.container, height: 280, background: { color: '#F2F7FA', }, mousewheel: { enabled: true, global: true, modifiers: ['ctrl', 'meta'], ...options, }, }) this.graph.addNode({ x: 200, y: 100, width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const source = this.graph.addNode({ x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 160, y: 180, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph.centerContent() } onSettingsChanged = (options: State) => { this.initGraph(options) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="mousewheel-app"> <div className="app-side"> <Settings onChange={this.onSettingsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Configuration You can use the `mousewheel` configuration to zoom in/out of the canvas, often used in combination with modifier keys. The usage is as follows: ```ts const graph = new Graph({ mousewheel: { enabled: true, modifiers: ['ctrl', 'meta'], }, }) ``` Supported options are as follows: ```ts interface MouseWheelOptions { enabled?: boolean global?: boolean factor?: number zoomAtMousePosition?: boolean modifiers?: string | ('alt' | 'ctrl' | 'meta' | 'shift')[] | null guard?: (this: Graph, e: WheelEvent) => boolean } ``` ### enabled Whether to enable mouse wheel zooming interaction. ### factor The zoom factor. Defaults to `1.2`. ### zoomAtMousePosition Whether to zoom in/out at the mouse position. Defaults to `true`. ### global Whether to bind the wheel event globally. If set to `true`, the wheel event is bound to the `Document`, otherwise it is bound to the canvas container. Defaults to `false`. ### modifiers Modifier keys (`alt`, `ctrl`, `meta`, `shift`), which can be set to resolve conflicts between default wheel behavior and canvas zooming. Modifier keys support the following formats: - `alt` represents pressing the `alt` key. - `[alt, ctrl]` represents pressing either `alt` or `ctrl`. - `alt|ctrl` represents pressing either `alt` or `ctrl`. - `alt&ctrl` represents pressing both `alt` and `ctrl` simultaneously. - `alt|ctrl&shift` represents pressing both `alt` and `shift` simultaneously or both `ctrl` and `shift` simultaneously. ### guard Determines whether a wheel event should be handled, returning `false` to ignore the event. ```ts new Graph({ mousewheel: { enabled: true, guard(e: WheelEvent) { if (e.altKey) { // Ignore all wheel events when the alt key is pressed return false } return true }, }, }) ``` ## Methods ### isMouseWheelEnabled() ```ts isMouseWheelEnabled(): boolean ``` Returns whether mouse wheel zooming is enabled. ### enableMouseWheel() ```ts enableMouseWheel(): this ``` Enables mouse wheel zooming. ### disableMouseWheel() ```ts disableMouseWheel(): this ``` Disables mouse wheel zooming. ### toggleMouseWheel(...) ```ts toggleMouseWheel(enabled?: boolean): this ``` Toggles the enabled state of mouse wheel zooming. | Name | Type | Required | Default | Description | | --- | --- | :-: | --- | --- | | enabled | boolean | | - | Whether to enable mouse wheel zooming, toggles the state if not provided. | ### docs/demos/api/mousewheel/playground/index.less ```less .mousewheel-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } } ``` ### docs/demos/api/mousewheel/playground/settings.tsx ```tsx import React from 'react' import { Checkbox, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { zoomAtMousePosition: boolean } export class Settings extends React.Component<Props, State> { state: State = { zoomAtMousePosition: true, } notifyChange() { this.props.onChange({ ...this.state, }) } onFixedChanged = (e: any) => { this.setState({ zoomAtMousePosition: e.target.checked }, () => { this.notifyChange() }) } render() { return ( <Card title="MouseWheel Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.zoomAtMousePosition} onChange={this.onFixedChanged} > Zoom at Mouse Postion </Checkbox> </Col> </Row> </Card> ) } } ``` #### Element Attributes - Canonical page: https://x6.antv.antgroup.com/en/api/model/attrs/ - Markdown source: https://x6.antv.antgroup.com/en/api/model/attrs.md - Description: Element Attributes --- title: "Element Attributes" description: "Element Attributes" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/model/attrs/" version: "3.x" --- For native SVG attributes, there are many tutorials available online, such as the [SVG Attribute Reference](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute) provided by MDN. Here, we will focus more on how to define and use special attributes. Special attributes provide more flexible and powerful functionality than native SVG attributes. When applying attributes, native attributes are directly passed to the corresponding element, while special attributes are further processed and converted into native attributes recognized by the browser before being passed to the corresponding element. ## Relative Size and Position When customizing nodes or edges, setting the relative size of elements is a very common requirement. We provide a series of special attributes prefixed with `ref` in X6, which can be used to set the relative size of elements. These attributes are calculated based on the data size of nodes/edges, which means that all calculations do not rely on the browser's bbox calculation, so there are no performance issues. - [`refWidth`](/en/api/registry/attr#refwidth) and [`refHeight`](/en/api/registry/attr#refheight) set the element size. - [`refX`](/en/api/registry/attr#refx) and [`refY`](/en/api/registry/attr#refy) set the element position. - [`refCx`](/en/api/registry/attr#refcx) and [`refCy`](/en/api/registry/attr#refcy) set the center position of `<ellipse>` and `<circle>`. - [`refRx`](/en/api/registry/attr#refrx) and [`refRy`](/en/api/registry/attr#refry) set the radius of `<ellipse>`. - [`refR`](/en/api/registry/attr#refr) sets the radius of `<circle>`. Let's take a look at how to use these relative attributes. In the following example, we define a red ellipse `e`, a green rectangle `r`, a blue circle `c`, and a rectangle `outline` that represents the node size. ```ts graph.addNode({ shape: 'custom-rect', x: 160, y: 100, width: 280, height: 120, attrs: { e: { refRx: '50%', // ellipse x-axis radius is half of the width refRy: '25%', // ellipse y-axis radius is a quarter of the height refCx: '50%', // ellipse center x-coordinate is half of the width refCy: 0, // ellipse center y-coordinate is 0 refX: '-50%', // offset to the left by half of the width refY: '25%', // offset down by a quarter of the height }, r: { refX: '100%', // rectangle x-coordinate is at the right bottom corner of the node refY: '100%', // rectangle y-coordinate is at the right bottom corner of the node refWidth: '50%', // rectangle width is half of the node width refHeight: '50%', // rectangle height is half of the node height x: -10, // offset to the left by 10px y: -10, // offset up by 10px }, c: { refRCircumscribed: '50%', // circle radius is half of the larger value of node width and height refCx: '50%', // circle center x-coordinate is at the node center refCy: '50%', // circle center y-coordinate is at the node center }, }, }) ``` ## Relative Sub-elements The above attributes are calculated relative to the node size by default. We can also use the `ref` attribute to provide a sub-element selector, so that all calculations are relative to the element referred to by `ref`, achieving relative size and position to sub-elements. > **Warning** Note that setting `ref` will make all calculations dependent on the sub-element's bbox measurement in the browser, which may affect performance. ```ts graph.addNode({ shape: 'custom-text', x: 320, y: 160, width: 280, height: 120, attrs: { label: { text: 'H', }, e: { ref: 'label', refRx: '50%', refRy: '25%', refCx: '50%', refCy: 0, refX: '-50%', refY: '25%', }, r: { ref: 'label', refX: '100%', refY: '100%', x: -10, y: -10, refWidth: '50%', refHeight: '50%', }, c: { ref: 'label', refRCircumscribed: '50%', }, }, }) ``` ## Relative Position Along the Edge We provide the following attributes to set the position of edges and sub-elements relative to the edge. - [`connection`](/en/api/registry/attr#connection) is only applicable to `<path>` elements of edges, indicating that the edge will be rendered on this element when set to `true`. - [`atConnectionLength`](/en/api/registry/attr#atconnectionlengthkeepgradient) is an abbreviation of `atConnectionLengthKeepGradient`, indicating that the element will be moved to the specified offset position and automatically rotated to match the slope of the edge at that position. - [`atConnectionRatio`](/en/api/registry/attr#atconnectionratiokeepgradient) is an abbreviation of `atConnectionRatioKeepGradient`, indicating that the element will be moved to the specified ratio `[0, 1]` position and automatically rotated to match the slope of the edge at that position. - [`atConnectionLengthIgnoreGradient`](/en/api/registry/attr#atconnectionlengthignoregradient) will move the element to the specified offset position, ignoring the edge's slope, without automatic rotation. - [`atConnectionRatioIgnoreGradient`](/en/api/registry/attr#atconnectionratioignoregradient) will move the element to the specified ratio `[0, 1]` position, ignoring the edge's slope, without automatic rotation. ```ts graph.addEdge({ shape: 'custom-edge', source: { x: 100, y: 60 }, target: { x: 500, y: 60 }, vertices: [{ x: 300, y: 160 }], attrs: { symbol: { atConnectionRatio: 0.75, // along the edge, 75% from the start point }, arrowhead: { atConnectionLength: 100, // along the edge, 100px from the start point }, }, }) ``` ```ts graph.addEdge({ shape: 'custom-edge', source: { x: 100, y: 60 }, target: { x: 500, y: 60 }, vertices: [{ x: 300, y: 160 }], attrs: { relativeLabel: { text: '0.25', atConnectionRatio: 0.25, }, relativeLabelBody: { atConnectionRatio: 0.25, }, absoluteLabel: { text: '150', atConnectionLength: 150, }, absoluteLabelBody: { atConnectionLength: 150, }, absoluteReverseLabel: { text: '-100', atConnectionLength: -100, }, absoluteReverseLabelBody: { atConnectionLength: -100, }, offsetLabelPositive: { y: 40, text: 'keepGradient: 0,40', atConnectionRatio: 0.66, }, offsetLabelPositiveBody: { x: -60, // 0 + -60 y: 30, // 40 + -10 atConnectionRatio: 0.66, }, offsetLabelNegative: { y: -40, text: 'keepGradient: 0,-40', atConnectionRatio: 0.66, }, offsetLabelNegativeBody: { x: -60, // 0 + -60 y: -50, // -40 + -10 atConnectionRatio: 0.66, }, offsetLabelAbsolute: { x: -40, y: 80, text: 'ignoreGradient: -40,80', atConnectionRatioIgnoreGradient: 0.66, }, offsetLabelAbsoluteBody: { x: -110, // -40 + -70 y: 70, // 80 + -10 atConnectionRatioIgnoreGradient: 0.66, }, }, }) ``` ## Using Arrows We can use the `sourceMarker` and `targetMarker` special attributes to specify the start and end arrowheads of edges, respectively. For more information, please refer to [this tutorial](/en/api/model/marker). #### Routing - Canonical page: https://x6.antv.antgroup.com/en/api/registry/router/ - Markdown source: https://x6.antv.antgroup.com/en/api/registry/router.md - Description: Routing --- title: "Routing" description: "Routing" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/registry/router/" version: "3.x" --- Routing further processes the edge's waypoints [vertices](/en/tutorial/basic/edge#vertices), adding additional points when necessary, and then returns the processed points (excluding the start and end points of the edge). For example, after [`orth`](#orth) routing, each segment of the edge is a horizontal or vertical orthogonal segment. X6 has the following built-in routing options. | Routing Name | Description | |--------------|---------------------------------------------------------------------------------------------------------------| | normal | [Default routing](#normal), returns the waypoints as they are. | | orth | [Orthogonal routing](#orth), composed of horizontal or vertical orthogonal segments. | | oneSide | [Restricted orthogonal routing](#oneside), composed of three restricted horizontal or vertical orthogonal segments. | | manhattan | [Smart orthogonal routing](#manhattan), composed of horizontal or vertical orthogonal segments that automatically avoid other nodes (obstacles) on the path. | | metro | [Smart subway line routing](#metro), composed of horizontal or vertical orthogonal segments and diagonal segments, similar to a subway map, and automatically avoids other nodes (obstacles) on the path. | | er | [Entity-relationship routing](#er), composed of zigzag diagonal segments. | When using, you can set the routing for an edge: ```ts const edge = graph.addEdge({ source, target, router: { name: 'oneSide', args: { side: 'right', }, }, }) ``` When the router has no parameters, it can also be simplified to: ```ts const edge = graph.addEdge({ source, target, router: 'oneSide', }) ``` You can also call the [`edge.setRouter`]() method to set the routing: ```ts edge.setRouter('oneSide', { side: 'right' }) ``` When creating a canvas, you can set a global default routing through the `connecting` option (the default routing for the canvas is `'normal'`): ```ts new Graph({ connecting: { router: { name: 'oneSide', args: { side: 'right', }, }, }, }) ``` When the router has no parameters, it can also be simplified to: ```ts new Graph({ connecting: { router: 'orth', }, }) ``` Now let's take a look at how to use the built-in routing and how to define and register custom routing. ## Built-in Routing ### normal The system's default routing, which returns the input `vertices` path points as is. ### orth Orthogonal routing, which adds extra points along the path to ensure that each line segment of the edge is horizontally or vertically orthogonal. The supported parameters are as follows: | Parameter Name | Parameter Type | Required | Default Value | Description | |----------------|----------------|:--------:|---------------|-----------------------------------------------| | padding | SideOptions | No | 20 | Sets the minimum distance from the anchor point to the corner. | `SideOptions` is defined as follows: ```ts export type SideOptions = | number | { vertical?: number horizontal?: number left?: number top?: number right?: number bottom?: number } ``` For example: ```ts graph.addEdge({ source, target, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], router: { name: 'orth', args: { padding: { left: 50, }, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect1 = graph.addNode({ x: 30, y: 30, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 300, y: 240, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addNode({ shape: 'circle', x: 80 - 5, y: 200 - 5, width: 10, height: 10, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) graph.addNode({ shape: 'circle', x: 300 - 5, y: 120 - 5, width: 10, height: 10, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source: rect1, target: rect2, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, vertices: [ { x: 80, y: 200 }, { x: 300, y: 120 }, ], router: { name: 'orth', }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="orth-router-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### oneSide The `oneSide` routing is a restricted version of the orthogonal routing `orth`, which generates a strict three-segment route: starting from the `side` side of the starting node, passing through the middle segment, and ending at the `side` side of the target node. It is important to note that when using this routing, do not specify `vertices` at the same time, as it will result in poor routing performance. The supported parameters are as follows: | Parameter Name | Parameter Type | Required | Default Value | Description | |----------------|------------------------------------------------------|:--------:|---------------|-----------------------------------------------| | side | `'left'` \| `'right'` \| `'top'` \| `'bottom'` | No | `'bottom'` | The starting/ending direction of the route, default is `'bottom'`. | | padding | SideOptions | No | 20 | Sets the minimum distance from the anchor point to the corner. | For example: ```ts graph.addEdge({ source, target, router: { name: 'oneSide', args: { side: 'right' }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect1 = graph.addNode({ x: 30, y: 30, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 200, y: 200, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source: rect1, target: rect2, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, router: { name: 'oneSide', args: { side: 'right', padding: 50 }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="oneside-router-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### manhattan The Manhattan routing `manhattan` is an intelligent version of the orthogonal routing `orth`, consisting of horizontal or vertical orthogonal line segments that automatically avoid other nodes (obstacles) along the path. We provide a rich set of options for this routing algorithm: | Parameter Name | Parameter Type | Required | Default Value | Description | |---------------------|-------------------------------|:--------:|---------------------------------------------|---------------------------------------------------------------| | step | number | No | `10` | The step length of the routing algorithm; smaller values increase computation. It is recommended to use the canvas grid size. | | excludeTerminals | ('source' \| 'target')[] | No | `[]` | Ignore starting or ending nodes; ignored nodes will not be considered as obstacles. | | excludeShapes | string[] | No | `[]` | Ignore specified shape nodes; ignored nodes will not be considered as obstacles. | | excludeNodes | (Node \| string)[] | No | `[]` | Nodes to ignore; ignored nodes will not be considered as obstacles. | | startDirections | string[] | No | `['top', 'right', 'bottom', 'left']` | Supported directions to start routing. | | endDirections | string[] | No | `['top', 'right', 'bottom', 'left']` | Supported directions to end routing. | | padding | SideOptions | No | - | Sets the minimum distance from the anchor point to the corner. | | fallbackRouter | Router | No | `routerPresets.orth` | In scenarios where obstacles cannot be avoided, downgrade to the specified routing. | For example, import `routerPresets` from `@antv/x6` to set the fallback explicitly: ```ts import { routerPresets } from '@antv/x6' graph.addEdge({ source, target, router: { name: 'manhattan', args: { fallbackRouter: routerPresets.orth, startDirections: ['top'], endDirections: ['bottom'], }, }, }) ``` When using the `manhattan` router together with the `Selection` plugin, if batch dragging multiple nodes causes performance lag, it is recommended to enable temporary routing downgrade in the `Selection` configuration by setting `movingRouterFallback: 'orth'`. This temporarily switches edges connected to the selected nodes to the lightweight `orth` router during dragging, reduces frequent path recalculations, and restores the original routers after idle: ```ts graph.use( new Selection({ // Switch router during dragging to orth movingRouterFallback: 'orth', }), ) ``` > **Note** The characteristic of the manhattan routing is to automatically avoid obstacles in the path. If an unavoidable situation arises, it will automatically downgrade to the orth routing. In this case, to help developers identify the issue, a warning will be logged in the console: Unable to execute manhattan algorithm, use orth instead. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Edge, EdgeView } from '@antv/x6' 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({ x: 80, y: 40, width: 120, height: 50, label: 'Source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode( source.clone().translate(460, 230).setAttrByPath('label/text', 'Target'), ) const obstacles: Node[] = [] const obstacle = graph.addNode({ x: 200, y: 150, width: 120, height: 50, label: 'Obstacle', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) let edge: Edge const update = () => { const edgeView = graph.findViewByCell(edge) as EdgeView edgeView.update() } obstacles.push(obstacle) obstacles.push(graph.addNode(obstacle.clone().translate(280, -110))) obstacles.push(graph.addNode(obstacle.clone().translate(180, 10))) obstacles.forEach((obstacle) => obstacle.on('change:position', update)) edge = graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, router: { name: 'manhattan', args: { startDirections: ['top'], endDirections: ['bottom'], }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="manhattan-router-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### metro The metro routing `metro` is a variant of the Manhattan routing `manhattan`, consisting of horizontal or vertical orthogonal line segments and diagonal segments, similar to a subway map, and automatically avoids other nodes (obstacles) along the path. Its options are the same as [manhattan](#manhattan), but the default value of `maxDirectionChange` is `45`, indicating that the maximum slope angle of the routing line segment is `45` degrees. For example: ```ts graph.addEdge({ source, target, router: { name: 'metro', args: { startDirections: ['top'], endDirections: ['bottom'], }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Edge, EdgeView } from '@antv/x6' 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({ x: 80, y: 40, width: 120, height: 50, label: 'Source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode( source.clone().translate(460, 230).setAttrByPath('label/text', 'Target'), ) const obstacles: Node[] = [] const obstacle = graph.addNode({ x: 200, y: 150, width: 120, height: 50, label: 'Obstacle', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) let edge: Edge const update = () => { const edgeView = graph.findViewByCell(edge) as EdgeView edgeView.update() } obstacles.push(obstacle) obstacles.push(graph.addNode(obstacle.clone().translate(280, -110))) obstacles.push(graph.addNode(obstacle.clone().translate(180, 10))) obstacles.forEach((obstacle) => obstacle.on('change:position', update)) edge = graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, router: { name: 'metro', args: { startDirections: ['top'], endDirections: ['bottom'], }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="metro-router-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### er The entity-relationship routing `er` consists of zigzag diagonal segments, commonly used to represent connections between entities in an ER diagram. The supported parameters are as follows: | Parameter Name | Parameter Type | Required | Default Value | Description | |----------------|-----------------------------------------------|:--------:|---------------|----------------------------------------------------------------------------------------------------| | offset | number \| 'center' | No | `32` | The distance between the first and last points of the route and the nodes. When set to `'center'`, the center of the node is used as the route point coordinate. | | min | number | No | `16` | The minimum distance between the first and last points of the route and the nodes. | | direction | `'T'` \| `'B'` \| `'L'` \| `'R'` \| `'H'` \| `'V'` | No | - | The routing direction; if omitted, the optimal direction will be automatically selected. | For example: ```ts graph.addEdge({ source, target, router: { name: 'er', args: { offset: 24, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Edge, Graph } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private edge: Edge componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect1 = graph.addNode({ x: 40, y: 30, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 240, y: 150, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.edge = graph.addEdge({ source: rect1, target: rect2, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, router: { name: 'er', args: this.getERArgs(defaults), }, }) graph.centerContent() } getERArgs(state: State) { const { center, offset, min, direction } = state return { min, direction: direction || undefined, offset: center ? 'center' : offset, } } updateConnection = (state: State) => { this.edge.prop('router/args', this.getERArgs(state)) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="er-router-app"> <div className="app-left"> <Settings onChange={this.updateConnection} /> </div> <div ref={this.refContainer} className="app-content" /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Custom Routing In addition to built-in routing, we can also create custom routing according to certain rules, for example, implementing random routing: ```ts // Routing parameters interface RandomRouterArgs { bounces?: number } function randomRouter( vertices: Point.PointLike[], args: RandomRouterArgs, view: EdgeView, ) { const bounces = args.bounces || 20 const points = vertices.map((p) => Point.create(p)) for (var i = 0; i < bounces; i++) { const sourceCorner = view.sourceBBox.getCenter() const targetCorner = view.targetBBox.getCenter() const randomPoint = Point.random( sourceCorner.x, targetCorner.x, sourceCorner.y, targetCorner.y, ) points.push(randomPoint) } return points } Graph.registerRouter('random', randomRouter) edge.setRouter('random', { bounces: 3 }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Point } from '@antv/x6' Graph.registerRouter( 'random', (vertices, args, view) => { const BOUNCES = args.bounces || 20 const points = vertices.map((p) => Point.create(p)) for (let i = 0; i < BOUNCES; i += 1) { const sourceCorner = view.sourceBBox.getCenter() const targetCorner = view.targetBBox.getCenter() const randomPoint = Point.random( sourceCorner.x, targetCorner.x, sourceCorner.y, targetCorner.y, ) points.push(randomPoint) } return points }, 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({ x: 30, y: 30, width: 100, height: 40, label: 'Source', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode( source.clone().translate(300, 200).attr('label/text', 'Target'), ) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, router: { name: 'random', args: { bounces: 3, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="random-router-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/api/router/orth/index.less ```less .orth-router-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/router/oneside/index.less ```less .oneside-router-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/router/manhattan/index.less ```less .manhattan-router-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 380px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/router/metro/index.less ```less .metro-router-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 380px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/router/er/index.less ```less .er-router-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/router/er/settings.tsx ```tsx import React from 'react' import { Switch, Radio, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { offset: number min: number center: boolean direction: '' | 'T' | 'B' | 'L' | 'R' | 'H' | 'V' } export const defaults: State = { offset: 32, min: 16, center: false, direction: '', } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange(this.state) } onCenterChanged = (center: boolean) => { this.setState({ center }, () => { this.notifyChange() }) } onOffsetChanged = (offset: number) => { this.setState({ offset }, () => { this.notifyChange() }) } onMinChanged = (min: number) => { this.setState({ min }, () => { this.notifyChange() }) } onDirectionChanged = (e: any) => { const direction = e.target.value this.setState({ direction }, () => { this.notifyChange() }) } render() { return ( <Card title="Options" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>offset</Col> <Col span={14}> <Switch checkedChildren="center" unCheckedChildren="number" checked={this.state.center} onChange={this.onCenterChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}></Col> <Col span={14}> <Slider min={8} max={64} step={1} disabled={this.state.center} value={this.state.offset} onChange={this.onOffsetChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.offset}</div> </Col> </Row> <Row align="middle"> <Col span={6}>min</Col> <Col span={14}> <Slider min={8} max={64} step={1} value={this.state.min} onChange={this.onMinChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.min}</div> </Col> </Row> <Row align="top"> <Col span={6}>direction</Col> <Col span={14}> <Radio.Group name="direction" value={this.state.direction} onChange={this.onDirectionChanged} > <Radio value="" style={{ display: 'block', marginBottom: 8 }}> NONE </Radio> <Radio value="L" style={{ marginBottom: 8 }}> L </Radio> <Radio value="R">R</Radio> <Radio value="H">H</Radio> <Radio value="T">T</Radio> <Radio value="B">B</Radio> <Radio value="V">V</Radio> </Radio.Group> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/router/random/index.less ```less .random-router-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### Viewport Transformation - Canonical page: https://x6.antv.antgroup.com/en/api/graph/transform/ - Markdown source: https://x6.antv.antgroup.com/en/api/graph/transform.md - Description: Viewport Transformation --- title: "Viewport Transformation" description: "Viewport Transformation" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/graph/transform/" version: "3.x" --- ## Configuration ### scaling Configure the minimum or maximum zoom level of the canvas through the scaling configuration. ```ts new Graph({ scaling: { min: 0.05, // default value is 0.01 max: 12, // default value is 16 }, }) ``` ## Methods ### resize(...) ```ts resize(width?: number, height?: number): this ``` Set the canvas size. | Name | Type | Required | Default Value | Description | | ------ | ------ | :--: | ------ | ------------------------------ | | width | number | | | Canvas width, remains unchanged if not provided. | | height | number | | | Canvas height, remains unchanged if not provided. | ### zoom(...) ```ts zoom(): number ``` Get the canvas zoom ratio. ```ts zoom(factor: number, options?: ZoomOptions): this ``` Zoom the canvas. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | factor | number | ✓ | | Zoom ratio. | | options.absolute | boolean | | `false` | Whether to zoom absolutely. | | options.minScale | number | | - | Minimum zoom ratio. | | options.maxScale | number | | - | Maximum zoom ratio. | | options.scaleGrid | number | | - | Correct the zoom ratio to be an integer multiple of `scaleGrid`. | | options.center | Point.PointLike | | - | Zoom center. | When `options.absolute` is `true`, it means zooming the canvas to the value represented by `factor`. Otherwise, `factor` represents the zoom-in or zoom-out factor. When `factor` is a positive number, it means zooming in, and when it's a negative number, it means zooming out. ### zoomTo(...) ```ts zoomTo(factor: number, options?: ZoomOptions): this ``` Zoom the canvas to a specified ratio. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | factor | number | ✓ | | Zoom ratio. | | options.minScale | number | | - | Minimum zoom ratio. | | options.maxScale | number | | - | Maximum zoom ratio. | | options.scaleGrid | number | | - | Correct the zoom ratio to be an integer multiple of `scaleGrid`. | | options.center | Point.PointLike | | - | Zoom center. | ### zoomToFit(...) ```ts zoomToFit(options?: Options): this ``` Zoom the canvas content to fit the viewport. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | rect | Rectangle.RectangleLike | ✓ | | Rectangle area. | | options.padding | number \| `{ left: number, top: number, right: number, bottom: number }` | | - | Margin. | | options.contentArea | Rectangle.RectangleLike | | - | Content area, defaults to getting the canvas content area. | | options.viewportArea | Rectangle.RectangleLike | | - | Viewport area, defaults to getting the canvas viewport. | | options.scaleGrid | number | | - | Correct the zoom ratio to be an integer multiple of `scaleGrid`. | | options.minScale | number | | - | Minimum zoom ratio. | | options.maxScale | number | | - | Maximum zoom ratio. | | options.minScaleX | number | | - | Minimum zoom ratio in the X-axis direction. | | options.maxScaleX | number | | - | Maximum zoom ratio in the X-axis direction. | | options.minScaleY | number | | - | Minimum zoom ratio in the Y-axis direction. | | options.maxScaleY | number | | - | Maximum zoom ratio in the Y-axis direction. | | options.preserveAspectRatio | boolean | | `false` | Whether to preserve the aspect ratio. | | options.useCellGeometry | boolean | | `true` | Whether to use node/edge geometry information (Model) to calculate the bounding box. | ### rotate(...) ```ts rotate(): { angle: number cx?: number cy?: number } ``` Get the canvas rotation angle and center. ```ts rotate(angle: number, cx?: number, cy?: number): this ``` Rotate the canvas. | Name | Type | Required | Default Value | Description | | ----- | ------ | :--: | ------ | ----------------------------------- | | angle | number | ✓ | | Rotation angle. | | cx | number | | - | Rotation center x-coordinate, defaults to the canvas center. | | cy | number | | - | Rotation center y-coordinate, defaults to the canvas center. | ### translate(...) ```ts translate(): { tx: number ty: number } ``` Get the canvas translation. ```ts translate(tx: number, ty: number): this ``` Translate the canvas. | Name | Type | Required | Default Value | Description | | ---- | ------ | :--: | ------ | ------------ | | tx | number | ✓ | | X-axis translation. | | ty | number | ✓ | | Y-axis translation. | ### getContentArea(...) ```ts getContentArea(options?: Transform.GetContentAreaOptions): Rectangle ``` Get the bounding box of the canvas content, represented in local coordinates. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | options.useCellGeometry | boolean | | `true` | Whether to use node/edge geometry information (Model) to calculate the content size. | ### getContentBBox(...) ```ts getContentBBox(options?: Transform.GetContentAreaOptions): Rectangle ``` Get the bounding box of the canvas content, represented in graph coordinates. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | options.useCellGeometry | boolean | | `true` | Whether to use node/edge geometry information (Model) to calculate the content size. | ### center(...) ```ts center(options?: CenterOptions): this ``` Align the canvas center with the viewport center. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | options.padding | number \| Padding | | - | Margin, only effective in scroller canvas. | ### centerPoint(...) ```ts centerPoint(x?: number | null, y?: number | null, options?: CenterOptions): this ``` Align the point `(x, y)` (relative to the canvas) with the viewport center. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | x | number | | - | X-coordinate relative to the canvas. | | y | number | | - | Y-coordinate relative to the canvas. | | options.padding | number \| Padding | | - | Margin, only effective in scroller canvas. | ```ts graph.centerPoint(100, 200) graph.centerPoint(100, null, { padding: { left: 100 } }) graph.centerPoint(null, 200, { padding: { left: 100 } }) ``` ### centerContent(...) ```ts centerContent(options?: PositionContentOptions): this ``` Align the canvas content center with the viewport center. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | options.padding | number \| Padding | | - | Margin, only effective in scroller canvas. | | options.useCellGeometry | boolean | | `true` | Whether to use node/edge geometry information (Model) to calculate the content area. | ```ts graph.centerContent() graph.centerContent({ padding: { left: 100 } }) ``` ### centerCell(...) ```ts centerCell(options?: CenterOptions): this ``` Align the node/edge center with the viewport center. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | cell | Cell | ✓ | | Node/edge. | | options.padding | number \| Padding | | - | Margin, only effective in scroller canvas. | ```ts graph.centerCell(cell) graph.centerCell(cell, { padding: { left: 100 } }) ``` ### positionContent(...) ```ts positionContent(pos: Position, options?: PositionContentOptions): this ``` Align the canvas content bounding box position with the corresponding viewport position. For example, if `pos` is `'bottom-left'`, it means aligning the bottom-left corner of the content bounding box with the bottom-left corner of the viewport. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | pos | Position | ✓ | | Alignment position. | | options.padding | number \| Padding | | - | Margin, only effective in scroller canvas. | | options.useCellGeometry | boolean | | `true` | Whether to use node/edge geometry information (Model) to calculate the content area. | Supported alignment positions: ```ts type Position = | 'center' | 'top' | 'top-right' | 'top-left' | 'right' | 'bottom-right' | 'bottom' | 'bottom-left' | 'left' ``` ### positionCell(...) ```ts positionCell(cell: Cell, pos: Direction, options?: CenterOptions): this ``` Align the node/edge bounding box position with the corresponding viewport position. For example, if `pos` is `'bottom-left'`, it means aligning the bottom-left corner of the node/edge bounding box with the bottom-left corner of the viewport. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | cell | Cell | ✓ | | Node/edge. | | pos | Position | ✓ | | Alignment position. | | options.padding | number \| Padding | | - | Margin, only effective in scroller canvas. | ```ts type Position = | 'center' | 'top' | 'top-right' | 'top-left' | 'right' | 'bottom-right' | 'bottom' | 'bottom-left' | 'left' ``` ### positionRect(...) ```ts positionRect(rect: Rectangle.RectangleLike, pos: Direction, options?: CenterOptions): this ``` Align the rectangle position with the corresponding viewport position. For example, if `pos` is `'bottom-left'`, it means aligning the bottom-left corner of the rectangle with the bottom-left corner of the viewport. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | rect | Rectangle.RectangleLike | ✓ | | Rectangle area. | | pos | Position | ✓ | | Alignment position. | | options.padding | number \| Padding | | - | Margin, only effective in scroller canvas. | ```ts type Position = | 'center' | 'top' | 'top-right' | 'top-left' | 'right' | 'bottom-right' | 'bottom' | 'bottom-left' | 'left' ``` ### positionPoint(...) ```ts positionPoint(point: Point.PointLike, x: number | string, y: number | string, options?: CenterOptions): this ``` Align the point `(x, y)` (relative to the canvas) with the corresponding viewport position. | Name | Type | Required | Default Value | Description | | --- | --- | :-: | --- | --- | | point | Point.PointLike | ✓ | | Point to be aligned. | | x | number \| string | ✓ | | Viewport x-coordinate, supports percentage and negative values. | | y | number \| string | ✓ | | Viewport y-coordinate, supports percentage and negative values. | | options.padding | number \| Padding | | - | Margin, only effective in scroller canvas. | ```ts // Align the top-left corner of the canvas with the point [100, 50] in the viewport graph.positionPoint({ x: 0, y: 0 }, 100, 50) // Align the point { x: 30, y: 80 } on the canvas with the point 25% from the left and 40px from the bottom of the viewport graph.positionPoint({ x: 30, y: 80 }, '25%', -40) // Align the point { x: 30, y: 80 } on the canvas with the point 25% from the right and 40px from the top of the viewport graph.positionPoint({ x: 30, y: 80 }, '-25%', 40) ``` #### Interaction - Canonical page: https://x6.antv.antgroup.com/en/api/model/interaction/ - Markdown source: https://x6.antv.antgroup.com/en/api/model/interaction.md - Description: Interaction --- title: "Interaction" description: "Interaction" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/model/interaction/" version: "3.x" --- One of the most appealing aspects of X6 for developers is its comprehensive interaction customization capabilities, which allow us to achieve a wide range of complex effects. Below are some common interactive behaviors. ## Connecting By configuring `connecting`, you can achieve rich connection interactions. The usage is as follows: ```typescript const graph = new Graph({ ..., connecting: { snap: true, } }) ``` Below are the configurations supported by `connecting`. ### snap ```typescript snap: boolean | { radius: number, anchor: 'center' | 'bbox' } ``` When `snap` is set to `true` or `false`, it represents enabling or disabling automatic snapping during the connection process. When enabled, a distance of `50px` from the target will trigger the snap. You can customize the snap radius by configuring the `radius` property. ```ts const graph = new Graph({ connecting: { snap: true, }, }) // Equivalent to const graph = new Graph({ connecting: { snap: { radius: 50, }, }, }) ``` When calculating the distance to determine if it snaps to a node, it defaults to the center of the node. You can change this to calculate the distance based on the bounding box of the node by configuring `anchor` to `bbox`. ### allowBlank ```typescript allowBlank: boolean | ((this: Graph, args: ValidateConnectionArgs) => boolean) ``` Whether to allow connections to blank areas of the canvas. The default is `true`, and it also supports dynamic adjustment through a function. ```typescript const graph = new Graph({ connecting: { allowBlank() { // Return true or false based on conditions return true }, }, }) ``` ### allowLoop ```typescript allowLoop: boolean | ((this: Graph, args: ValidateConnectionArgs) => boolean) ``` Whether to allow the creation of loop connections, where the starting and ending nodes are the same. The default is `true`. ### allowNode ```typescript allowNode: boolean | ((this: Graph, args: ValidateConnectionArgs) => boolean) ``` Whether to allow edges to connect to nodes (not connection ports on nodes). The default is `true`. ### allowEdge ```typescript allowEdge: boolean | ((this: Graph, args: ValidateConnectionArgs) => boolean) ``` Whether to allow edges to connect to other edges. The default is `true`. ### allowPort ```typescript allowPort: boolean | ((this: Graph, args: ValidateConnectionArgs) => boolean) ``` Whether to allow edges to connect to connection ports. The default is `true`. ### allowMulti ```typescript allowMulti: boolean | 'withPort' | ((this: Graph, args: ValidateConnectionArgs) => boolean) ``` Whether to allow multiple edges to be created between the same starting and ending nodes. The default is `true`. When set to `false`, only one edge is allowed between the starting and ending nodes. When set to `'withPort'`, only one edge is allowed between the same connection ports of the starting and ending nodes (i.e., multiple edges can be created between the starting and ending nodes, but they must connect at different ports). ### highlight ```typescript highlight: boolean ``` Whether to highlight all available connection ports or nodes while dragging an edge. The default value is `false`. This is generally used in conjunction with [highlighting](/en/tutorial/basic/interacting#highlighting)highlighting ### anchor ```typescript anchor: NodeAnchorOptions ``` When connecting to a node, the anchor point of the connected node is specified through [ `anchor` ](/en/api/registry/node-anchor), with the default value being `center`. #### sourceAnchor ```typescript sourceAnchor?: NodeAnchorOptions ``` When connecting to a node, the anchor point of the source node is specified through `sourceAnchor`. ### targetAnchor ```typescript targetAnchor?: NodeAnchorOptions ``` When connecting to a node, the anchor point of the target node is specified through `targetAnchor`. ### edgeAnchor ```typescript edgeAnchor: EdgeAnchorOptions ``` When connecting to an edge, the anchor point of the connected edge is specified through [ `edgeAnchor` ](/en/api/registry/edge-anchor), with the default value being `ratio`. ### sourceEdgeAnchor ```typescript sourceEdgeAnchor?: EdgeAnchorOptions ``` When connecting to an edge, the anchor point of the source edge is specified through `sourceEdgeAnchor`. ### targetEdgeAnchor ```typescript targetEdgeAnchor?: EdgeAnchorOptions ``` When connecting to an edge, the anchor point of the target edge is specified through `targetEdgeAnchor`. ### connectionPoint ```typescript connectionPoint: ConnectionPointOptions ``` Specifies the [connection point](/en/api/registry/connector), with the default value being `boundary`. ### sourceConnectionPoint ```typescript sourceConnectionPoint?: ConnectionPointOptions ``` The connection point of the source. ### targetConnectionPoint ```typescript targetConnectionPoint?: ConnectionPointOptions ``` The connection point of the target. ### router ```typescript router: string | Router.NativeItem | Router.ManaualItem ``` The [router](/en/api/registry/router) further processes the edge's path points `vertices`, adding extra points if necessary, and returns the processed points. The default value is `normal`. ### connector ```typescript connector: string | Connector.NativeItem | Connector.ManaualItem ``` The [connector](/en/api/registry/connector) processes the starting point, the points returned by the router, and the endpoint into the `d` attribute of the `path` element, determining the style of the edge after rendering on the canvas. The default value is `normal`. ### createEdge ```typescript createEdge?: ( this: Graph, args: { sourceCell: Cell sourceView: CellView sourceMagnet: Element }, ) => Nilable<Edge> | void ``` This method allows you to customize the style of the newly created edge. ### validateMagnet ```typescript validateMagnet?: ( this: Graph, args: { cell: Cell view: CellView magnet: Element e: Dom.MouseDownEvent | Dom.MouseEnterEvent }, ) => boolean ``` When clicking on a `magnet`, the return value of `validateMagnet` determines whether to add a new edge. The trigger occurs when the `magnet` is pressed. If it returns `false`, there is no response; if it returns `true`, a new edge will be created at the current `magnet`. ### validateConnection ```typescript validateConnection: (this: Graph, args: ValidateConnectionArgs) => boolean ``` When moving an edge, this checks if the connection is valid. If it returns `false`, the edge will not connect to the current element when the mouse is released; otherwise, it will connect. ### validateEdge ```typescript validateEdge?: ( this: Graph, args: { edge: Edge type: Edge.TerminalType previous: Edge.TerminalData }, ) => boolean ``` When stopping the drag of an edge, this checks if the edge is valid based on the return value of `validateEdge`. If it returns `false`, the edge will be removed. ## Embedding By using `embedding`, you can drag a node into another node, making it a child of the other node. This feature is disabled by default. The supported configurations are as follows: ### enabled ```typescript enabled?: boolean ``` Whether to allow nesting between nodes. The default value is `false`. ### findParent ```typescript findParent?: | 'bbox' | 'center' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' | ((this: Graph, args: { node: Node; view: NodeView }) => Cell[]) ``` When a node is moved, the method specified by `findParent` returns the parent node. The default value is `bbox`. ### frontOnly ```typescript frontOnly?: boolean ``` If `frontOnly` is `true`, only nodes displayed in the front can be embedded. The default value is `true`. ### validate ```typescript validate: ( this: Graph, args: { child: Node parent: Node childView: CellView parentView: CellView }, ) => boolean ``` `validate` is a function that determines whether a node can be embedded in a parent node. The default return value is `true`. ## Restrictions Limit the interaction behavior of nodes and edges. The `interacting` configuration supports the following: ```typescript export type Interacting = | boolean | InteractionMap | ((this: Graph, cellView: CellView) => InteractionMap | boolean) ``` - `boolean`: Whether the node or edge is interactive. - `InteractionMap`: Interaction details for the node or edge, supporting the following properties: - `'nodeMovable'`: Whether the node can be moved. - `'magnetConnectable'`: Whether to trigger connection interactions when dragging starts on elements with the `'magnet'` attribute. - `'edgeMovable'`: Whether the edge can be moved. - `'edgeLabelMovable'`: Whether the edge's label can be moved. - `'arrowheadMovable'`: Whether the starting/ending arrow of the edge can be moved. - `'vertexMovable'`: Whether the path points of the edge can be moved. - `'vertexAddable'`: Whether path points can be added to the edge. - `'vertexDeletable'`: Whether path points can be deleted from the edge. - `(this: Graph, cellView: CellView) => InteractionMap | boolean` ```ts const graph = new Graph({ container: this.container, width: 800, height: 1400, grid: 10, interacting: function (cellView: CellView) { if (cellView.cell.getProp('customLinkInteractions')) { return { vertexAdd: false } } return true }, }) ``` ## Highlighting You can specify the highlighting style triggered by certain interactions through the `highlighting` option, such as: ```ts new Graph({ highlighting: { // When connection ports are available for linking, render a 2px wide red rectangle around the port magnetAvailable: { name: 'stroke', args: { padding: 4, attrs: { 'stroke-width': 2, stroke: 'red', }, }, }, }, }) ``` The supported `highlighting` configuration options include: - `'default'`: Default highlighting options used when the following highlighting configurations are absent. - `'embedding'`: Used when a node can be embedded during the drag operation. - `'nodeAvailable'`: Used when a node can be linked during the connection process. - `'magnetAvailable'`: Used when connection ports can be linked during the connection process. - `'magnetAdsorbed'`: Used when automatically snapping to connection ports during the connection process. The `magnetAvailable.name` above is actually the name of the highlighter. X6 has built-in highlighters `stroke` and `className`. For more details, refer to [Highlighter](/en/api/registry/highlighter). ## Movement Range You can globally configure `translating` to limit the movement range of nodes. ```ts const graph = new Graph({ translating: { restrict: true, }, }) ``` ### restrict The movable range of nodes. Supports the following two methods: - `boolean`: If set to `true`, nodes cannot move outside the canvas area. - `Rectangle.RectangleLike | (arg: CellView) => Rectangle.RectangleLike`: Specify a node's movement range. ```ts const graph = new Graph({ translating: { restrict: { x: 0, y: 0, width: 100, height: 100, }, }, }) ``` #### Connector - Canonical page: https://x6.antv.antgroup.com/en/api/registry/connector/ - Markdown source: https://x6.antv.antgroup.com/en/api/registry/connector.md - Description: Connector --- title: "Connector" description: "Connector" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/registry/connector/" version: "3.x" --- The connector processes the start point, route return point, and end point into a `<path>` element's `d` attribute, determining the style of the edge rendering on the canvas. X6 has several built-in connectors. | Connector | Description | |----------|-------------------------------------------------------------------------------------| | normal | [Simple Connector](#normal), connects the start point, route point, and end point with a straight line. | | smooth | [Smooth Connector](#smooth), connects the start point, route point, and end point with a cubic Bezier curve. | | rounded | [Rounded Connector](#rounded), connects the start point, route point, and end point with a straight line and uses an arc to connect the line segments (fillet). | | jumpover | [Jumpover Connector](#jumpover), connects the start point, route point, and end point with a straight line and uses a jump symbol to connect the intersecting edges. | You can set a route for a specific edge: ```ts const edge = graph.addEdge({ source, target, connector: { name: 'rounded', args: { radius: 20, }, }, }) ``` When there is no connector parameter, it can be simplified to: ```ts const edge = graph.addEdge({ source, target, connector: 'rounded', }) ``` You can also call `edge.setConnector` to set the connector: ```ts edge.setConnector('rounded', { radius: 20 }) ``` When creating a canvas, you can set the global default connector (default is `'normal'`) through the `connecting` option: ```ts new Graph({ connecting: { connector: { name: 'rounded', args: { radius: 20, }, }, }, }) ``` When there is no route parameter, it can be simplified to: ```ts new Graph({ connecting: { connector: 'rounded', }, }) ``` Let's take a look at how to use built-in connectors and how to customize and register custom connectors. ## Built-in Connectors ### normal The system's default connector, connects the start point, route point, and end point in sequence with a straight line. Supported parameters are as follows: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|---------------|:-------:|---------|-------------------------------------------------------------| | raw | boolean | No | `false` | Whether to return a `Path` object, default value is `false` returns a serialized string. | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect1 = graph.addNode({ x: 30, y: 30, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 300, y: 240, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source: rect1, target: rect2, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, connector: { name: 'normal', }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="connector-normal-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### smooth A smooth connector, connects the start point, route point, and end point with a cubic Bezier curve. Supported parameters are as follows: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |-----------------|-----------------|----------|---------|--------------------------------------------------------------| | raw | boolean | No | `false` | Whether to return a `Path` object, default value is `false` returns a serialized string. | | direction | `H` \| `V` | No | - | Keep horizontal connection or keep vertical connection, not set will dynamically calculate based on the start and end points. | For example: ```ts graph.addEdge({ source: rect1, target: rect2, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], connector: 'smooth', }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect1 = graph.addNode({ x: 30, y: 30, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 300, y: 240, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source: rect1, target: rect2, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, connector: { name: 'smooth', }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="connector-smooth-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### rounded A rounded connector, connects the start point, route point, and end point with a straight line and uses an arc to connect the line segments (fillet). Supported parameters are as follows: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|---------------|:-------:|---------|-------------------------------------------------------------| | radius | number | No | `10` | Fillet radius. | | raw | boolean | No | `false` | Whether to return a `Path` object, default value is `false` returns a serialized string. | For example: ```ts graph.addEdge({ source: rect1, target: rect2, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], connector: { name: 'rounded', args: { radius: 10, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Edge } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private edge: Edge componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect1 = graph.addNode({ x: 30, y: 30, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 300, y: 220, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.edge = graph.addEdge({ source: rect1, target: rect2, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], connector: { name: 'rounded', }, }) graph.centerContent() } onAttrsChanged = (args: State) => { this.edge.setConnector('rounded', args) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="connector-rounded-app"> <div className="app-left"> <Settings onChange={this.onAttrsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### jumpover A jumpover connector, connects the start point, route point, and end point with a straight line and uses a jump symbol to connect the intersecting edges. Supported parameters are as follows: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|-------------------------------|:-------:|---------|-------------------------------------------------------------| | type | 'arc' \| 'gap' \| 'cubic' | No | `'arc'` | Jump type. | | size | number | No | `5` | Jump size. | | radius | number | No | `0` | Fillet radius. | | raw | boolean | No | `false` | Whether to return a `Path` object, default value is `false` returns a serialized string. | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Edge } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private edge: Edge componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const source = graph.addNode({ x: 24, y: 70, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode({ x: 24, y: 160, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.edge = graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, vertices: [ { x: 760, y: 150 }, { x: 760, y: 240 }, ], connector: { name: 'jumpover', args: { type: 'arc', size: 5, radius: 0, }, }, }) const rect = graph.addNode({ x: 160, y: 24, width: 70, height: 30, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) for (let i = 0; i < 6; i += 1) { const source = rect.clone().translate(i * 100, i * 10) graph.addNode(source) const target = source.clone().translate(0, 200) graph.addNode(target) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } } onAttrsChanged = (args: State) => { this.edge.setConnector('jumpover', args) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="connector-jumpover-app"> <div className="app-left"> <Settings onChange={this.onAttrsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Custom Connectors A connector is a function with the following signature, returning a `Path` object or a serialized string. ```ts export type Definition<T> = ( this: EdgeView, // Edge view sourcePoint: Point.PointLike, // Start point targetPoint: Point.PointLike, // End point routePoints: Point.PointLike[], // Route return points args: T, // Connector parameters edgeView: EdgeView, // Edge view ) => Path | string ``` | Parameter Name | Parameter Type | Parameter Description | |--------------------|-------------------------|-------------------------| | this | EdgeView | Edge view. | | sourcePoint | Point.PointLike | Start point. | | targetPoint | Point.PointLike | End point. | | routePoints | Point.PointLike[] | Route return points. | | args | T | Connector parameters. | | edgeView | EdgeView | Edge view. | I'll define a `wobble` connector: ```ts export interface WobbleArgs { spread?: number raw?: boolean } function wobble( sourcePoint: Point.PointLike, targetPoint: Point.PointLike, vertices: Point.PointLike[], args: WobbleArgs, ) { const spread = args.spread || 20 const points = [...vertices, targetPoint].map((p) => Point.create(p)) let prev = Point.create(sourcePoint) const path = new Path(Path.createSegment('M', prev)) for (let i = 0, n = points.length; i < n; i += 1) { const next = points[i] const distance = prev.distance(next) let d = spread while (d < distance) { const current = prev.clone().move(next, -d) current.translate( Math.floor(7 * Math.random()) - 3, Math.floor(7 * Math.random()) - 3, ) path.appendSegment(Path.createSegment('L', current)) d += spread } path.appendSegment(Path.createSegment('L', next)) prev = next } return args.raw ? path : path.serialize() } ``` Then register the connector: ```ts Graph.registerConnector('wobble', wobble) ``` After registration, we can use it by connector name: ```ts edge.setConnector('wobble', { spread: 16 }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Path, Point } from '@antv/x6' export interface WobbleArgs { spread?: number raw?: boolean } Graph.registerConnector( 'wobble', (sourcePoint, targetPoint, vertices, args: WobbleArgs) => { const spread = args.spread || 20 const points = [...vertices, targetPoint].map((p) => Point.create(p)) let prev = Point.create(sourcePoint) const path = new Path() path.appendSegment(Path.createSegment('M', prev)) for (let i = 0, n = points.length; i < n; i += 1) { const next = points[i] const distance = prev.distance(next) let d = spread while (d < distance) { const current = prev.clone().move(next, -d) current.translate( Math.floor(7 * Math.random()) - 3, Math.floor(7 * Math.random()) - 3, ) path.appendSegment(Path.createSegment('L', current)) d += spread } path.appendSegment(Path.createSegment('L', next)) prev = next } return path }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect1 = graph.addNode({ x: 30, y: 30, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 300, y: 240, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source: rect1, target: rect2, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], connector: { name: 'wobble', args: { spread: 16, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="connector-wobble-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/api/connector/normal/index.less ```less .connector-normal-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/connector/smooth/index.less ```less .connector-smooth-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/connector/rounded/index.less ```less .connector-rounded-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 320px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/connector/rounded/settings.tsx ```tsx import React from 'react' import { Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { radius: number } export class Settings extends React.Component<Props, State> { state: State = { radius: 10, } notifyChange() { this.props.onChange(this.state) } onRadiusChanged = (radius: number) => { this.setState({ radius }, () => { this.notifyChange() }) } render() { return ( <Card title="Args" size="small" bordered={false} style={{ width: 320 }}> <Row align="middle"> <Col span={5}>radius</Col> <Col span={14}> <Slider min={1} max={30} step={1} value={this.state.radius} onChange={this.onRadiusChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.radius}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/connector/jumpover/index.less ```less .connector-jumpover-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/connector/jumpover/settings.tsx ```tsx import React from 'react' import { Slider, Radio, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { type: string size: number radius: number } export class Settings extends React.Component<Props, State> { state: State = { type: 'arc', size: 5, radius: 0, } notifyChange() { this.props.onChange(this.state) } onSizeChanged = (size: number) => { this.setState({ size }, () => { this.notifyChange() }) } onRadiusChanged = (radius: number) => { this.setState({ radius }, () => { this.notifyChange() }) } onTypeChange = (e: any) => { this.setState({ type: e.target.value }, () => { this.notifyChange() }) } render() { return ( <Card title="Args" size="small" bordered={false} style={{ width: 320 }}> <Row align="middle"> <Col span={5}>type</Col> <Col span={18}> <Radio.Group onChange={this.onTypeChange} value={this.state.type}> <Radio value={'arc'}>arc</Radio> <Radio value={'cubic'}>cubic</Radio> <Radio value={'gap'}>gap</Radio> </Radio.Group> </Col> </Row> <Row align="middle"> <Col span={5}>size</Col> <Col span={14}> <Slider min={1} max={30} step={1} value={this.state.size} onChange={this.onSizeChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.size}</div> </Col> </Row> <Row align="middle"> <Col span={5}>radius</Col> <Col span={14}> <Slider min={0} max={30} step={1} value={this.state.radius} onChange={this.onRadiusChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.radius}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/connector/wobble/index.less ```less .connector-wobble-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### Coordinate Systems - Canonical page: https://x6.antv.antgroup.com/en/api/graph/coordinate/ - Markdown source: https://x6.antv.antgroup.com/en/api/graph/coordinate.md - Description: Coordinate Systems --- title: "Coordinate Systems" description: "Coordinate Systems" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/graph/coordinate/" version: "3.x" --- ## Demo ```ts import './index.less' import { createRoot } from 'react-dom/client' import * as React from 'react' import { Graph } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private v: HTMLDivElement private h: HTMLDivElement private c: HTMLDivElement componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const source = this.graph.addNode({ x: 80, y: 80, width: 80, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ x: 240, y: 240, width: 80, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.onChanged(defaults) this.start() } componentWillUnmount() { document.removeEventListener('mousemove', this.onMouseMove) } onMouseMove = (e: MouseEvent) => { const target = e.target as HTMLElement if ( this.container.contains(target) || this.container === target || target === this.v || target === this.h || target === this.c ) { const pageX = e.pageX const pageY = e.pageY const clientX = e.clientX const clientY = e.clientY this.v.style.left = `${pageX + 2}px` this.h.style.top = `${pageY + 2}px` this.c.style.left = `${pageX + 10}px` this.c.style.top = `${pageY + 10}px` const p1 = this.graph.pageToLocal(pageX, pageY) const p2 = this.graph.localToGraph(p1) this.c.innerHTML = ` <div>Page(pageX, pageY): ${pageX} x ${pageY}</div> <div>Client(clientX, clientY): ${clientX} x ${clientY}</div> <div>Local Point: ${p1.x} x ${p1.y}</div> <div>Graph Point: ${p2.x} x ${p2.y}</div> ` } else { this.v.style.left = `${-1000}px` this.h.style.top = `${-1000}px` this.c.style.left = `${-10000}px` this.c.style.top = `${-10000}px` } } start() { const root = document.getElementById('container')! const v = document.createElement('div') const h = document.createElement('div') const c = document.createElement('div') v.style.position = 'absolute' v.style.width = '1px' v.style.top = '0' v.style.bottom = '0' v.style.left = '-100px' v.style.zIndex = `99` v.style.borderLeft = '1px dashed red' h.style.position = 'absolute' h.style.height = '1px' h.style.left = '0' h.style.right = '0' h.style.top = '-100px' h.style.zIndex = `99` h.style.borderTop = '1px dashed red' c.style.position = 'absolute' c.style.display = 'inline-block' c.style.fontSize = '12px' c.style.zIndex = `99` c.style.padding = '4px 8px' c.style.borderRadius = '2px' c.style.lineHeight = '20px' c.style.background = '#f6ffed' c.style.border = '1px solid #b7eb8f' root.appendChild(v) root.appendChild(h) root.appendChild(c) this.v = v this.h = h this.c = c document.addEventListener('mousemove', this.onMouseMove) } onChanged = (settgins: State) => { this.graph.scale(settgins.scale) this.graph.translate(settgins.tx, settgins.ty) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="coord-app"> <div className="app-left"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` In position calculations, we often need to perform coordinate conversions. In X6, there are two coordinate systems: the local canvas coordinate system `local` and the graph coordinate system `graph`. Sometimes, we also need to involve the browser coordinate system. Here's a unified explanation: - `local`: The local canvas coordinate system, which defaults to being consistent with the `graph` coordinate system but will change with canvas scaling and translation. All node coordinates in the canvas are based on the `local` coordinate system. - `graph`: The graph coordinate system, which is the canvas viewport we see and will not change with canvas scaling and translation. - `client`: The browser coordinate system, where `e.clientX` and `e.clientY` in mouse events are relative to the browser coordinate system. - `page`: The page coordinate system, which considers page horizontal and vertical scrolling compared to `client`. `e.pageX` and `e.pageY` in mouse events are relative to the page coordinate system. ## Methods ### pageToLocal(...) ```ts pageToLocal(rect: Rectangle.RectangleLike): Rectangle pageToLocal(x: number, y: number, width: number, height: number): Rectangle pageToLocal(p: Point.PointLike): Point pageToLocal(x: number, y: number): Point ``` Converts page coordinates to local canvas coordinates. ### localToPage(...) ```ts localToPage(rect: Rectangle.RectangleLike): Rectangle localToPage(x: number, y: number, width: number, height: number): Rectangle localToPage(p: Point.PointLike): Point localToPage(x: number, y: number): Point ``` Converts local canvas coordinates to page coordinates. ### clientToLocal(...) ```ts clientToLocal(rect: Rectangle.RectangleLike): Rectangle clientToLocal(x: number, y: number, width: number, height: number): Rectangle clientToLocal(p: Point.PointLike): Point clientToLocal(x: number, y: number): Point ``` Converts browser coordinates to local canvas coordinates. ### localToClient(...) ```ts localToClient(rect: Rectangle.RectangleLike): Rectangle localToClient(x: number, y: number, width: number, height: number): Rectangle localToClient(p: Point.PointLike): Point localToClient(x: number, y: number): Point ``` Converts local canvas coordinates to browser coordinates. ### localToGraph(...) ```ts localToGraph(rect: Rectangle.RectangleLike): Rectangle localToGraph(x: number, y: number, width: number, height: number): Rectangle localToGraphPoint(p: Point.PointLike): Point localToGraphPoint(x: number, y: number): Point ``` Converts local canvas coordinates to graph coordinates. ### graphToLocal(...) ```ts graphToLocal(rect: Rectangle.RectangleLike): Rectangle graphToLocal(x: number, y: number, width: number, height: number): Rectangle graphToLocal(p: Point.PointLike): Point graphToLocal(x: number, y: number): Point ``` Converts graph coordinates to local canvas coordinates. ### snapToGrid(...) ```ts snapToGrid(p: Point.PointLike): Point snapToGrid(x: number, y: number): Point ``` Convert browser coordinates to canvas [local coordinates](#clienttolocal) and align to the canvas grid. ### docs/demos/api/coord/playground/index.less ```less .bar { position: absolute; top: 48px; right: 0; left: 0; height: 2px; background: linear-gradient(to right, #9198e5, #e66465); } .coord-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { top: 64px; z-index: 10; width: 336px; padding: 0 8px; } .app-content { flex: 1; height: 318px; margin-right: 8px; overflow: auto; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } #elapsed { padding: 0 7px; color: #52c41a; font-size: 12px; font-variant: tabular-nums; line-height: 20px; white-space: nowrap; background: #f6ffed; border-color: #b7eb8f; border-radius: 2px; } } ``` ### docs/demos/api/coord/playground/settings.tsx ```tsx import React from 'react' import { Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { scale: number tx: number ty: number } export const defaults: State = { scale: 1, tx: 0, ty: 0, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange(this.state) } onScaleChanged = (scale: number) => { this.setState({ scale }, () => { this.notifyChange() }) } onTxChanged = (tx: number) => { this.setState({ tx }, () => { this.notifyChange() }) } onTyChanged = (ty: number) => { this.setState({ ty }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={7}>scale</Col> <Col span={12}> <Slider min={0.1} max={2} step={0.1} value={this.state.scale} onChange={this.onScaleChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.scale}</div> </Col> </Row> <Row align="middle"> <Col span={7}>translateX</Col> <Col span={12}> <Slider min={-50} max={50} step={1} value={this.state.tx} onChange={this.onTxChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.tx}</div> </Col> </Row> <Row align="middle"> <Col span={7}>translateY</Col> <Col span={12}> <Slider min={-50} max={50} step={1} value={this.state.ty} onChange={this.onTyChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.ty}</div> </Col> </Row> </Card> ) } } ``` #### View - Canonical page: https://x6.antv.antgroup.com/en/api/mvc/view/ - Markdown source: https://x6.antv.antgroup.com/en/api/mvc/view.md - Description: View --- title: "View" description: "View" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/mvc/view/" version: "3.x" --- ## Configuration ### async Whether the canvas is rendered asynchronously. Asynchronous rendering does not block the UI and significantly improves performance when adding a large number of nodes and edges. However, it's important to note that some synchronous operations may produce unexpected results, such as getting the view of a node or getting the bounding box of nodes/edges, because these synchronous operations may be triggered before the asynchronous rendering is complete. ### virtual Whether to render only the elements in the visible area. Supports passing a `boolean` or an object `VirtualOptions`, default is `false`. When set to the boolean value `true`, only the current visible area and the buffer margin (default: `120px`) are rendered. When an object `VirtualOptions` is provided: - `enabled`: Whether to enable virtual rendering. - `margin`: Buffer margin (px). Elements within this margin outside the viewport are pre-rendered to reduce repaint jitter during drag/zoom and improve interaction continuity. Example: ```ts new Graph({ container: el, virtual: { enabled: true, margin: 200 }, }) ``` When dragging, zooming the graph, or scrolling, the remaining elements are dynamically loaded based on the window size. This significantly improves performance in scenarios with a large number of elements. ### magnetThreshold The number of mouse movements required before triggering a connection, or set to 'onleave' to trigger a connection when the mouse leaves the element. Default is `0`. ### moveThreshold The number of mouse movements allowed before triggering the 'mousemove' event, default is `0`. ### clickThreshold When the number of mouse movements exceeds the specified number, the mouse click event will not be triggered. Default is `0`. ### preventDefaultContextMenu Whether to disable the default right-click menu of the canvas, default is `true`. ### preventDefaultBlankAction Whether to disable the default mouse behavior when responding to mouse events in blank areas of the canvas, default is `true`. ### onPortRendered ```ts ( this: Graph, args: { node: Node port: Port container: Element selectors?: Markup.Selectors labelContainer: Element labelSelectors?: Markup.Selectors contentContainer: Element contentSelectors?: Markup.Selectors }, ) => void ``` Callback triggered when a port is rendered, with the following parameters: | Name | Type | Required | Description | |------------------|------------------|:--------:|------------------------------------------------| | node | Node | ✓ | Node instance. | | port | Port | ✓ | Port options. | | container | Element | ✓ | Container element of the port. | | selectors | Markup.Selectors | | Selector key-value pairs after port Markup rendering. | | labelContainer | Element | ✓ | Container element of the port label. | | labelSelectors | Markup.Selectors | | Selector key-value pairs after port label Markup rendering. | | contentContainer | Element | ✓ | Container element of the port content. | | contentSelectors | Markup.Selectors | | Selector key-value pairs after port content Markup rendering. | For example, we can render a React-type port: ```tsx const graph = new Graph({ container: this.container, onPortRendered(args) { const selectors = args.contentSelectors const container = selectors && selectors.foContent if (container) { ReactDOM.render( <Tooltip title="port"> <div className="my-port" /> </Tooltip>, container, ) } }, }) ``` ### onEdgeLabelRendered ```ts type OnEdgeLabelRenderedArgs = { edge: Edge label: Edge.Label container: Element selectors: Markup.Selectors } ( this: Graph, args: OnEdgeLabelRenderedArgs, ) => void | ((args: OnEdgeLabelRenderedArgs) => void) ``` The callback triggered when an edge label is rendered, And it can return a cleanup function, which will be executed when the label is destroyed, with the following parameters: | Name | Type | Required | Description | |-----------|------------------|:--------:|------------------------------------------------| | edge | Edge | ✓ | Edge instance. | | label | Edge.Label | ✓ | Text label options. | | container | Element | ✓ | Text label container. | | selectors | Markup.Selectors | ✓ | Selector key-value pairs after text label Markup rendering. | We can add a `<foreignObject>` element when defining the Label Markup to support HTML and React rendering capabilities. ```tsx const graph = new Graph({ container: this.container, onEdgeLabelRendered: (args) => { const { selectors } = args const content = selectors.foContent as HTMLDivElement if (content) { content.style.display = 'flex' content.style.alignItems = 'center' content.style.justifyContent = 'center' ReactDOM.render(<Button size="small">Antd Button</Button>, content) } // And it can return a cleanup function, which will be executed when the label is destroyed. return (edgeLabelRenderedArgs: typeof args) => { // Remove event listeners... } }, }) // The main purpose of this code is to test the cleanup function of onEdgeLabelRendered in a specific scenario. const edge = graph.addEdge({ source: [170, 160], target: [550, 160], labels: [ { attrs: { text: { text: "Custom Label", }, }, }, ], }); setTimeout(() => { edge.prop("labels", ["Updated Label"]); }, 2000); ``` ### createCellView ```ts (this: Graph, cell: Cell) => CellView | null | undefined ``` Customize the view of an element. It can return a `CellView` to replace the default view. If it returns `null`, the element won't be rendered. If it returns `undefined`, the element will be rendered in the default way. ## Methods ### findView(...) ```ts findView(ref: Cell | Element): CellView | null ``` Find the corresponding view based on the node/edge or element. ### findViewByCell(...) ```ts findViewByCell(cellId: string | number): CellView | null findViewByCell(cell: Cell | null): CellView | null ``` Find the corresponding view based on the node/edge ID or instance. ### findViewByElem(...) ```ts findViewByElem(elem: string | Element | undefined | null): CellView | null ``` Find the corresponding view based on the element selector or element object. ### findViewsFromPoint(...) ```ts findViewsFromPoint(x: number, y: number): CellView[] findViewsFromPoint(p: Point.PointLike): CellView[] ``` Return views of nodes/edges whose bounding boxes contain the specified point. ### findViewsInArea(...) ```ts findViewsInArea( x: number, y: number, width: number, height: number, options?: FindViewsInAreaOptions, ): CellView[] findViewsInArea( rect: Rectangle.RectangleLike, options?: FindViewsInAreaOptions, ): CellView[] ``` Return views of nodes/edges whose bounding boxes intersect with the specified rectangle. When `options.strict` is `true`, the bounding box of the node/edge must completely contain the specified rectangle. ### findViews(...) ```ts findViews(ref: Point.PointLike | Rectangle.RectangleLike): CellView[] ``` Return views of nodes/edges whose bounding boxes contain the specified point or intersect with the specified rectangle. #### Node Anchor - Canonical page: https://x6.antv.antgroup.com/en/api/registry/node-anchor/ - Markdown source: https://x6.antv.antgroup.com/en/api/registry/node-anchor.md - Description: Node Anchor --- title: "Node Anchor" description: "Node Anchor" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/registry/node-anchor/" version: "3.x" --- When connecting to a node, you can specify the anchor point of the connected node using `NodeAnchor`, which, together with the connection point [ConnectionPoint](/en/api/registry/connection-point), determines the starting and ending points of the edge. - Starting point: Draw a reference line from the first path point or the center of the target node (when there is no path point) to the anchor point of the source node, and then calculate the intersection point of the reference line and the graph according to the intersection calculation method specified by [connectionPoint](/en/api/registry/connection-point), which is the starting point of the edge. - Ending point: Draw a reference line from the last path point or the center of the source node (when there is no path point) to the anchor point of the target node, and then calculate the intersection point of the reference line and the graph according to the intersection calculation method specified by [connectionPoint](/en/api/registry/connection-point), which is the ending point of the edge. X6 has built-in the following anchor point definitions. - [center](#center) The center point of the element connected to the edge (default value). - [top](#top) The top center point of the element connected to the edge. - [bottom](#bottom) The bottom center point of the element connected to the edge. - [left](#left) The left center point of the element connected to the edge. - [right](#right) The right center point of the element connected to the edge. - [midSide](#midside) The center point of the nearest side of the element connected to the edge. - [topLeft](#topleft) The top-left corner of the element connected to the edge. - [topRight](#topright) The top-right corner of the element connected to the edge. - [bottomLeft](#bottomleft) The bottom-left corner of the element connected to the edge. - [bottomRight](#bottomright) The bottom-right corner of the element connected to the edge. - [orth](#orth) The orthogonal point. - [nodeCenter](#nodecenter) The center point of the node. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Edge } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private edge1: Edge private edge2: Edge componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, interacting: false, }) const rect1 = graph.addNode({ x: 160, y: 80, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const rect2 = graph.addNode({ x: 460, y: 80, width: 100, height: 40, markup: [ { tagName: 'rect', selector: 'body' }, { tagName: 'rect', selector: 'port' }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, port: { x: 10, y: 10, width: 30, height: 10, strokeWidth: 1, stroke: '#fe8550', }, }, }) const edge1 = graph.addEdge({ source: { x: 100, y: 100 }, target: { cell: rect1.id }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, }) const edge2 = graph.addEdge({ source: { x: 320, y: 100 }, target: { cell: rect2, selector: 'port' }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, }) function animate() { const animateEdge = ( edge: Edge, origin: { x: number; y: number }, radius: number, steps: number, ) => { const p = edge.getSourcePoint() const start = Math.atan2(p.y - origin.y, p.x - origin.x) const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = start + (2 * Math.PI * i) / steps xs.push(origin.x + radius * Math.cos(theta)) ys.push(origin.y + radius * Math.sin(theta)) } edge.animate( { 'source/x': xs, 'source/y': ys, }, { duration: 5000, easing: 'linear', fill: 'forwards', iterations: Infinity, }, ) } const steps = 60 animateEdge(edge1, { x: 210, y: 100 }, 140, steps) animateEdge(edge2, { x: 485, y: 95 }, 120, steps) } animate() edge1.on('animation:finish', animate) this.edge1 = edge1 this.edge2 = edge2 } onAttrsChanged = ({ type }: State) => { this.edge1.prop('target/anchor', { name: type }) this.edge2.prop('target/anchor', { name: type }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="node-anchor-app"> <div className="app-side"> <Settings onChange={this.onAttrsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` You can specify the anchor point when creating an edge: ```ts const edge = graph.addEdge({ source: { cell: 'source-id', anchor: { name: 'midSide', args: { dx: 10, }, }, }, target: { cell: 'target-id', anchor: 'orth', // Simplified writing when there are no parameters }, }) ``` After creation, you can modify the anchor point by calling the `edge.setSource` and `edge.setTarget` methods: ```ts edge.setSource({ cell: 'source-id', anchor: { name: 'midSide', args: { dx: 10, }, }, }) ``` When creating a canvas, you can set the global default anchor point through the `connecting` option: ```ts new Graph({ connecting: { anchor: { name: 'midSide', args: { dx: 10, }, }, }, }) ``` Simplified writing when there are no parameters: ```ts new Graph({ connecting: { anchor: 'midSide', }, }) ``` ## Built-in Anchors ### center The center point of the element, supporting the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|------------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | No | `0` | X-axis offset, supporting absolute offset and percentage relative offset. | | dy | number \| string | No | `0` | Y-axis offset, supporting absolute offset and percentage relative offset. | | rotate | boolean | No | `false` | Whether to use the element's bounding box rotated with the node, defaulting to not considering the rotation angle. | ### top The top center point of the element, supporting the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|------------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | No | `0` | X-axis offset, supporting absolute offset and percentage relative offset. | | dy | number \| string | No | `0` | Y-axis offset, supporting absolute offset and percentage relative offset. | | rotate | boolean | No | `false` | Whether to use the element's bounding box rotated with the node, defaulting to not considering the rotation angle. | ### bottom The bottom center point of the element, supporting the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|------------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | No | `0` | X-axis offset, supporting absolute offset and percentage relative offset. | | dy | number \| string | No | `0` | Y-axis offset, supporting absolute offset and percentage relative offset. | | rotate | boolean | No | `false` | Whether to use the element's bounding box rotated with the node, defaulting to not considering the rotation angle. | ### left The left center point of the element, supporting the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|------------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | No | `0` | X-axis offset, supporting absolute offset and percentage relative offset. | | dy | number \| string | No | `0` | Y-axis offset, supporting absolute offset and percentage relative offset. | | rotate | boolean | No | `false` | Whether to use the element's bounding box rotated with the node, defaulting to not considering the rotation angle. | ### right The right center point of the element, supporting the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|------------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | No | `0` | X-axis offset, supporting absolute offset and percentage relative offset. | | dy | number \| string | No | `0` | Y-axis offset, supporting absolute offset and percentage relative offset. | | rotate | boolean | No | `false` | Whether to use the element's bounding box rotated with the node, defaulting to not considering the rotation angle. | ### midSide The center point of the nearest side of the element, supporting the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|------------------|:-------:|---------|--------------------------------------------------------------------------------------| | padding | number | No | `0` | Offset the center point by `padding` pixels, supporting absolute offset and percentage relative offset. | | rotate | boolean | No | `false` | Whether to use the element's bounding box rotated with the node, defaulting to not considering the rotation angle. | | direction | 'H' \| 'V' | No | - | The direction of the connection point, such as setting to `H` to only connect to the left or right center point of the node, automatically judging based on the position. | ### topLeft The top-left corner of the element, supporting the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|------------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | No | `0` | X-axis offset, supporting absolute offset and percentage relative offset. | | dy | number \| string | No | `0` | Y-axis offset, supporting absolute offset and percentage relative offset. | | rotate | boolean | No | `false` | Whether to use the element's bounding box rotated with the node, defaulting to not considering the rotation angle. | ### topRight The top-right corner of the element, supporting the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|------------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | No | `0` | X-axis offset, supporting absolute offset and percentage relative offset. | | dy | number \| string | No | `0` | Y-axis offset, supporting absolute offset and percentage relative offset. | | rotate | boolean | No | `false` | Whether to use the element's bounding box rotated with the node, defaulting to not considering the rotation angle. | ### bottomLeft The bottom-left corner of the element, supporting the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|------------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | No | `0` | X-axis offset, supporting absolute offset and percentage relative offset. | | dy | number \| string | No | `0` | Y-axis offset, supporting absolute offset and percentage relative offset. | | rotate | boolean | No | `false` | Whether to use the element's bounding box rotated with the node, defaulting to not considering the rotation angle. | ### bottomRight The bottom-right corner of the element, supporting the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|------------------------|:-------:|---------|----------------------------------------------------| | dx | number \| string | No | `0` | X-axis offset, supporting absolute offset and percentage relative offset. | | dy | number \| string | No | `0` | Y-axis offset, supporting absolute offset and percentage relative offset. | | rotate | boolean | No | `false` | Whether to use the element's bounding box rotated with the node, defaulting to not considering the rotation angle. | ### orth The orthogonal point, supporting the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|----------------|:-------:|--------|---------| | padding | number | No | `0` | X-axis offset. | ### nodeCenter The center point of the node, supporting the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|----------------|:-------:|--------|---------| | dx | number | No | `0` | X-axis offset. | | dy | number | No | `0` | Y-axis offset. | ## Custom Anchors An anchor point definition is a function with the following signature, returning an anchor point. ```ts export type Definition<T> = ( this: EdgeView, nodeView: NodeView, magnet: SVGElement, ref: Point.PointLike | SVGElement, args: T, type: Edge.TerminalType, ) => Point ``` | Parameter Name | Parameter Type | Parameter Description | |---------------|-------------------------------|-------------------| | this | EdgeView | The edge view. | | nodeView | NodeView | The node view. | | magnet | SVGElement | The magnet element. | | ref | Point.PointLike \| SVGElement | The reference point/element. | | args | T | The arguments. | | type | Edge.TerminalType | The edge terminal type. | After completing the anchor point definition, we register the anchor point: ```ts Graph.registerAnchor('custom-anchor', ...) ``` After registration, we can use the anchor point by name: ```ts new Graph({ connecting: { anchor: { name: 'custom-anchor', }, }, }) ``` ### docs/demos/api/node-anchor/playground/index.less ```less .node-anchor-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/node-anchor/playground/settings.tsx ```tsx import React from 'react' import { Radio, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { type: string } export class Settings extends React.Component<Props, State> { state: State = { type: 'center', } notifyChange() { this.props.onChange(this.state) } onChange = (e: any) => { this.setState({ type: e.target.value }, () => { this.notifyChange() }) } render() { return ( <Card title="Anchor" size="small" bordered={false} style={{ width: 320 }}> <Row align="middle"> <Col span={22} offset={2}> <Radio.Group onChange={this.onChange} value={this.state.type}> <Radio value="center">center</Radio> <Radio value="nodeCenter">nodeCenter</Radio> <Radio value="orth">orth</Radio> <Radio value="midSide">midSide</Radio> <Radio value="top">top</Radio> <Radio value="bottom">bottom</Radio> <Radio value="left">left</Radio> <Radio value="right">right</Radio> <Radio value="topLeft">topLeft</Radio> <Radio value="topRight">topRight</Radio> <Radio value="bottomLeft">bottomLeft</Radio> <Radio value="bottomRight">bottomRight</Radio> </Radio.Group> </Col> </Row> </Card> ) } } ``` #### Edge Anchor - Canonical page: https://x6.antv.antgroup.com/en/api/registry/edge-anchor/ - Markdown source: https://x6.antv.antgroup.com/en/api/registry/edge-anchor.md - Description: Edge Anchor --- title: "Edge Anchor" description: "Edge Anchor" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/registry/edge-anchor/" version: "3.x" --- When an edge connects to another edge, you can use EdgeAnchor to specify the anchor point on the connected edge. The anchor point, along with the connection point [ConnectionPoint](/en/api/registry/connection-point), determines the starting and ending points of the edge. - Starting point: Draw a reference line from the first path point or the center of the target node (if there are no path points) to the anchor point of the source node, and then calculate the intersection point of the reference line and the graph according to the intersection calculation method specified by [connectionPoint](/en/api/registry/connection-point). This intersection point is the starting point of the edge. - Ending point: Draw a reference line from the last path point or the center of the source node (if there are no path points) to the anchor point of the target node, and then calculate the intersection point of the reference line and the graph according to the intersection calculation method specified by [connectionPoint](/en/api/registry/connection-point). This intersection point is the ending point of the edge. X6 provides the following built-in anchor point definitions: - [ratio](#ratio) The default value, where the anchor point is located at a specified proportion of the connected edge. - [length](#length) The anchor point is located at a specified length from the connected edge. - [closest](#closest) Uses the point closest to the reference point as the anchor point. - [orth](#orth) Orthogonal anchor point. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Edge } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private edge: Edge componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, interacting: false, }) const rect = graph.addEdge({ source: { x: 40, y: 80 }, target: { x: 360, y: 80 }, vertices: [ { x: 120, y: 120 }, { x: 200, y: 40 }, { x: 280, y: 120 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) const edge = graph.addEdge({ source: { x: 100, y: 100 }, target: { cell: rect.id }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, }) function animate() { const origin = { x: 200, y: 100 } const radius = 140 const steps = 60 const p = edge.getSourcePoint() const start = Math.atan2(p.y - origin.y, p.x - origin.x) const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = start + (2 * Math.PI * i) / steps xs.push(origin.x + radius * Math.cos(theta)) ys.push(origin.y + radius * Math.sin(theta)) } edge.animate( { 'source/x': xs, 'source/y': ys, }, { duration: 5000, easing: 'linear', fill: 'forwards', }, ) } animate() edge.on('animation:finish', animate) this.edge = edge } onAttrsChanged = ({ type, value }: State) => { const anchor = type === 'ratio' || type === 'length' ? { name: type, args: { [type]: value, }, } : { name: type } this.edge.prop('target/anchor', anchor) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="edge-anchor-app"> <div className="app-side"> <Settings onChange={this.onAttrsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Built-in Anchor Points ### ratio The anchor point is located at a specified proportion of the connected edge. Supports the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|----------------|---------|--------------|-----------------------| | ratio | number | No | `0.5` | The proportion of the edge length from the starting point, defaulting to the center of the edge length. | ### length The anchor point is located at a specified length from the connected edge. Supports the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|----------------|---------|--------------|-----------------------| | length | number | No | `20` | The length from the starting point of the edge, defaulting to 20px from the starting point. | ### closest Uses the point closest to the reference point as the anchor point. ### orth Orthogonal anchor point. Supports the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|----------------|---------|--------------|-----------------------| | fallbackAt | number \| string | No | `undefined` | When there is no orthogonal point, use the point specified by `fallbackAt` as the anchor point.<br />When `fallbackAt` is a percentage string, it represents the proportion of the edge length from the starting point.<br />When `fallbackAt` is a number, it represents the length from the starting point of the edge. | ## Custom Anchor Points The edge anchor point definition is a function with the following signature, which returns the anchor point. ```ts export type Definition<T> = ( this: EdgeView, view: EdgeView, magnet: SVGElement, ref: Point.PointLike | SVGElement, args: T, ) => Point ``` | Parameter Name | Parameter Type | Parameter Description | |---------------|---------------------------------------|-----------------------| | this | EdgeView | The view of the edge. | | view | EdgeView | The view of the connected edge. | | magnet | SVGElement | The element of the connected edge. | | ref | Point.PointLike \| SVGElement | The reference point/element. | | args | T | The arguments. | After completing the anchor point definition, we register the anchor point: ```ts Graph.registerEdgeAnchor('custom-anchor', ...) ``` After registration, we can use the anchor point by name: ```ts new Graph({ connecting: { edgeAnchor: { name: 'custom-anchor', }, }, }) ``` ### docs/demos/api/edge-anchor/playground/index.less ```less .edge-anchor-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/edge-anchor/playground/settings.tsx ```tsx import React from 'react' import { Radio, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { type: string value: number } export class Settings extends React.Component<Props, State> { state: State = { type: 'ratio', value: 50, } notifyChange() { this.props.onChange(this.state) } onAngleChanged = (value: number) => { this.setState({ value }, () => { this.notifyChange() }) } onChange = (e: any) => { this.setState({ type: e.target.value }, () => { this.notifyChange() }) } render() { return ( <Card title="Anchor" size="small" bordered={false} style={{ width: 320 }}> <Row align="middle"> <Col span={22} offset={2}> <Radio.Group onChange={this.onChange} value={this.state.type}> <Radio value="ratio">ratio</Radio> <Radio value="length">length</Radio> <Radio value="orth">orth</Radio> <Radio value="closest">closest</Radio> </Radio.Group> </Col> </Row> {(this.state.type === 'ratio' || this.state.type === 'length') && ( <Row align="middle"> <Col span={5}> {this.state.type === 'ratio' ? 'ratio' : 'length'} </Col> <Col span={14}> <Slider min={0} max={100} step={1} value={this.state.value} onChange={this.onAngleChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value"> {this.state.value} {this.state.type === 'ratio' ? '%' : ''} </div> </Col> </Row> )} </Card> ) } } ``` #### Connection Point - Canonical page: https://x6.antv.antgroup.com/en/api/registry/connection-point/ - Markdown source: https://x6.antv.antgroup.com/en/api/registry/connection-point.md - Description: Connection Point --- title: "Connection Point" description: "Connection Point" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/registry/connection-point/" version: "3.x" --- The Connection Point and Anchor jointly determine the starting or ending point of an edge. - Starting point: Draw a reference line from the center of the first path point or target node (if there is no path point) to the anchor point of the source node, and calculate the intersection point of the reference line and the shape according to the specified connection point calculation method. This intersection point is the starting point of the edge. - Ending point: Draw a reference line from the center of the last path point or source node (if there is no path point) to the anchor point of the target node, and calculate the intersection point of the reference line and the shape according to the specified connection point calculation method. This intersection point is the ending point of the edge. X6 provides the following built-in connection point calculation methods. - [boundary](#boundary) Default value, intersection with the boundary of the linked shape. - [bbox](#bbox) Intersection with the bounding box of the linked element. - [rect](#rect) Intersection with the rotated rectangular region of the linked element. - [anchor](#anchor) Use the anchor point as the connection point. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Edge, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private ellipse: Node private edge1: Edge private edge2: Edge componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, interacting: false, }) const rect = graph.addNode({ x: 160, y: 160, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const ellipse = graph.addNode({ shape: 'ellipse', x: 460, y: 160, width: 100, height: 40, angle: 45, markup: [ { tagName: 'ellipse', selector: 'body' }, { tagName: 'rect', selector: 'outline' }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', stroke: '#8f8f8f', strokeWidth: 1, }, outline: { refWidth: '100%', refHeight: '100%', strokeWidth: 1, strokeDasharray: '5 5', stroke: '#8f8f8f', fill: 'none', }, }, }) let bbox: Node const updateBBox = () => { if (bbox) { bbox.remove() } const rect = ellipse.findView(graph)!.getBBox() bbox = graph.addNode({ x: rect.x, y: rect.y, width: rect.width, height: rect.height, attrs: { body: { strokeWidth: 1, strokeDasharray: '5 5', stroke: '#8f8f8f', fill: 'none', }, }, }) } ellipse.on('change:angle', updateBBox) updateBBox() const edge1 = graph.addEdge({ source: { x: 100, y: 180 }, target: { cell: rect.id }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, }) const edge2 = graph.addEdge({ source: { x: 320, y: 180 }, target: { cell: ellipse.id }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, targetMarker: 'classic', }, }, }) function animate() { const animateEdge = ( edge: Edge, origin: { x: number; y: number }, radius: number, steps: number, ) => { const p = edge.getSourcePoint() const start = Math.atan2(p.y - origin.y, p.x - origin.x) const xs: number[] = [] const ys: number[] = [] for (let i = 0; i < steps; i += 1) { const theta = start + (2 * Math.PI * i) / steps xs.push(origin.x + radius * Math.cos(theta)) ys.push(origin.y + radius * Math.sin(theta)) } edge.animate( { 'source/x': xs, 'source/y': ys, }, { duration: 5000, easing: 'linear', fill: 'forwards', }, ) } const steps = 60 animateEdge(edge1, { x: 210, y: 180 }, 120, steps) animateEdge(edge2, { x: 485, y: 180 }, 120, steps) } animate() edge1.on('animation:finish', animate) this.edge1 = edge1 this.edge2 = edge2 this.ellipse = ellipse } updateBBox() {} onAttrsChanged = ({ anchor, connectionPoint, angle }: State) => { this.edge1.prop('target/anchor', { name: anchor }) this.edge1.prop('target/connectionPoint', { name: connectionPoint }) this.edge2.prop('target/anchonr', { name: anchor }) this.edge2.prop('target/connectionPoint', { name: connectionPoint }) this.ellipse.rotate(angle, { absolute: true }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="connection-point-app"> <div className="app-side"> <Settings onChange={this.onAttrsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` You can specify the connection point when creating an edge: ```ts const edge = graph.addEdge({ source: { cell: 'source-id', connectionPoint: { name: 'boundary', args: { sticky: true, }, }, }, target: { cell: 'target-id', connectionPoint: 'boundary', // Simplified writing when there are no parameters }, }) ``` After creation, you can modify the connection point by calling the `edge.setSource` and `edge.setTarget` methods: ```ts edge.setSource({ cell: 'source-id', connectionPoint: { name: 'boundary', args: { sticky: true, }, }, }) ``` When creating a canvas, you can set the global default connection point through the `connecting` option: ```ts new Graph({ connecting: { connectionPoint: { name: 'boundary', args: { sticky: true, }, }, }, }) ``` Simplified writing when there are no parameters: ```ts new Graph({ connecting: { connectionPoint: 'boundary', }, }) ``` ## Built-in Connection Points ### boundary Automatically recognize the boundary of the linked shape and calculate the intersection point of the reference line and the anchor point (Anchor). For example, an `<ellipse>` element will be recognized as an ellipse, and the intersection point of the ellipse and the reference line will be calculated. Elements that cannot be recognized (such as `text` or `<path>`) will use the bounding box of the shape as the connection point, which is the same as using `'bbox'`. Supported parameters are as follows: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |--------------------|---------------------------|:-------:|-------------|--------------------------------------------------------------------------------------------------------------------| | offset | number \| Point.PointLike | No | `0` | Offset of the connection point. | | stroked | boolean | No | `true` | Whether to consider the stroke width of the shape. | | insideout | boolean | No | `true` | When the reference line is inside the shape and there is no intersection point, whether to extend the reference line to calculate the intersection point, default is `true`. | | extrapolate | boolean | No | `false` | When the reference line is outside the shape and there is no intersection point, whether to extend the reference line to calculate the intersection point, default is `false`. This parameter has higher priority than `sticky`. | | sticky | boolean | No | `false` | When the reference line is outside the shape and there is no intersection point, whether to use the point on the boundary closest to the reference line as the connection point, default is `false`. | | precision | number | No | `2` | Precision of intersection point calculation. | | selector | string | No | `undefined` | Selector, used to identify an element, and use the boundary of the element to calculate the intersection point. Default uses the first child element that is not in the `<g>` element in the node. | ### anchor Use the anchor point as the connection point, supporting the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|---------------------------|:-------:|--------|-------------| | offset | number \| Point.PointLike | No | `0` | Offset of the connection point. | ### bbox Intersection point of the bounding box of the linked element and the reference line, supporting the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|---------------------------|:-------:|---------|---------------------| | offset | number \| Point.PointLike | No | `0` | Offset of the connection point. | | stroked | boolean | No | `false` | Whether to consider the stroke width of the shape. | ### rect Intersection point of the rotated rectangular region of the linked element and the reference line, supporting the following parameters: | Parameter Name | Parameter Type | Required | Default Value | Parameter Description | |---------------|---------------------------|:-------:|---------|---------------------| | offset | number \| Point.PointLike | No | `0` | Offset of the connection point. | | stroked | boolean | No | `false` | Whether to consider the stroke width of the shape. | ## Custom Connection Points A connection point definition is a function with the following signature, returning the connection point. ```ts export type Definition<T> = ( line: Line, view: NodeView, magnet: SVGElement, args: T, ) => Point ``` | Parameter Name | Parameter Type | Parameter Description | |----------------|-----------------|---------------------------------| | line | Line | Reference line. | | nodeView | NodeView | View of the connected node. | | magnet | SVGElement | Element on the connected node. | | args | T | Parameters. | After completing the connection point definition, we register the connection point: ```ts Graph.registerConnectionPoint('custom-connection-point', ...) ``` After registration, we can use the connection point by name: ```ts new Graph({ connecting: { connectionPoint: 'custom-connection-point' }, }) ``` ### docs/demos/api/connection-point/playground/index.less ```less .connection-point-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/connection-point/playground/settings.tsx ```tsx import React from 'react' import { Radio, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { anchor: string connectionPoint: string angle: number } export class Settings extends React.Component<Props, State> { state: State = { anchor: 'center', connectionPoint: 'boundary', angle: 45, } notifyChange() { this.props.onChange(this.state) } onAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } onAnchorChange = (e: any) => { this.setState({ anchor: e.target.value }, () => { this.notifyChange() }) } onConnectionPointChange = (e: any) => { this.setState({ connectionPoint: e.target.value }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={5}>anchor</Col> </Row> <Row align="middle"> <Col span={22} offset={2}> <Radio.Group onChange={this.onAnchorChange} value={this.state.anchor} > <Radio value="center">center</Radio> <Radio value="nodeCenter">nodeCenter</Radio> <Radio value="orth">orth</Radio> <Radio value="midSide">midSide</Radio> <Radio value="top">top</Radio> <Radio value="bottom">bottom</Radio> <Radio value="left">left</Radio> <Radio value="right">right</Radio> <Radio value="topLeft">topLeft</Radio> <Radio value="topRight">topRight</Radio> <Radio value="bottomLeft">bottomLeft</Radio> <Radio value="bottomRight">bottomRight</Radio> </Radio.Group> </Col> </Row> <Row align="middle"> <Col span={5}>connectionPoint</Col> </Row> <Row align="middle"> <Col span={22} offset={2}> <Radio.Group onChange={this.onConnectionPointChange} value={this.state.connectionPoint} > <Radio value="boundary">boundary</Radio> <Radio value="anchor">anchor</Radio> <Radio value="bbox">bbox</Radio> <Radio value="rect">rect</Radio> </Radio.Group> </Col> </Row> <Row align="middle"> <Col span={5}>angle</Col> <Col span={14}> <Slider min={0} max={360} step={1} value={this.state.angle} onChange={this.onAngleChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.angle}°</div> </Col> </Row> </Card> ) } } ``` #### Port Layout Algorithm - Canonical page: https://x6.antv.antgroup.com/en/api/registry/port-layout/ - Markdown source: https://x6.antv.antgroup.com/en/api/registry/port-layout.md - Description: Port Layout Algorithm --- title: "Port Layout Algorithm" description: "Port Layout Algorithm" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/registry/port-layout/" version: "3.x" --- The port layout algorithm is a function with the following signature, which returns the relative position of the port relative to the node. For example, if a node is located at `{ x: 30, y: 40 }` on the canvas, and the returned position of a port is `{ x: 2, y: 4 }`, then the port will be rendered at `{ x: 32, y: 44 }` on the canvas. ```ts type Definition<T> = ( portsPositionArgs: T[], // layout algorithm parameters specified in the port elemBBox: Rectangle, // bounding box of the node groupPositionArgs: T, // default layout algorithm parameters defined in the group ) => Result[] interface Result { position: Point.PointLike // relative position to the node angle?: number // rotation angle } ``` Note that when configuring the port `ports`, we can only configure the layout algorithm through the `groups` option, and provide optional layout algorithm parameters `args` in `items` to affect the layout result. ```ts graph.addNode( ..., ports: { // port group groups: { group1: { position: { name: 'xxx', // layout algorithm name args: { }, // default parameters of the layout algorithm }, }, }, // port definition items: [ { groups: 'group1', args: { }, // override the default parameters specified in group1 }, ], }, ) ``` Let's take a look at how to use the built-in port layout algorithms and how to customize and register custom layout algorithms. ## Built-in Layouts ### absolute Absolute positioning, specifying the port position through `args`. ```ts interface AbsoluteArgs { x?: string | number y?: string | number angle?: number } ``` | Name | Type | Required | Default Value | Description | |-------|------------------|:----:|--------|----------------------| | x | string \| number | | `0` | Relative position on the X-axis. | | y | string \| number | | `0` | Relative position on the Y-axis. | | angle | number | | `0` | Rotation angle. | When `x` and `y` are percentage strings or between `[0, 1]`, they represent the percentage offset in the width and height directions, otherwise, they represent absolute offsets. ```ts graph.addNode({ ports: { groups: { group1: { position: { name: 'absolute', args: { x: 0, y: 0 }, }, }, }, items: [ { group: 'group1', args: { x: '60%', y: 32, angle: 45, }, }, ], }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ x: 120, y: 48, width: 280, height: 120, markup: [ { tagName: 'rect', selector: 'body', }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, }, ports: { groups: { group1: { attrs: { circle: { r: 6, magnet: true, stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, text: { fontSize: 12, fill: '#888', }, }, position: { name: 'absolute', }, }, }, items: [ { id: 'port1', group: 'group1', args: { x: 0, y: 60 }, attrs: { text: { text: '{ x: 0, y: 60 }' }, }, }, { id: 'port2', group: 'group1', args: { x: 0.6, y: 32, angle: 45 }, markup: [ { tagName: 'path', selector: 'path', }, ], zIndex: 10, attrs: { path: { d: 'M -6 -8 L 0 8 L 6 -8 Z', magnet: true, fill: '#8f8f8f', }, text: { text: '{ x: 0.6, y: 32, angle: 45 }', fill: '#888' }, }, }, { id: 'port3', group: 'group1', args: { x: '100%', y: '100%' }, attrs: { text: { text: "{ x: '100%', y: '100%' }" }, }, label: { position: { name: 'right', }, }, }, ], }, }) } onAttrsChanged = (args: State) => { this.node.portProp('port2', { args, attrs: { text: { text: `{ x: ${args.x}, y: ${args.y}, angle: ${args.angle} }` }, }, } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="port-absolute-app"> <div className="app-side"> <Settings onChange={this.onAttrsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### left, right, top, bottom Ports are evenly distributed along the specified edge of the rectangle, and `left`, `right`, `top`, and `bottom` are four layout algorithms that are very friendly to rectangular nodes. You can set the offset and rotation angle through `args`. ```ts interface SideArgs { dx?: number dy?: number angle?: number x?: number y?: number } ``` | Name | Type | Required | Default Value | Description | |--------|---------|:----:|---------|---------------------------------------| | strict | boolean | | `false` | Whether to strictly distribute evenly. | | dx | number | | `0` | Offset in the X-axis direction. | | dy | number | | `0` | Offset in the Y-axis direction. | | angle | number | | `0` | Rotation angle. | | x | number | | - | Override the calculated X-coordinate with the specified value. | | y | number | | - | Override the calculated Y-coordinate with the specified value. | ```ts graph.addNode({ ports: { groups: { group1: { position: 'left', }, }, items: [ { group: 'group1', args: { dx: 2, }, }, ], }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ x: 160, y: 110, width: 280, height: 120, attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, label: { text: 'left', fill: '#888' }, }, ports: { groups: { group1: { attrs: { circle: { r: 6, magnet: true, stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, text: { fontSize: 12, fill: '#888', }, }, position: { name: 'left', }, }, }, items: [ { id: 'port1', group: 'group1', }, { id: 'port2', group: 'group1', args: { angle: 45 }, markup: [ { tagName: 'path', selector: 'path', }, ], attrs: { path: { d: 'M -6 -8 L 0 8 L 6 -8 Z', magnet: true, fill: '#8f8f8f', }, }, }, { id: 'port3', group: 'group1', args: {}, }, ], }, }) } onAttrsChanged = ({ position, strict, ...args }: State) => { this.node.prop('ports/groups/group1/position', { name: position, args: { strict }, }) this.node.attr('label/text', position) this.node.portProp('port2', { args } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="port-side-app"> <div className="app-side"> <Settings onChange={this.onAttrsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### line Ports are evenly distributed along the line segment. ```ts interface LineArgs { start?: Point.PointLike end?: Point.PointLike dx?: number dy?: number angle?: number x?: number y?: number } ``` | Name | Type | Required | Default Value | Description | |--------|-----------------|:----:|---------|---------------------------------------| | start | Point.PointLike | | | Start point of the line segment. | | end | Point.PointLike | | | End point of the line segment. | | strict | boolean | | `false` | Whether to strictly distribute evenly. | | dx | number | | `0` | Offset in the X-axis direction. | | dy | number | | `0` | Offset in the Y-axis direction. | | angle | number | | `0` | Rotation angle. | | x | number | | - | Override the calculated X-coordinate with the specified value. | | y | number | | - | Override the calculated Y-coordinate with the specified value. | ```ts graph.addNode({ ports: { groups: { group1: { position: { name: 'line', args: { start: { x: 10, y: 10 }, end: { x: 210, y: 10 }, }, }, }, }, items: [ { group: 'group1', args: { dx: 2, }, }, ], }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ x: 160, y: 80, width: 280, height: 120, markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'path', selector: 'line', }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, line: { d: 'M 0 0 280 120', stroke: '#8f8f8f', strokeDasharray: '5 5', }, }, ports: { groups: { group1: { attrs: { circle: { r: 6, magnet: true, stroke: '#8f8f8f', strokeWidth: 2, fill: '#fff', }, text: { fontSize: 12, fill: '#888', }, }, position: { name: 'line', args: { start: { x: 0, y: 0 }, end: { x: 280, y: 120 }, }, }, }, }, items: [ { id: 'port1', group: 'group1', }, { id: 'port2', group: 'group1', args: { angle: 45 }, markup: [ { tagName: 'path', selector: 'path', }, ], attrs: { path: { d: 'M -6 -8 L 0 8 L 6 -8 Z', magnet: true, fill: '#8f8f8f', }, }, }, { id: 'port3', group: 'group1', args: {}, }, ], }, }) } onAttrsChanged = ({ strict, ...args }: State) => { this.node.prop('ports/groups/group1/position', { name: 'line', args: { strict }, }) this.node.portProp('port2', { args } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="port-line-app"> <div className="app-side"> <Settings onChange={this.onAttrsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### ellipse Ports are distributed along the ellipse, starting from the `start` angle, with a step size of `step`. ```ts interface EllipseArgs { start?: number step?: number compensateRotate?: boolean dr?: number dx?: number dy?: number angle?: number x?: number y?: number } ``` | Name | Type | Required | Default Value | Description | |------------------|--------|:----:|---------|---------------------------------------| | start | number | | | Start angle. | | step | number | | `20` | Step size. | | compensateRotate | number | | `false` | Whether to compensate for the rotation angle of the ellipse. | | dr | number | | `0` | Offset in the radial direction. | | dx | number | | `0` | Offset in the X-axis direction. | | dy | number | | `0` | Offset in the Y-axis direction. | | angle | number | | `0` | Rotation angle. | | x | number | | - | Override the calculated X-coordinate with the specified value. | | y | number | | - | Override the calculated Y-coordinate with the specified value. | ```ts const node = graph.addNode({ ports: { groups: { group1: { position: { name: 'ellipse', args: { start: 45, }, }, }, }, }, }) Array.from({ length: 10 }).forEach((_, index) => { node.addPort({ id: `${index}`, group: 'group1', attrs: { text: { text: index } }, }) }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Point } from '@antv/x6' import { Settings, State } from './settings.tsx' function getPathData(deg: number) { const center = new Point(180, 100) const start = new Point(180, 0) const ratio = center.x / center.y const p = start .clone() .rotate(90 - deg, center) .scale(ratio, 1, center) return `M ${center.x} ${center.y} ${p.x} ${p.y}` } export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ x: 120, y: 90, width: 360, height: 200, shape: 'ellipse', markup: [ { tagName: 'ellipse', selector: 'body' }, { tagName: 'text', selector: 'label' }, { tagName: 'path', selector: 'line' }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, line: { d: getPathData(45), stroke: '#8f8f8f', strokeDasharray: '5 5', }, }, ports: { groups: { group1: { markup: [ { tagName: 'rect', selector: 'rect', }, { tagName: 'circle', selector: 'dot', }, ], attrs: { rect: { magnet: true, stroke: '#8f8f8f', fill: 'rgba(255,255,255,0.8)', strokeWidth: 1, width: 16, height: 16, x: -8, y: -8, }, dot: { fill: '#8f8f8f', r: 2, }, text: { fontSize: 12, fill: '#888', }, }, label: { position: 'radial', }, position: { name: 'ellipse', args: { start: 45, }, }, }, }, }, }) Array.from({ length: 10 }).forEach((_, index) => { this.node.addPort({ id: `${index}`, group: 'group1', attrs: { text: { text: index } }, }) }) this.node.portProp('0', { attrs: { rect: { stroke: '#8f8f8f' }, dot: { fill: '#8f8f8f' }, }, }) } onAttrsChanged = ({ start, step, compensateRotate, ...args }: State) => { this.node.prop('ports/groups/group1/position/args', { start, step, compensateRotate, }) this.node.attr({ line: { d: getPathData(start) }, }) this.node.portProp('0', { args, } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="port-ellipse-app"> <div className="app-side"> <Settings onChange={this.onAttrsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### ellipseSpread Uniformly distributes connection points along an ellipse, starting from the specified angle `start`. ```ts interface EllipseSpreadArgs { start?: number compensateRotate?: boolean dr?: number dx?: number dy?: number angle?: number x?: number y?: number } ``` | Name | Type | Required | Default Value | Description | |------------------|--------|:----:|---------|---------------------------------------| | start | number | | | Starting angle. | | compensateRotate | number | | `false` | Whether to adjust the rotation angle of the connection points along the arc. | | dr | number | | `0` | Offset along the radial direction. | | dx | number | | `0` | Offset along the X-axis direction. | | dy | number | | `0` | Offset along the Y-axis direction. | | angle | number | | `0` | Rotation angle of the connection points. | | x | number | | - | Override the X-coordinate of the calculated result with a specified X-coordinate. | | y | number | | - | Override the Y-coordinate of the calculated result with a specified Y-coordinate. | ```ts const node = graph.addNode({ ports: { groups: { group1: { position: { name: 'ellipseSpread', args: { start: 45, }, }, }, }, }, }) Array.from({ length: 36 }).forEach(function (_, index) { ellipse.addPort({ group: 'group1', id: `${index}`, attrs: { text: { text: index } }, }) }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ x: 120, y: 90, width: 360, height: 200, shape: 'ellipse', attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, }, ports: { groups: { group1: { markup: [ { tagName: 'rect', selector: 'rect', }, { tagName: 'circle', selector: 'dot', }, ], attrs: { rect: { magnet: true, stroke: '#8f8f8f', fill: 'rgba(255,255,255,0.8)', strokeWidth: 1, width: 16, height: 16, x: -8, y: -8, }, dot: { fill: '#8f8f8f', r: 2, }, text: { fontSize: 12, fill: '#888', }, }, label: { position: 'radial', }, position: { name: 'ellipseSpread', args: { start: 45, }, }, }, }, }, }) Array.from({ length: 10 }).forEach((_, index) => { this.node.addPort({ id: `${index}`, group: 'group1', attrs: { text: { text: index } }, }) }) this.node.portProp('0', { attrs: { rect: { stroke: '#8f8f8f' }, dot: { fill: '#8f8f8f' }, }, }) } onAttrsChanged = ({ start, compensateRotate, ...args }: State) => { this.node.prop('ports/groups/group1/position/args', { start, compensateRotate, }) this.node.portProp('0', { args, } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="port-ellipse-spread-app"> <div className="app-side"> <Settings onChange={this.onAttrsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Custom Connection Point Layout A connection point layout algorithm is a function with the following signature, which returns the relative position of each connection point relative to the node. For example, if a node is located at `{ x: 30, y: 40 }` on the canvas, and the returned position of a connection point is `{ x: 2, y: 4 }`, then the rendered position of the connection point on the canvas would be `{ x: 32, y: 44 }`. ```ts type Definition<T> = ( portsPositionArgs: T[], // Layout algorithm parameters specified in the connection points elemBBox: Rectangle, // Node bounding box groupPositionArgs: T, // Default layout algorithm parameters defined in the group ) => Result[] interface Result { position: Point.PointLike // Relative position to the node angle?: number // Rotation angle } ``` We can create a custom layout algorithm according to the above rules, for example, implementing a sine distribution layout algorithm: ```ts function sin(portsPositionArgs, elemBBox) { return portsPositionArgs.map((_, index) => { const step = -Math.PI / 8 const y = Math.sin(index * step) * 50 return { position: { x: index * 12, y: y + elemBBox.height, }, angle: 0, } }) } ``` After implementing the layout algorithm, we need to register it to the system. After registration, we can use it like the built-in layout algorithms. ```ts Graph.registerPortLayout('sin', sin) ``` After registration, we can use it like the built-in layout algorithms: ```ts const rect = graph.addNode({ ports: { groups: { sin: { position: { name: 'sin', args: { start: 45, }, }, attrs: { rect: { fill: '#fe854f', width: 11, }, text: { fill: '#fe854f', }, circle: { fill: '#fe854f', r: 5, magnet: true, }, }, }, }, }, }) Array.from({ length: 24 }).forEach(() => { rect.addPort({ group: 'sin' }) }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerPortLayout( 'sin', (portsPositionArgs, elemBBox) => { return portsPositionArgs.map((_, index) => { const step = -Math.PI / 8 const y = Math.sin(index * step) * 50 return { position: { x: index * 12, y: y + elemBBox.height, }, angle: 0, } }) }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const rect = graph.addNode({ x: 120, y: 40, width: 280, height: 120, attrs: { body: { fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, }, ports: { groups: { sin: { attrs: { circle: { r: 6, magnet: true, stroke: '#8f8f8f', strokeWidth: 1, fill: '#fe854f', }, }, position: 'sin', }, }, }, }) Array.from({ length: 24 }).forEach(() => { rect.addPort({ group: 'sin' }) }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="port-sin-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/api/port-layout/absolute/index.less ```less .port-absolute-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-layout/absolute/settings.tsx ```tsx import React from 'react' import { Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { x: number y: number angle: number } export class Settings extends React.Component<Props, State> { state: State = { x: 0.6, y: 32, angle: 45, } notifyChange() { this.props.onChange(this.state) } onXChanged = (x: number) => { this.setState({ x }, () => { this.notifyChange() }) } onYChanged = (y: number) => { this.setState({ y }, () => { this.notifyChange() }) } onAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } render() { return ( <Card title="Args" size="small" bordered={false} style={{ width: 320 }}> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 8 }}> x </Col> <Col span={14}> <Slider min={0} max={0.99} step={0.01} value={this.state.x} onChange={this.onXChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.x}</div> </Col> </Row> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 8 }}> y </Col> <Col span={14}> <Slider min={-20} max={140} step={1} value={this.state.y} onChange={this.onYChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.y}</div> </Col> </Row> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 8 }}> angle </Col> <Col span={14}> <Slider min={0} max={360} step={1} value={this.state.angle} onChange={this.onAngleChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.angle}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/port-layout/side/index.less ```less .port-side-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-layout/side/settings.tsx ```tsx import React from 'react' import { Radio, Switch, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { position: string strict: boolean dx: number dy: number angle: number } export class Settings extends React.Component<Props, State> { state: State = { position: 'left', strict: false, dx: 0, dy: 0, angle: 45, } notifyChange() { this.props.onChange(this.state) } onDxChanged = (dx: number) => { this.setState({ dx }, () => { this.notifyChange() }) } onDyChanged = (dy: number) => { this.setState({ dy }, () => { this.notifyChange() }) } onAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } onStrictChange = (strict: boolean) => { this.setState({ strict }, () => { this.notifyChange() }) } onPositionChange = (e: any) => { this.setState( { position: e.target.value, }, () => { this.notifyChange() }, ) } render() { return ( <Card title="Args" size="small" bordered={false} style={{ width: 320 }}> <Row align="middle"> <Col span={6} style={{ textAlign: 'right', paddingRight: 16 }}> position </Col> <Col span={12}> <Radio.Group value={this.state.position} onChange={this.onPositionChange} > <Radio value={'left'}>left</Radio> <Radio value={'right'}>right</Radio> <Radio value={'top'}>top</Radio> <Radio value={'bottom'}>bottom</Radio> </Radio.Group> </Col> </Row> <Row align="middle" style={{ borderBottom: '1px solid #f0f0f0', paddingBottom: 24 }} > <Col span={6} style={{ textAlign: 'right', paddingRight: 16 }}> strict </Col> <Col span={14}> <Switch checked={this.state.strict} onChange={this.onStrictChange} /> </Col> </Row> <Row align="middle"> <Col span={6} style={{ textAlign: 'right', paddingRight: 16 }}> dx </Col> <Col span={14}> <Slider min={-10} max={10} step={1} value={this.state.dx} onChange={this.onDxChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.dx}</div> </Col> </Row> <Row align="middle"> <Col span={6} style={{ textAlign: 'right', paddingRight: 16 }}> dy </Col> <Col span={14}> <Slider min={-10} max={10} step={1} value={this.state.dy} onChange={this.onDyChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.dy}</div> </Col> </Row> <Row align="middle"> <Col span={6} style={{ textAlign: 'right', paddingRight: 16 }}> angle </Col> <Col span={14}> <Slider min={0} max={360} step={1} value={this.state.angle} onChange={this.onAngleChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.angle}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/port-layout/line/index.less ```less .port-line-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-layout/line/settings.tsx ```tsx import React from 'react' import { Slider, Switch, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { strict: boolean dx: number dy: number angle: number } export class Settings extends React.Component<Props, State> { state: State = { strict: false, dx: 0, dy: 0, angle: 45, } notifyChange() { this.props.onChange(this.state) } onStrictChange = (strict: boolean) => { this.setState({ strict }, () => { this.notifyChange() }) } onDxChanged = (dx: number) => { this.setState({ dx }, () => { this.notifyChange() }) } onDyChanged = (dy: number) => { this.setState({ dy }, () => { this.notifyChange() }) } onAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } render() { return ( <Card title="Args" size="small" bordered={false} style={{ width: 320 }}> <Row align="middle" style={{ borderBottom: '1px solid #f0f0f0', paddingBottom: 24 }} > <Col span={6} style={{ textAlign: 'right', paddingRight: 16 }}> strict </Col> <Col span={14}> <Switch checked={this.state.strict} onChange={this.onStrictChange} /> </Col> </Row> <Row align="middle"> <Col span={6} style={{ textAlign: 'right', paddingRight: 16 }}> dx </Col> <Col span={14}> <Slider min={-10} max={10} step={1} value={this.state.dx} onChange={this.onDxChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.dx}</div> </Col> </Row> <Row align="middle"> <Col span={6} style={{ textAlign: 'right', paddingRight: 16 }}> dy </Col> <Col span={14}> <Slider min={-10} max={10} step={1} value={this.state.dy} onChange={this.onDyChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.dy}</div> </Col> </Row> <Row align="middle"> <Col span={6} style={{ textAlign: 'right', paddingRight: 16 }}> angle </Col> <Col span={14}> <Slider min={0} max={360} step={1} value={this.state.angle} onChange={this.onAngleChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.angle}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/port-layout/ellipse/index.less ```less .port-ellipse-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-layout/ellipse/settings.tsx ```tsx import React from 'react' import { Slider, Checkbox, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { start: number step: number compensateRotate: boolean dr: number dx: number dy: number angle: number } export class Settings extends React.Component<Props, State> { state: State = { start: 45, step: 20, compensateRotate: false, dr: 0, dx: 0, dy: 0, angle: 45, } notifyChange() { this.props.onChange(this.state) } onStartChanged = (start: number) => { this.setState({ start }, () => { this.notifyChange() }) } onStepChanged = (step: number) => { this.setState({ step }, () => { this.notifyChange() }) } onCompensateRotateChange = (e: any) => { this.setState({ compensateRotate: e.target.checked }, () => { this.notifyChange() }) } onDxChanged = (dx: number) => { this.setState({ dx }, () => { this.notifyChange() }) } onDrChanged = (dr: number) => { this.setState({ dr }, () => { this.notifyChange() }) } onDyChanged = (dy: number) => { this.setState({ dy }, () => { this.notifyChange() }) } onAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } render() { return ( <Card title="Args" size="small" bordered={false} style={{ width: 320 }}> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 8 }}> start </Col> <Col span={14}> <Slider min={0} max={360} step={1} value={this.state.start} onChange={this.onStartChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.start}</div> </Col> </Row> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 8 }}> step </Col> <Col span={14}> <Slider min={10} max={36} step={1} value={this.state.step} onChange={this.onStepChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.step}</div> </Col> </Row> <Row align="middle" style={{ borderBottom: '1px solid #f0f0f0', paddingBottom: 24 }} > <Col span={19} offset={5}> <Checkbox onChange={this.onCompensateRotateChange} checked={this.state.compensateRotate} > compensateRotate </Checkbox> </Col> </Row> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 8 }}> dr </Col> <Col span={14}> <Slider min={-10} max={10} step={1} value={this.state.dr} onChange={this.onDrChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.dr}</div> </Col> </Row> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 8 }}> dx </Col> <Col span={14}> <Slider min={-10} max={10} step={1} value={this.state.dx} onChange={this.onDxChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.dx}</div> </Col> </Row> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 8 }}> dy </Col> <Col span={14}> <Slider min={-10} max={10} step={1} value={this.state.dy} onChange={this.onDyChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.dy}</div> </Col> </Row> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 8 }}> angle </Col> <Col span={14}> <Slider min={0} max={360} step={1} value={this.state.angle} onChange={this.onAngleChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.angle}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/port-layout/ellipse-spread/index.less ```less .port-ellipse-spread-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-layout/ellipse-spread/settings.tsx ```tsx import React from 'react' import { Slider, Checkbox, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { start: number compensateRotate: boolean dr: number dx: number dy: number angle: number } export class Settings extends React.Component<Props, State> { state: State = { start: 45, compensateRotate: false, dr: 0, dx: 0, dy: 0, angle: 45, } notifyChange() { this.props.onChange(this.state) } onStartChanged = (start: number) => { this.setState({ start }, () => { this.notifyChange() }) } onCompensateRotateChange = (e: any) => { this.setState({ compensateRotate: e.target.checked }, () => { this.notifyChange() }) } onDxChanged = (dx: number) => { this.setState({ dx }, () => { this.notifyChange() }) } onDrChanged = (dr: number) => { this.setState({ dr }, () => { this.notifyChange() }) } onDyChanged = (dy: number) => { this.setState({ dy }, () => { this.notifyChange() }) } onAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } render() { return ( <Card title="Args" size="small" bordered={false} style={{ width: 320 }}> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 8 }}> start </Col> <Col span={14}> <Slider min={0} max={360} step={1} value={this.state.start} onChange={this.onStartChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.start}</div> </Col> </Row> <Row align="middle" style={{ borderBottom: '1px solid #f0f0f0', paddingBottom: 24 }} > <Col span={19} offset={5}> <Checkbox onChange={this.onCompensateRotateChange} checked={this.state.compensateRotate} > compensateRotate </Checkbox> </Col> </Row> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 8 }}> dr </Col> <Col span={14}> <Slider min={-10} max={10} step={1} value={this.state.dr} onChange={this.onDrChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.dr}</div> </Col> </Row> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 8 }}> dx </Col> <Col span={14}> <Slider min={-10} max={10} step={1} value={this.state.dx} onChange={this.onDxChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.dx}</div> </Col> </Row> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 8 }}> dy </Col> <Col span={14}> <Slider min={-10} max={10} step={1} value={this.state.dy} onChange={this.onDyChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.dy}</div> </Col> </Row> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 8 }}> angle </Col> <Col span={14}> <Slider min={0} max={360} step={1} value={this.state.angle} onChange={this.onAngleChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.angle}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/port-layout/sin/index.less ```less .port-sin-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### Port Label Layout - Canonical page: https://x6.antv.antgroup.com/en/api/registry/port-label-layout/ - Markdown source: https://x6.antv.antgroup.com/en/api/registry/port-label-layout.md - Description: Port Label Layout --- title: "Port Label Layout" description: "Port Label Layout" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/registry/port-label-layout/" version: "3.x" --- The port label layout algorithm is a function with the following signature, returning the position and rotation angle of the label relative to the port. ```ts type Definition<T> = ( portPosition: Point, // port position elemBBox: Rectangle, // node bounding box args: T, // label position arguments ) => Result interface Result { position: Point.PointLike // label position relative to port angle: number // label rotation angle attrs: Attr.CellAttrs // label attributes } ``` When creating a port, you can specify the label layout in the `groups` or override it in `items`: ```ts graph.addNode( ..., ports: { // port group groups: { group1: { label: { position: { // label layout algorithm name: 'xxx', // label layout algorithm name args: { ... }, // label layout algorithm arguments }, }, }, }, // port definition items: [ { groups: 'group1', label: { position: { // override label layout algorithm specified in group1 name: 'xxx', args: { ... }, } }, } ], }, ) ``` Let's take a look at how to use built-in label layout algorithms and how to define and register custom layout algorithms. ## Built-in Port Label Layout ### Side The label is located on one side of the port. - `'left'` The label is located on the left side of the port. - `'right'` The label is located on the right side of the port. - `'top'` The label is located on the top side of the port. - `'bottom'` The label is located on the bottom side of the port. You can specify the label position and rotation angle through `args`. ```ts interface SideArgs { x?: number y?: number angle?: number attrs?: Attr.CellAttrs } ``` | Name | Type | Required | Default Value | Description | |-------|----------------|:----:|--------|-----------------------------------------| | x | number | | - | Replace the calculated X coordinate with the specified value. | | y | number | | - | Replace the calculated Y coordinate with the specified value. | | angle | number | | - | Replace the calculated rotation angle with the specified value. | | attrs | Attr.CellAttrs | | - | Label attributes. | ```ts label: { position: { name : 'right', args: { y: 10, // Force the y-coordinate to be 10, replacing the calculated y-coordinate attrs: { text: { fill: 'red', // Set the label color to red }, }, }, }, } ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ shape: 'rect', x: 80, y: 68, width: 240, height: 80, attrs: { label: { text: 'left', fill: '#6a6c8a', }, body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { a: { position: { name: 'top', args: { dr: 0, dx: 0, dy: -10, }, }, label: { position: { name: 'left', }, }, attrs: { circle: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, r: 10, }, text: { fill: '#6a6c8a', }, }, }, }, }, }) Array.from({ length: 3 }).forEach((_, index) => { const label = index === 2 ? { position: { args: { x: 20, y: -20 } }, } : {} const stroke = index === 2 ? { stroke: 'red' } : {} const fill = index === 2 ? { fill: 'red' } : {} this.node.addPort({ label, group: 'a', attrs: { circle: { magnet: true, ...stroke }, text: { text: `P${index}`, ...fill }, }, }) }) } onAttrsChanged = ({ position }: State) => { this.node.prop('ports/groups/a/label/position/name', position) this.node.attr('label/text', position) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="port-label-side-app"> <div className="app-side"> <Settings onChange={this.onAttrsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### Inside/Outside The label is located inside or outside the node, supporting the following four layouts: - `'inside'` The label is located inside the node, close to the edge. - `'outside'` The label is located outside the node, close to the edge. - `'insideOriented'` The label is located inside the node and automatically adjusts the text direction based on the position. - `'outsideOriented'` The label is located outside the node and automatically adjusts the text direction based on the position. You can set the offset from the node center to the label position through `args.offset`. ```ts interface InOutArgs { offset?: number x?: number y?: number angle?: number attrs?: Attr.CellAttrs } ``` | Name | Type | Required | Default Value | Description | |--------|----------------|:----:|--------|-----------------------------------------| | offset | number | | `15` | Offset from the node center to the label position. | | x | number | | - | Replace the calculated X coordinate with the specified value. | | y | number | | - | Replace the calculated Y coordinate with the specified value. | | angle | number | | - | Replace the calculated rotation angle with the specified value. | | attrs | Attr.CellAttrs | | - | Label attributes. | ```ts label: { position: { name : 'outside', }, } ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ shape: 'ellipse', x: 70, y: 85, width: 260, height: 100, attrs: { label: { text: 'outside', fill: '#888', fontSize: 12, }, body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { a: { position: { name: 'ellipseSpread', args: { compensateRotate: true, }, }, label: { position: { name: 'outside', }, }, attrs: { circle: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, r: 10, magnet: true, }, text: { fill: '#6a6c8a', fontSize: 12, }, }, }, }, }, }) Array.from({ length: 10 }).forEach((_, index) => { this.node.addPort({ attrs: { text: { text: `P ${index}` } }, group: 'a' }) }) } onAttrsChanged = ({ position, offset }: State) => { this.node.prop('ports/groups/a/label/position', { name: position, args: { offset }, }) this.node.attr('label/text', position) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="port-label-inside-outside-app"> <div className="app-side"> <Settings onChange={this.onAttrsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### Radial Place the label on the outer circle or ellipse of the node. Supports the following two layouts: - `'radial'` The label is located on the outer circle or ellipse of the node. - `'radialOriented'` The label is located on the outer circle or ellipse of the node and automatically adjusts the text direction based on the position. ```ts interface RadialArgs { offset?: number x?: number y?: number angle?: number attrs?: Attr.CellAttrs } ``` | Name | Type | Required | Default Value | Description | |--------|----------------|:----:|--------|-----------------------------------------| | offset | number | | `20` | Offset from the node center to the label position. | | x | number | | - | Replace the calculated X coordinate with the specified value. | | y | number | | - | Replace the calculated Y coordinate with the specified value. | | angle | number | | - | Replace the calculated rotation angle with the specified value. | | attrs | Attr.CellAttrs | | - | Label attributes. | ```ts label: { position: { name : 'radial', }, } ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Node componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ shape: 'ellipse', x: 70, y: 50, width: 260, height: 100, attrs: { label: { text: 'outside', fill: '#888', fontSize: 12, }, body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { a: { position: { name: 'ellipseSpread', args: { compensateRotate: true, }, }, label: { position: { name: 'outside', }, }, attrs: { circle: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, r: 10, magnet: true, }, text: { fill: '#6a6c8a', fontSize: 12, }, }, }, }, }, }) Array.from({ length: 10 }).forEach((_, index) => { this.node.addPort({ attrs: { text: { text: `P ${index}` } }, group: 'a' }) }) } onAttrsChanged = ({ position, offset }: State) => { this.node.prop('ports/groups/a/label/position', { name: position, args: { offset }, }) this.node.attr('label/text', position) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="port-label-radial-app"> <div className="app-side"> <Settings onChange={this.onAttrsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Custom Port Label Layout The port label layout algorithm is a function with the following signature, returning the position and rotation angle of the label relative to the port. ```ts type Definition<T> = ( portPosition: Point, // port position elemBBox: Rectangle, // node bounding box args: T, // label position arguments ) => Result interface Result { position: Point.PointLike // label position relative to port angle: number // label rotation angle attrs: Attr.CellAttrs // label attributes } ``` So we can create a custom layout algorithm according to the above rules, for example, implementing a layout that is located at the bottom right of the port: ```ts function bottomRight(portPosition, elemBBox, args) { const dx = args.dx || 10 const dy = args.dy || 10 return { position: { x: portPosition.x + dx, y: portPosition.y + dy, } } } ``` After implementing the layout algorithm, we need to register it to the system. After registration, we can use it like built-in layout algorithms. ```ts Graph.registerPortLabelLayout('bottomRight', bottomRight) ``` After registration, we can use it like built-in layout algorithms: ```ts const rect = graph.addNode({ ports: { groups: { group1: { position: { name: 'top', }, label: { position: { name: 'bottomRight', }, }, }, }, items: [ { id: 'port1', group: 'group1' }, { id: 'port2', label: { position: 'bottomRight' } }, ], }, }) ``` ### docs/demos/api/port-label-layout/side/index.less ```less .port-label-side-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-label-layout/side/settings.tsx ```tsx import React from 'react' import { Radio, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { position: string dx: number dy: number angle: number } export class Settings extends React.Component<Props, State> { state: State = { position: 'left', dx: 0, dy: 0, angle: 45, } notifyChange() { this.props.onChange(this.state) } onDxChanged = (dx: number) => { this.setState({ dx }, () => { this.notifyChange() }) } onDyChanged = (dy: number) => { this.setState({ dy }, () => { this.notifyChange() }) } onAngleChanged = (angle: number) => { this.setState({ angle }, () => { this.notifyChange() }) } onPositionChange = (e: any) => { this.setState( { position: e.target.value, }, () => { this.notifyChange() }, ) } render() { const radioStyle = { display: 'block', height: '36px', lineHeight: '36px', } return ( <Card title="Port Label Position" size="small" bordered={false} style={{ width: 240 }} > <Row align="middle" justify="center"> <Col> <Radio.Group value={this.state.position} onChange={this.onPositionChange} > <Radio style={radioStyle} value={'left'}> left </Radio> <Radio style={radioStyle} value={'right'}> right </Radio> <Radio style={radioStyle} value={'top'}> top </Radio> <Radio style={radioStyle} value={'bottom'}> bottom </Radio> </Radio.Group> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/port-label-layout/inside-outside/index.less ```less .port-label-inside-outside-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-label-layout/inside-outside/settings.tsx ```tsx import React from 'react' import { Radio, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { position: string offset: number } export class Settings extends React.Component<Props, State> { state: State = { position: 'outside', offset: 15, } notifyChange() { this.props.onChange(this.state) } onOffsetChanged = (offset: number) => { this.setState({ offset }, () => { this.notifyChange() }) } onPositionChange = (e: any) => { this.setState( { position: e.target.value, }, () => { this.notifyChange() }, ) } render() { const radioStyle = { display: 'block', height: '36px', lineHeight: '36px', } return ( <Card title="Port Label Position" size="small" bordered={false} style={{ width: 240 }} > <Row align="middle" justify="center" style={{ borderBottom: '1px solid #f0f0f0', paddingBottom: 16, marginBottom: 16, }} > <Col> <Radio.Group value={this.state.position} onChange={this.onPositionChange} > <Radio style={radioStyle} value={'inside'}> inside </Radio> <Radio style={radioStyle} value={'outside'}> outside </Radio> <Radio style={radioStyle} value={'insideOriented'}> insideOriented </Radio> <Radio style={radioStyle} value={'outsideOriented'}> outsideOriented </Radio> </Radio.Group> </Col> </Row> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 16 }}> offset </Col> <Col span={14}> <Slider min={-30} max={30} step={1} value={this.state.offset} onChange={this.onOffsetChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.offset}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/port-label-layout/radial/index.less ```less .port-label-radial-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/port-label-layout/radial/settings.tsx ```tsx import React from 'react' import { Radio, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { position: string offset: number } export class Settings extends React.Component<Props, State> { state: State = { position: 'radial', offset: 20, } notifyChange() { this.props.onChange(this.state) } onOffsetChanged = (offset: number) => { this.setState({ offset }, () => { this.notifyChange() }) } onPositionChange = (e: any) => { this.setState( { position: e.target.value, }, () => { this.notifyChange() }, ) } render() { const radioStyle = { display: 'block', height: '36px', lineHeight: '36px', } return ( <Card title="Port Label Position" size="small" bordered={false} style={{ width: 240 }} > <Row align="middle" justify="center" style={{ borderBottom: '1px solid #f0f0f0', paddingBottom: 16, marginBottom: 16, }} > <Col> <Radio.Group value={this.state.position} onChange={this.onPositionChange} > <Radio style={radioStyle} value={'radial'}> radial </Radio> <Radio style={radioStyle} value={'radialOriented'}> radialOriented </Radio> </Radio.Group> </Col> </Row> <Row align="middle"> <Col span={5} style={{ textAlign: 'right', paddingRight: 16 }}> offset </Col> <Col span={14}> <Slider min={-30} max={50} step={1} value={this.state.offset} onChange={this.onOffsetChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.offset}</div> </Col> </Row> </Card> ) } } ``` #### Attributes - Canonical page: https://x6.antv.antgroup.com/en/api/registry/attr/ - Markdown source: https://x6.antv.antgroup.com/en/api/registry/attr.md - Description: Attributes --- title: "Attributes" description: "Attributes" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/registry/attr/" version: "3.x" --- X6 provides a wide range of built-in attributes as well as methods for custom attributes. ## Built-in Attributes ### ref Specifies a selector pointing to an element, which serves as the reference element. ```typescript graph.addNode({ ..., markup: [ { tagName: 'rect', selector: 'body,' }, { tagName: 'rect', selector: 'custom,' } ], attrs: { body: { width: 100, height: 50, }, // The length and width of the custom element are half of the body custom: { ref: 'body', refWidth: 0.5, refHeight: 0.5, } } }) ``` ### refX Sets the `x` coordinate of the element. The target `x` coordinate is relative to the `x` coordinate of the top-left corner of the element referenced by [`ref`](#ref). - When its value is between `[0, 1]` or a percentage (e.g., `50%`), it represents the relative offset of the target `x` coordinate as a percentage of the reference element's width from the reference `x` coordinate. For example, `refX: 0.5` means the target `x` coordinate is offset to the right by 50% of the reference width from the reference `x` coordinate. - When its value is `<0` or `>1`, it represents the absolute offset of the target `x` coordinate from the reference `x` coordinate. For example, `refX: 20` means the target `x` coordinate is offset 20px to the right from the reference `x` coordinate. ### refX2 Same as [`refX`](#refx), used when both relative and absolute offsets need to be specified simultaneously. ```ts { refX: '50%', refX2: 20, } ``` The above code indicates that the target `x` coordinate is offset 50% of the reference element's width to the right from the reference `x` coordinate, plus an additional 20px. ### refY Sets the `y` coordinate of the element. The target `y` coordinate is relative to the `y` coordinate of the top-left corner of the element referenced by [`ref`](#ref) (reference `y` coordinate). - When its value is between `[0, 1]` or a percentage (e.g., `50%`), it represents the relative offset of the target `y` coordinate as a percentage of the reference element's height from the reference `y` coordinate. For example, `refY: 0.5` means the target `y` coordinate is offset downward by 50% of the reference height from the reference `y` coordinate. - When its value is `<0` or `>1`, it represents the absolute offset of the target `y` coordinate from the reference `y` coordinate. For example, `refY: 20` means the target `y` coordinate is offset 20px downward from the reference `y` coordinate. ### refY2 Same as [`refY`](#refy), used when both relative and absolute offsets need to be specified simultaneously. ```ts { refY: '50%', refY2: 20, } ``` The above code indicates that the target `y` coordinate is offset 50% of the reference element's height downward from the reference `y` coordinate, plus an additional 20px. ### refDx Sets the `x` coordinate of the element. The target `x` coordinate is relative to the `x` coordinate of the bottom-right corner of the element referenced by [`ref`](#ref) (reference `x` coordinate). - When its value is between `[0, 1]` or a percentage (e.g., `50%`), it represents the relative offset of the target `x` coordinate as a percentage of the reference element's width from the reference `x` coordinate. For example, `refDx: 0.5` means the target `x` coordinate is offset to the right by 50% of the reference width from the reference `x` coordinate. - When its value is `<0` or `>1`, it represents the absolute offset of the target `x` coordinate from the reference `x` coordinate. For example, `refDx: 20` means the target `x` coordinate is offset 20px to the right from the reference `x` coordinate. ### refDy Sets the `y` coordinate of the element. The target `y` coordinate is relative to the `y` coordinate of the bottom-right corner of the element referenced by [`ref`](#ref) (reference `y` coordinate). - When its value is between `[0, 1]` or a percentage (e.g., `50%`), it represents the relative offset of the target `y` coordinate as a percentage of the reference element's height from the reference `y` coordinate. For example, `refDy: 0.5` means the target `y` coordinate is offset downward by 50% of the reference height from the reference `y` coordinate. - When its value is `<0` or `>1`, it represents the absolute offset of the target `y` coordinate from the reference `y` coordinate. For example, `refDy: 20` means the target `y` coordinate is offset 20px downward from the reference `y` coordinate. ### refWidth Sets the width of the element. The width is calculated relative to the width of the element referenced by [`ref`](#ref) (reference width). - When its value is between `[0, 1]` or a percentage (e.g., `75%`), it represents the element's width as a percentage of the reference width. For example, `refWidth: 0.75` means the element's width is 75% of the reference width. - When its value is `<0` or `>1`, it represents how much the element's width is decreased or increased based on the reference width. For example, `refWidth: 20` means the element is 20px wider than the reference element. > **Note** This attribute only applies to elements that support width and height, such as the `<rect>` element. ### refWidth2 Same as [`refWidth`](#refwidth), used when both absolute and relative widths need to be specified simultaneously. ```ts { refWidth: '75%', refWidth2: 20, } ``` The above code indicates that the target width is 75% of the reference width plus an additional 20px. ### refHeight Sets the height of the element. The height is calculated relative to the height of the element referenced by [`ref`](#ref) (reference height). - When its value is between `[0, 1]` or a percentage (e.g., `75%`), it represents the element's height as a percentage of the reference height. For example, `refHeight: 0.75` means the element's height is 75% of the reference height. - When its value is `<0` or `>1`, it represents how much the element's height is decreased or increased based on the reference height. For example, `refHeight: 20` means the element is 20px taller than the reference element. > **Note** This attribute only applies to elements that support width and height, such as the `<rect>` element. ### refHeight2 Same as [`refHeight`](#refheight), used when both absolute and relative heights need to be specified simultaneously. ```ts { refHeight: '75%', refHeight2: 20, } ``` The above code indicates that the target height is 75% of the reference height plus an additional 20px. ### refCx Sets the center `x` coordinate of the element, i.e., the [native `cx` attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/cx). The target value is calculated relative to the width of the element referenced by [`ref`](#ref) (reference width). - When its value is between `[0, 1]` or a percentage (e.g., `75%`), it represents the element's `cx` as a percentage of the reference width. For example, `refCx: 0.75` means the element's center `x` coordinate is at 75% of the reference width. - When its value is `<0` or `>1`, it represents how much the element's `cx` is decreased or increased based on the reference width. For example, `refCx: 20` means the element's center `x` coordinate is at the reference width plus 20px. > **Note** This attribute only applies to elements that support `cx` and `cy` attributes, such as the `<ellipse>` element. ### refCy Sets the center `y` coordinate of the element, i.e., the [native `cy` attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/cy). The target value is calculated relative to the height of the element referenced by [`ref`](#ref) (reference height). - When its value is between `[0, 1]` or a percentage (e.g., `75%`), it represents the element's `cy` as a percentage of the reference height. For example, `refCy: 0.75` means the element's center `y` coordinate is at 75% of the reference height. - When its value is `<0` or `>1`, it represents how much the element's `cy` is decreased or increased based on the reference height. For example, `refCy: 20` means the element's center `y` coordinate is at the reference height plus 20px. > **Note** This attribute only applies to elements that support `cx` and `cy` attributes, such as the `<ellipse>` element. ### refRx Sets the [`rx` attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/rx) of the element. The target value is calculated relative to the width of the element referenced by [`ref`](#ref) (reference width). - When its value is between `[0, 1]` or a percentage (e.g., `75%`), it represents the element's `rx` as a percentage of the reference width. For example, `refRx: 0.75` means the element's `rx` is 75% of the reference width. - When its value is `<0` or `>1`, it represents how much the element's `rx` is decreased or increased based on the reference width. For example, `refRx: 20` means the element's `rx` is the reference width plus 20px. > **Note** This attribute only applies to elements that support `rx` and `ry` attributes, such as the `<rect>` element. ### refRy Sets the [`ry` attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/ry) of the element. The target value is calculated relative to the height of the element referenced by [`ref`](#ref) (reference height). - When its value is between `[0, 1]` or a percentage (e.g., `75%`), it represents the element's `ry` as a percentage of the reference height. For example, `refRy: 0.75` means the element's `ry` is 75% of the reference height. - When its value is `<0` or `>1`, it represents how much the element's `ry` is decreased or increased based on the reference height. For example, `refRy: 20` means the element's `ry` is the reference height plus 20px. > **Note** This attribute only applies to elements that support `rx` and `ry` attributes, such as the `<rect>` element. ### refRCircumscribed Sets the [`r` attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/r) of the element. The target value is relative to the **diagonal length** of the element referenced by [`ref`](#ref) (reference length). - When its value is between `[0, 1]` or a percentage (e.g., `75%`), it represents `r` as a percentage of the reference length. For example, `refRCircumscribed: 0.75` means `r` is 75% of the reference length. - When its value is `<0` or `>1`, it represents how much `r` is decreased or increased based on the reference length. For example, `refRCircumscribed: 20` means `r` is the reference length plus 20px. > **Note** This attribute only applies to elements that support the `r` attribute, such as the `<rect>` element. ### refRInscribed _Alias_: **`refR`** Sets the [`r` attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/r) of the element. The target value is relative to the **minimum of the width and height** of the element referenced by [`ref`](#ref) (reference length). - When its value is between `[0, 1]` or a percentage (e.g., `75%`), it represents `r` as a percentage of the reference length. For example, `refRInscribed: 0.75` means `r` is 75% of the reference length. - When its value is `<0` or `>1`, it represents how much `r` is decreased or increased based on the reference length. For example, `refRInscribed: 20` means `r` is the reference length plus 20px. > **Note** This attribute only applies to elements that support the `r` attribute, such as the `<rect>` element. ### refDKeepOffset Sets the [`d` attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) of the `<path>` element. It scales the original pathData to make the target `<path>` element the same size as the element referenced by [`ref`](#ref), and translates the original pathData to align the starting point of the target `<path>` element with the starting point of the element referenced by [`ref`](#ref). At the same time, the offset of the provided pathData will be preserved, which means that if the top-left corner of the provided pathData is not at the coordinate origin `0, 0`, this offset will be preserved when the `<path>` element is rendered on the canvas. ```ts import { Graph, Node } from '@antv/x6' const Path = Node.define({ markup: [{ tagName: 'path' }], attrs: { path: { refDKeepOffset: 'M 10 10 30 10 30 30 z', // path offset is 10,10 fill: 'red', stroke: 'black', }, }, }) const container = document.getElementById('container')! const graph = new Graph({ container, width: 800, height: 80, grid: true, }) const path = new Path().resize(40, 40).addTo(graph) const view = graph.findView(path) console.log(view.findOne('path').getAttribute('d')) // 'M 10 10 50 10 50 50 z' ``` ### refDResetOffset _Alias_: **`refD`** Sets the [`d` attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) of the `<path>` element. It scales the original pathData to make the target `<path>` element the same size as the element referenced by [`ref`](#ref), and translates the original pathData to align the starting point of the target `<path>` element with the starting point of the element referenced by [`ref`](#ref). At the same time, the offset of the provided pathData will be removed, which means that if the top-left corner of the provided pathData is not at the coordinate origin `0, 0`, it will also be translated to the origin, and when the `<path>` element is rendered on the canvas, it will be strictly aligned with the reference element. ```ts import { Graph, Node } from '@antv/x6' const Path = Node.define({ markup: [{ tagName: 'path' }], attrs: { path: { refDResetOffset: 'M 10 10 30 10 30 30 z', // path offset is 10,10 fill: 'red', stroke: 'black', }, }, }) const container = document.getElementById('container')! const graph = new Graph({ container, width: 800, height: 80, grid: true, }) const path = new Path().resize(40, 40).addTo(graph) const view = graph.findView(path) console.log(view.findOne('path').getAttribute('d')) // 'M 0 0 40 0 40 40 z' ``` ### resetOffset When the `resetOffset` property value is `true`, the point matrix will be translated so that the top-left corner of the matrix is at the origin. ```ts path.attr({ path: { d: 'M 10 10 20 20', resetOffset: true, // The d attribute value after translation is "M 0 0 10 10" }, }) ``` ### refPointsKeepOffset Sets the [points attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/points) of `<polygon>` or `<polyline>` elements by scaling the original point matrix to make the target element the same size as the reference element specified by [`ref`](#ref), and translating the original point matrix to align the starting coordinates of the target element with the starting coordinates of the reference element specified by [`ref`](#ref). At the same time, the offset of the point matrix will be preserved, which means that if the top-left corner of the point matrix is not at the coordinate origin `0, 0`, this offset will be preserved when the element is rendered on the canvas. ```ts import { Graph, Node } from '@antv/x6' const Polygon = Node.define({ markup: [{ tagName: 'polygon' }], attrs: { polygon: { refPointsKeepOffset: '10,10 30,10 30,30', // points offset is 10,10 fill: 'red', stroke: 'black', }, }, }) const container = document.getElementById('container')! const graph = new Graph({ container, width: 800, height: 80, grid: true, }) const polygon = new Polygon().resize(40, 40).addTo(graph) const view = graph.findView(polygon) console.log(view.findOne('polygon').getAttribute('points')) // '10,10 50,10 50,50' ``` ### refPointsResetOffset _Alias_: **`refPoints`** Sets the [points attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/points) of `<polygon>` or `<polyline>` elements by scaling the original point matrix to make the target element the same size as the reference element specified by [`ref`](#ref), and translating the original point matrix to align the starting coordinates of the target element with the starting coordinates of the reference element specified by [`ref`](#ref). At the same time, the offset of the point matrix will be removed, which means that if the top-left corner of the point matrix is not at the coordinate origin `0, 0`, it will be translated to the origin simultaneously. When the `<path>` element is rendered on the canvas, it will be strictly aligned with the reference element. ```ts import { Graph, Node } from '@antv/x6' const Polygon = Node.define({ markup: [{ tagName: 'polygon' }], attrs: { polygon: { refPointsResetOffset: '10,10 30,10 30,30', // points offset is 10,10 fill: 'red', stroke: 'black', }, }, }) const container = document.getElementById('container')! const graph = new Graph({ container, width: 800, height: 80, grid: true, }) const polygon = new Polygon().resize(40, 40).addTo(graph) const view = graph.findView(polygon) console.log(view.findOne('polygon').getAttribute('points')) // '100,0 40,0 40,40' ``` ### xAlign The horizontal alignment of the element with its `x` coordinate. - `'left'` The left side of the target element aligns with `x`. - `'middle'` The center of the target element aligns with `x`. - `'right'` The right side of the target element aligns with `x`. ### yAlign The vertical alignment of the element with its `y` coordinate. - `'top'` The top of the target element aligns with `y`. - `'middle'` The center of the target element aligns with `y`. - `'bottom'` The bottom of the target element aligns with `y`. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Cell } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Cell componentDidMount() { const graph = new Graph({ container: this.container, width: 400, height: 250, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ x: 60, y: 60, width: 280, height: 120, markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'path', selector: 'hLine', }, { tagName: 'path', selector: 'vLine', }, { tagName: 'rect', selector: 'ref', }, { tagName: 'circle', selector: 'dot', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, }, dot: { r: 3, fill: '#8f8f8f', stroke: 'none', refX: 0.5, refY: 0.5, }, ref: { width: 70, height: 30, refX: 0.5, refY: 0.5, // xAlign: 'middle', // yAlign: 'middle', fill: '#8f8f8f', stroke: '#8f8f8f', }, label: { fontSize: 14, fill: '#333333', fontFamily: 'Arial, helvetica, sans-serif', }, hLine: { refY: 0.5, d: 'M -30 0 310 0', stroke: '#8f8f8f', strokeDasharray: '5 5', }, vLine: { refX: 0.5, d: 'M 0 -30 0 150', stroke: '#8f8f8f', strokeDasharray: '5 5', }, }, }) } onAttrsChanged = (attrs: State) => { this.node.attr({ ref: attrs, hLine: { refY: attrs.refY }, vLine: { refX: attrs.refX }, } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="attrs-x-align-app"> <div className="app-left"> <Settings onChange={this.onAttrsChanged} /> </div> <div ref={this.refContainer} className="app-content" /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### fill When the provided `fill` attribute value is an object, it indicates using a gradient fill; otherwise, it uses a string color fill. ```ts rect.attr('body/fill', { type: 'linearGradient', stops: [ { offset: '0%', color: '#E67E22' }, { offset: '20%', color: '#D35400' }, { offset: '40%', color: '#E74C3C' }, { offset: '60%', color: '#C0392B' }, { offset: '80%', color: '#F39C12' }, ], }) ``` ### filter When the provided `filter` attribute value is an object, it indicates using a custom filter; otherwise, it uses the native string form (e.g., `"url(#myfilter)"`). ```ts rect.attr('body/filter', { name: 'dropShadow', args: { dx: 2, dy: 2, blur: 3, }, }) ``` ### stroke When the provided `stroke` attribute value is an object, it indicates using a gradient fill; otherwise, it uses a string color fill. The usage is the same as the [fill](#fill) attribute. ### style Applies inline CSS styles to the specified element using jQuery's [`css()`](https://api.jquery.com/css) method. ### html Sets the innerHTML of the specified element using jQuery's [`html()`](https://api.jquery.com/html) method. ### title Adds a `<title>` child element to the specified element. The `<title>` element does not affect the rendering result but only adds a descriptive explanation. ```ts rect.attr('body/title', 'Description of the rectangle') ``` ### text Only applicable to `<text>` elements, used to set the text content. If the provided text is a single line (does not contain newline characters `'\n'`), the text is directly set as the content of the `<text>` element; otherwise, a `<tspan>` element is created for each line of text and then added to the `<text>` element. ### textWrap Only applicable to `<text>` elements, used to set the text content. Unlike the [`text`](#text) attribute, this attribute automatically adds line breaks to the text, making the provided text completely enclosed within the bounding box of the reference element. Its attribute value is a simple object, specifying the text content through `text`. Optional `width` and `height` options can be provided to adjust the size of the element. When negative, it indicates reducing the corresponding width or height (equivalent to setting a padding margin for the text); when positive, it increases the corresponding width or height; when a percentage, it indicates how much percentage of the reference element's width or height. When the provided text exceeds the display range, the text will be automatically truncated. If the `ellipsis` option is set to `true`, an ellipsis `...` will be added at the end of the truncated text. By default, English words will be truncated. If you don't want a complete word to be truncated, you can set `breakWord: false`. In this case, to display the complete word, the text may exceed the width range. ```ts textWrap: { text: 'lorem ipsum dolor sit amet consectetur adipiscing elit', width: -10, // Width reduced by 10px height: '50%', // Height is half of the reference element's height ellipsis: true, // Automatically add ellipsis when text exceeds display range breakWord: true, // Whether to truncate words } ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import * as React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addNode({ x: 40, y: 40, width: 160, height: 48, attrs: { body: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, rx: 5, ry: 5, }, label: { textAnchor: 'left', refX: 8, text: 'hello test foo bar lint css jsvascript typescript', textWrap: { width: 144, height: 40, ellipsis: true, }, }, }, }) graph.addNode({ x: 240, y: 40, width: 160, height: 48, attrs: { body: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, rx: 5, ry: 5, }, label: { textAnchor: 'left', refX: 8, text: 'hello test foo bar 中文字符 lint css jsvascript typescript', textWrap: { width: 144, height: 40, ellipsis: true, }, }, }, }) graph.addNode({ x: 440, y: 40, width: 160, height: 48, attrs: { body: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, rx: 5, ry: 5, }, label: { textAnchor: 'left', refX: 8, text: 'hello-test-foo-bar-lint-css-jsvascript-typescript', textWrap: { width: 144, height: 40, ellipsis: true, }, }, }, }) graph.addNode({ x: 40, y: 128, width: 160, height: 48, attrs: { body: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, rx: 5, ry: 5, }, label: { textAnchor: 'left', refX: 8, text: 'Thisissomeveryveryverylong word. Words will break according to usual rules.', textWrap: { width: 144, height: 40, ellipsis: true, }, }, }, }) graph.addNode({ x: 240, y: 128, width: 160, height: 48, attrs: { body: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, rx: 5, ry: 5, }, label: { textAnchor: 'left', refX: 8, text: 'Thisissomeveryveryverylong word. Words will break according to usual rules.', textWrap: { width: 144, height: 40, ellipsis: true, breakWord: false, }, }, }, }) graph.addNode({ x: 440, y: 128, width: 160, height: 48, attrs: { body: { fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, rx: 5, ry: 5, }, label: { textAnchor: 'left', refX: 8, text: 'hello test foo bar lint css jsvascript typescript', textWrap: { width: 144, height: 20, ellipsis: true, }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="attrs-text-wrap-app"> <div ref={this.refContainer} className="app-content" /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### textPath Only applicable to `<text>` elements, used to render text along a path. When the provided attribute value is a string, it indicates that the text is rendered along the path represented by the string (pathData). When the provided attribute value is an object, you can specify the rendering path of the text through the `d` option, or specify an SVGPathElement element in a node/edge through the `selector` option, supporting CSS selectors and selectors defined in [Markup](). You can also specify the position of the text on the path through the `startOffset` option, for example, `50%` indicates that the text is at 50% along the path, and `20` indicates that the text is 20px offset from the start of the path. ### lineHeight Only applicable to `<text>` elements, used to specify the [line height](https://www.w3.org/TR/SVG/text.html#LineHeightProperty) of the text. ### textVerticalAnchor Only applicable to `<text>` elements, the vertical alignment of the element with its `y` coordinate. - `'top'` The top of the target element aligns with `y`. - `'middle'` The center of the target element aligns with `y`. - `'bottom'` The bottom of the target element aligns with `y`. ```ts import './index.less' import { createRoot } from 'react-dom/client' import * as React from 'react' import { Graph, Cell } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private node: Cell componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = graph.addNode({ x: 120, y: 80, width: 200, height: 80, markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'path', selector: 'hLine', }, { tagName: 'path', selector: 'vLine', }, { tagName: 'rect', selector: 'bg', }, { tagName: 'text', selector: 'label', }, { tagName: 'circle', selector: 'dot', }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#ffffff', stroke: '#8f8f8f', strokeWidth: 1, }, dot: { r: 2, fill: '#8f8f8f', stroke: 'none', refX: 0.5, refY: 0.5, }, bg: { ref: 'label', fill: '#8f8f8f', stroke: '#8f8f8f', rx: 2, ry: 2, refWidth: 1, refHeight: 1, refX: 0, refY: 0, }, label: { fontSize: 14, refX: 0.5, refY: 0.5, textAnchor: 'start', textVerticalAnchor: 'top', fill: '#fff', stroke: '#fff', fontFamily: 'Arial, helvetica, sans-serif', text: 'Hello World', }, hLine: { refY: 0.5, d: 'M -40 0 240 0', stroke: '#8f8f8f', strokeDasharray: '5 5', }, vLine: { refX: 0.5, d: 'M 0 -40 0 120', stroke: '#8f8f8f', strokeDasharray: '5 5', }, }, }) } onGridChanged = (attrs: State) => { this.node.attr({ label: attrs, hLine: { refY: attrs.refY }, vLine: { refX: attrs.refX }, } as any) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="attrs-text-anchor-app"> <div className="app-left"> <Settings onChange={this.onGridChanged} /> </div> <div ref={this.refContainer} className="app-content" /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### connection Only applicable to the `<path>` element of edges. When this attribute is `true`, it indicates that the edge will be rendered on this element, i.e., setting the [`d`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) of this element to the pathData of the edge. ```ts edge.attr('pathSelector', { connection: true, stroke: 'red', fill: 'none', }) ``` It also supports an object with `stubs` and `reserve` options. - When `reverse` is `false`: - When `stubs` is positive, it indicates the length of the rendered start and end portions. For example, `connection: { stubs: 20 }` means only the first `20px` and last `20px` of the connection are rendered, and the rest is not rendered. - When `stubs` is negative, it indicates the length of the middle missing (not rendered) portion. For example, `connection: { stubs: -20 }` means there is a `20px` gap in the middle of the connection that is not rendered. - When `reverse` is `true`: - When `stubs` is positive, it indicates the length of the start and end portions that are not rendered. For example, `connection: { stubs: 20 }` means the first `20px` and last `20px` are not rendered. - When `stubs` is negative, it indicates the length of the middle portion. For example, `connection: { stubs: -20 }` means only the middle `20px` is rendered. ```ts edge.attr('pathSelector', { connection: { stubs: -20, reverse: true }, }) ``` ### atConnectionLengthKeepGradient _Alias_: **`atConnectionLength`** Moves the specified element in the edge to the position at the specified offset and automatically rotates the element so that its direction is consistent with the slope of the edge at that position. - When positive, it indicates the offset from the start point of the edge. - When negative, it indicates the offset from the end point of the edge. ```ts edge.attr('rectSelector', { atConnectionLengthKeepGradient: 30, // atConnectionLength: 30, width: 10, height: 10, fill: 'red', }) ``` ### atConnectionLengthIgnoreGradient Move the specified element in the edge to the position at the specified offset, ignoring the direction of the edge. This means it will not automatically rotate the element like the [`atConnectionLengthKeepGradient`](#atconnectionlengthkeepgradient) property. - When positive, it represents the offset from the start point of the edge. - When negative, it represents the offset from the end point of the edge. ```ts edge.attr('rectSelector', { atConnectionLengthIgnoreGradient: 30, width: 10, height: 10, fill: 'red', }) ``` ### atConnectionRatioKeepGradient _Alias_: **`atConnectionRatio`** Moves the specified element in the edge to the specified ratio position `[0, 1]`, and automatically rotates the element to keep its direction consistent with the slope of the edge at that position. ```ts edge.attr('rectSelector', { atConnectionRatioKeepGradient: 0.5, // atConnectionRatio: 0.5, width: 10, height: 10, fill: 'red', }) ``` ### atConnectionRatioIgnoreGradient Moves the specified element in the edge to the specified ratio position `[0, 1]`, ignoring the direction of the edge, i.e., it will not automatically rotate the element like the [`atConnectionRatioKeepGradient`](#atconnectionratiokeepgradient) property. ```ts edge.attr('rectSelector', { atConnectionRatioIgnoreGradient: 0.5, width: 10, height: 10, fill: 'red', }) ``` ### sourceMarker Applicable to all `<path>` elements, adds an SVG element (such as a start arrow) at the start point of the path and automatically rotates the element to keep it consistent with the path direction. For more details, please refer to [this tutorial](/en/api/model/marker). ```ts edge.attr('connection/sourceMarker', { tagName: 'circle', fill: '#666', stroke: '#333', r: 5, cx: 5, }) ``` ### targetMarker Applicable to all `<path>` elements, adds an SVG element (such as an end arrow) at the end point of the path and automatically rotates the element to keep it consistent with the path direction. For more details, please refer to [this tutorial](/en/api/model/marker). > **Note** This element is initially rotated by `180` degrees, and then the rotation angle is automatically adjusted to be consistent with the direction of the path. For example, for a horizontal straight line, if we specify a left-pointing arrow for its start point, we can also specify the same arrow for its end point, and this arrow will automatically point to the right side (automatically rotated by `180` degrees). ### vertexMarker Applicable to all `<path>` elements, used in the same way as [`sourceMarker`](#sourcemarker), adds additional elements at all vertex positions of the `<path>` element. ### magnet When the `magnet` attribute is `true`, it indicates that the element can be connected, i.e., it can be used as the start or end point of a connection during the connection process, similar to a connection port. ### port Specifies a port ID for elements marked as [`magnet`](#magnet). When an edge connects to this element, this ID will be saved in the `source` or `target` of the edge. - If it's a string, the string will be used as the port ID. - If it's an object, the `id` property value of the object will be used as the port ID. ### event Customizes a click event on the specified element, and then you can add a callback for this event on the Graph. ```ts node.attr({ // Represents a delete button, clicking it will delete the node image: { event: 'node:delete', xlinkHref: 'trash.png', width: 20, height: 20, }, }) // Bind event callback, delete the node when triggered graph.on('node:delete', ({ view, e }) => { e.stopPropagation() view.cell.remove() }) ``` ### xlinkHref Alias for the [`xlink:href`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href) attribute, for example: ```ts node.attr({ image: { xlinkHref: 'xxx.png', }, }) ``` ### xlinkShow Alias for the [`xlink:show`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:show) attribute. ### xlinkType Alias for the [`xlink:type`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:type) attribute. ### xlinkTitle Alias for the [`xlink:title`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:title) attribute. ### xlinkArcrole Alias for the [`xlink:arcrole`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:arcrole) attribute. ### xmlSpace Alias for the [`xml:space`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xml:space) attribute. ### xmlBase Alias for the [`xml:base`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xml:base) attribute. ### xmlLang Alias for the [`xml:lang`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xml:lang) attribute. ## registry We provide two methods, [`register`](#register) and [`unregister`](#unregister), on the `registry` object to register and delete special attributes. ### register ```ts register(entities: { [name: string]: Definition }, force?: boolean): void register(name: string, entity: Definition, force?: boolean): Definition ``` Registers custom attributes. ### unregister ```ts unregister(name: string): Definition | null ``` Deletes registered attributes. At the same time, we mount the [`register`](#register) and [`unregister`](#unregister) methods as two static methods of `Graph`: `Graph.registerAttr` and `Graph.unregisterAttr`. It is recommended to use these two static methods to register and delete special attributes. ### Definition In the internal implementation, we convert special attributes into attributes that the browser can recognize, so theoretically, the value of special attributes can be of any type, and the definition of special attributes also supports multiple forms. #### String Defines aliases for native attributes. For example, the definition of the special attribute `xlinkHref` is actually defining a more easily written alias for the [`xlink:href`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href) attribute. ```ts // Definition Graph.registerAttr('xlinkHref', 'xlink:href') // Usage node.attr({ image: { xlinkHref: 'xxx.png', }, }) ``` Before continuing to introduce attribute definitions, let's understand the attribute qualification `qualify` function. Only attribute values that pass the qualification function's judgment will be processed by special attributes. For example, the [`stroke`](#stroke) attribute is only treated as a special attribute (using gradient color to fill the border) when its value is of `Object` type. Let's look at the definition of the qualification function. ```ts type QualifyFunction = ( this: CellView, // View of the node/edge val: ComplexAttrValue, // Current attribute value options: { elem: Element // Element to which the current attribute is applied attrs: ComplexAttrs // Key-value pairs of attributes applied to this element cell: Cell // Node/edge view: CellView // View of the node/edge }, ) => boolean // Returns true if it passes the qualification function's judgment ``` For example, the definition of the [`stroke`](#stroke) attribute is as follows: ```ts export const stroke: Attr.Definition = { qualify(val) { // Only trigger special attribute processing logic when the attribute value is an object. return ObjectExt.isPlainObject(val) }, set(stroke, { view }) { return `url(#${view.graph.defineGradient(stroke as any)})` }, } ``` #### set Sets the attribute definition, suitable for most scenarios. ```ts export interface SetDefinition { qualify?: QualifyFunction // Qualification function set: ( this: CellView, // View of the node/edge val: ComplexAttrValue, // Current attribute value options: { refBBox: Rectangle // Bounding box of the reference element, or a rectangle representing the position and size of the node when there is no reference element elem: Element // Element to which the current attribute is applied attrs: ComplexAttrs // Key-value pairs of attributes applied to this element cell: Cell // Node/edge view: CellView // View of the node/edge }, ) => SimpleAttrValue | SimpleAttrs | void } ``` When the `set` method returns a `string` or `number`, it means to use the return value as the value of the special attribute. This approach is usually used to extend the definition of native attributes, such as the [`stroke`](#stroke) and [`fill`](#fill) attributes that support gradient color filling. ```ts // stroke attribute definition export const stroke: Attr.SetDefinition = { qualify: ObjectExt.isPlainObject, set(stroke, { view }) { // Returns a string, the return value is used as the attribute value of the stroke attribute. return `url(#${view.graph.defineGradient(stroke as any)})` }, } // Using the stroke attribute node.attr({ rect: { stroke: {...}, }, }) ``` When the `set` method returns a simple object, this object is applied to the corresponding element as attribute key-value pairs, such as the [`sourceMarker`](#sourcemarker) and [`targetMarker`](#targetmarker) attributes. ```ts export const sourceMarker: Attr.SetDefinition = { qualify: ObjectExt.isPlainObject, set(marker: string | JSONObject, { view, attrs }) { // Returns a simple object, the return value is applied to the corresponding element as attribute key-value pairs. return { 'marker-start': createMarker(marker, view, attrs), } }, } ``` When the `set` method doesn't have a return value, it indicates that the property assignment is completed within the method itself, such as the [`html`](#html) attribute. ```ts export const html: Attr.Definition = { set(html, { view, elem }) { // No return value, property assignment is completed within the method view.$(elem).html(`${html}`) }, } ``` #### offset Offset property definition. ```ts export interface OffsetDefinition { qualify?: QualifyFunction // Qualification function offset: ( this: CellView, // View of the node/edge val: ComplexAttrValue, // Current attribute value options: { refBBox: Rectangle // Bounding box of the reference element, if no reference element, use the rectangle represented by the node's position and size elem: Element // Element to which the current attribute is applied attrs: ComplexAttrs // Key-value pairs of attributes applied to the element cell: Cell // Node/edge view: CellView // View of the node/edge }, ) => Point.PointLike // Returns absolute offset } ``` Returns a `Point.PointLike` object representing absolute offsets in `x` and `y` directions, like the [`xAlign`](#xalign) attribute. ```ts export const xAlign: Attr.OffsetDefinition = { offset(alignment, { refBBox }) { ... // Returns the absolute offset on the x-axis return { x, y: 0 } }, } ``` #### position Position attribute definition. ```ts export interface PositionDefinition { qualify?: QualifyFunction // Qualification function offset: ( this: CellView, // View of the node/edge val: ComplexAttrValue, // Current attribute value options: { refBBox: Rectangle // Bounding box of the reference element, if no reference element, use the rectangle represented by the node's position and size elem: Element // Element to which the current attribute is applied attrs: ComplexAttrs // Key-value pairs of attributes applied to the element cell: Cell // Node/edge view: CellView // View of the node/edge }, ) => Point.PointLike // Returns absolute positioning coordinates } ``` Returns absolute positioning coordinates relative to the node, like the [`refX`](#refx) and [`refDx`](#refdx) attributes. ```ts export const refX: Attr.PositionDefinition = { position(val, { refBBox }) { ... // Returns the absolute positioning on the x-axis return { x, y: 0 } }, } ``` ### docs/demos/api/attrs/x-align/index.less ```less .attrs-x-align-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/attrs/x-align/settings.tsx ```tsx import React from 'react' import { Radio, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { refX: number refY: number xAlign?: string yAlign?: string } export class Settings extends React.Component<Props, State> { state: State = { refX: 0.5, refY: 0.5, xAlign: 'left', yAlign: 'top', } notifyChange() { this.props.onChange(this.state) } onRefXChanged = (refX: number) => { this.setState({ refX }, () => { this.notifyChange() }) } onRefYChanged = (refY: number) => { this.setState({ refY }, () => { this.notifyChange() }) } onXAlignChange = (e: any) => { this.setState( { xAlign: e.target.value, }, () => { this.notifyChange() }, ) } onYAlignChange = (e: any) => { this.setState( { yAlign: e.target.value, }, () => { this.notifyChange() }, ) } render() { return ( <Card title="Attrs" size="small" bordered={false} style={{ width: 320 }}> <Row align="middle"> <Col span={5}>refX</Col> <Col span={14}> <Slider min={0} max={0.99} step={0.01} value={this.state.refX} onChange={this.onRefXChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value"> {(this.state.refX * 100).toFixed(0)}% </div> </Col> </Row> <Row align="middle"> <Col span={5}>refY</Col> <Col span={14}> <Slider min={0} max={0.99} step={0.01} value={this.state.refY} onChange={this.onRefYChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value"> {(this.state.refY * 100).toFixed(0)}% </div> </Col> </Row> <Row align="middle"> <Col span={5}>xAlign</Col> <Col span={19}> <Radio.Group onChange={this.onXAlignChange} value={this.state.xAlign} > <Radio value={'left'}>Left</Radio> <Radio value={'middle'}>Middle</Radio> <Radio value={'right'}>Right</Radio> </Radio.Group> </Col> </Row> <Row align="middle"> <Col span={5}>yAlign</Col> <Col span={19}> <Radio.Group onChange={this.onYAlignChange} value={this.state.yAlign} > <Radio value={'top'}>Top</Radio> <Radio value={'middle'}>Middle</Radio> <Radio value={'bottom'}>Bottom</Radio> </Radio.Group> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/attrs/text-wrap/index.less ```less .attrs-text-wrap-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 216px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/api/attrs/text-anchor/index.less ```less .attrs-text-anchor-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/attrs/text-anchor/settings.tsx ```tsx import React from 'react' import { Radio, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { refX: number refY: number textAnchor?: string textVerticalAnchor?: string } export class Settings extends React.Component<Props, State> { state: State = { refX: 0.5, refY: 0.5, textAnchor: 'start', textVerticalAnchor: 'top', } notifyChange() { this.props.onChange(this.state) } onRefXChanged = (refX: number) => { this.setState({ refX }, () => { this.notifyChange() }) } onRefYChanged = (refY: number) => { this.setState({ refY }, () => { this.notifyChange() }) } onTextAnchorAlignChange = (e: any) => { this.setState( { textAnchor: e.target.value, }, () => { this.notifyChange() }, ) } onTextVerticalAnchorAlignChange = (e: any) => { this.setState( { textVerticalAnchor: e.target.value, }, () => { this.notifyChange() }, ) } render() { return ( <Card title="Attrs" size="small" bordered={false} style={{ width: 320 }}> <Row align="middle"> <Col span={5}>refX</Col> <Col span={14}> <Slider min={0} max={0.99} step={0.01} value={this.state.refX} onChange={this.onRefXChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value"> {(this.state.refX * 100).toFixed(0)}% </div> </Col> </Row> <Row align="middle"> <Col span={5}>refY</Col> <Col span={14}> <Slider min={0} max={0.99} step={0.01} value={this.state.refY} onChange={this.onRefYChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value"> {(this.state.refY * 100).toFixed(0)}% </div> </Col> </Row> <Row align="middle"> <Col>textAnchor</Col> </Row> <Row align="middle"> <Col span={19} offset={5}> <Radio.Group onChange={this.onTextAnchorAlignChange} value={this.state.textAnchor} > <Radio value={'start'}>Start</Radio> <Radio value={'middle'}>Middle</Radio> <Radio value={'end'}>end</Radio> </Radio.Group> </Col> </Row> <Row align="middle"> <Col>textVerticalAnchor</Col> </Row> <Row align="middle"> <Col span={19} offset={5}> <Radio.Group onChange={this.onTextVerticalAnchorAlignChange} value={this.state.textVerticalAnchor} > <Radio value={'top'}>Top</Radio> <Radio value={'middle'}>Middle</Radio> <Radio value={'bottom'}>Bottom</Radio> </Radio.Group> </Col> </Row> </Card> ) } } ``` #### Highlighter - Canonical page: https://x6.antv.antgroup.com/en/api/registry/highlighter/ - Markdown source: https://x6.antv.antgroup.com/en/api/registry/highlighter.md - Description: Highlighter --- title: "Highlighter" description: "Highlighter" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/registry/highlighter/" version: "3.x" --- Node/edge highlighters used to highlight specified elements. X6 includes the following types of highlighters. | Name | Description | |-----------|----------------------------------------------------------| | stroke | [Stroke Highlighter](#stroke), renders a highlighted border around the bounding box of the element. | | className | [Class Name Highlighter](#classname), highlights elements by adding additional class names. | When creating a Graph, you can specify the highlighting style triggered by certain interactions through the `highlighting` option, such as: ```ts new Graph({ highlighting: { // When a magnet is available for linking, render a 2px wide red rectangle around the magnet magnetAvailable: { name: 'stroke', args: { padding: 4, attrs: { 'stroke-width': 2, stroke: 'red', }, }, }, }, }) ``` Supported `highlighting` configuration options include: - `'default'` Default highlighting option, used when the following highlighting configurations are not specified. - `'embedding'` Used when dragging a node for embedding and the node can be embedded. - `'nodeAvailable'` Used during the linking process when a node can be linked. - `'magnetAvailable'` Used during the linking process when a magnet can be linked. - `'magnetAdsorbed'` Used during the linking process when automatically snapping to a magnet. ## Built-in Highlighters ### Stroke The border highlighter renders a highlighted border along the bounding box of the element. | Parameter Name | Type | Default Value | Description | |----------------|---------|---------------------------------------------|------------------| | rx | number | `0` | Border radius. | | ry | number | `0` | Border radius. | | padding | number | `3` | Padding. | | attrs | object | `{ 'stroke-width': 3, stroke: '#FEB663' }` | Border element attributes. | ### ClassName The style name highlighter highlights elements by adding an additional style name. | Parameter Name | Type | Default Value | Description | |----------------|---------|---------------------|--------------| | className | string | `x6-highlighted` | Style name. | ## Custom Highlighters A highlighter is an object with the following signature, containing two methods: `highlight` and `unhighlight`, which are used to highlight and unhighlight elements, respectively. ```ts export interface Definition<T> { highlight: (cellView: CellView, magnet: Element, options: T) => void unhighlight: (cellView: CellView, magnet: Element, options: T) => void } ``` | Parameter Name | Type | Default Value | Description | |----------------|----------|---------------|-----------------------| | cellView | CellView | | View. | | magnet | Element | | The highlighted element. | | options | T | | Highlight options. | Now, let's define a highlighter named `opacity`, which adds a `'highlight-opacity'` style name to the element. ```ts export interface OpacityHighlighterOptions {} const className = 'highlight-opacity' export const opacity: Highlighter.Definition<OpacityHighlighterOptions> = { highlight(cellView, magnet) { Dom.addClass(magnet, className) }, unhighlight(cellView, magnetEl) { Dom.removeClass(magnetEl, className) }, } ``` After defining it, we can register our highlighter: ```ts Graph.registerHighlighter('opacity', opacity, true) ``` Then we can use this highlighter with the string `opacity`: ```ts new Graph({ highlighting: { magnetAvailable: { name: 'opacity', }, }, }) ``` #### Filter - Canonical page: https://x6.antv.antgroup.com/en/api/registry/filter/ - Markdown source: https://x6.antv.antgroup.com/en/api/registry/filter.md - Description: Filter --- title: "Filter" description: "Filter" language: "en" canonical: "https://x6.antv.antgroup.com/en/api/registry/filter/" version: "3.x" --- We can use the special attribute [filter](/en/api/registry/attr#filter) to specify [SVG filters](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Filter_effects) for elements. For example, we can define a predefined object for the `filter` attribute of the element, where `name` and `args` specify the filter name and filter parameters, respectively. ```ts // Create a node by specifying the filter through the attrs option const rect = graph.addNode({ x: 40, y: 40, width: 80, height: 30, attrs: { body: { filter: { name: 'dropShadow', args: { dx: 2, dy: 2, blur: 3, }, }, }, }, }) // After creating the node, we can modify or specify the element's filter using the `attr()` method rect.attr('body/filter', { name: 'dropShadow', args: { dx: 2, dy: 2, blur: 3, }, }) ``` Additionally, we can call the `graph.defineFilter(...)` method to obtain a filter ID, and then set the `filter` attribute to this filter ID. ```ts const filterId = graph.defineFilter({ name: 'dropShadow', args: { dx: 2, dy: 2, blur: 3, }, }) rect.attr('body/filter', `#${filterId}`) ``` Through this brief introduction, we have learned how to use filters. Next, let's take a look at the predefined filters available in X6. ## Built-in Filters ### dropShadow Shadow filter. Refer to [CSS drop-shadow()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/drop-shadow) filter. | Parameter Name | Type | Default Value | Description | |----------------|--------|---------------|---------------------------------| | dx | number | `0` | Shadow offset on the X-axis. | | dy | number | `0` | Shadow offset on the Y-axis. | | blur | number | `0` | Shadow blur radius. | | opacity | number | `1` | Shadow opacity. | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 80, width: 400, height: 160, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `dropShadow\n\ncolor: ${args.color}\ndx: ${args.dx} \ndy: ${args.dy} \nblur: ${args.blur} \nopacity: ${args.opacity} \n` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'dropShadow', args: { ...args }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-drop-shadow-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### blur Gaussian blur filter. Refer to [CSS blur()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/blur) filter. | Parameter Name | Type | Default Value | Description | |----------------|--------|---------------|-----------------------------------------------| | x | number | `2` | Blur amount in the X direction. | | y | number | - | Blur amount in the Y direction; defaults to X. | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 80, y: 80, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `blur(${args.x}${args.y > 0 ? `,${args.y}` : ''})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'blur', args: { x: args.x, y: args.y }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-blur-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### grayScale Grayscale filter. Refer to [CSS grayscale()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/grayscale) filter. | Parameter Name | Type | Default Value | Description | |----------------|--------|---------------|--------------------------------------------------| | amount | number | `1` | Grayscale amount. Ranges from `[0-1]`, where `0` means no grayscale and `1` means full grayscale. | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 40, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `grayScale(${args.amount})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'grayScale', args: { amount: args.amount }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-gray-scale-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### sepia Sepia filter. Refer to [CSS sepia()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/sepia) filter. | Parameter Name | Type | Default Value | Description | |----------------|--------|---------------|-----------------------------------------------------| | amount | number | `1` | Sepia amount. Ranges from `[0-1]`, where `0` means no sepia and `1` means full sepia. | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 40, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `sepia(${args.amount})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'sepia', args: { amount: args.amount }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-sepia-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### saturate Saturation filter. Refer to [CSS saturate()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/saturate) filter. | Parameter Name | Type | Default Value | Description | |----------------|--------|---------------|-------------------------------| | amount | number | `1` | Saturation. Ranges from `[0-1]`. | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 40, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `saturate(${args.amount})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'saturate', args: { amount: args.amount }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-saturate-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### hueRotate Hue rotation filter. Refer to [CSS hue-rotate()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/hue-rotate) filter. | Parameter Name | Type | Default Value | Description | |----------------|--------|---------------|------------------------| | angle | number | `0` | Hue rotation angle. | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 40, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `hueRotate(${args.angle})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'hueRotate', args: { angle: args.angle }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-hue-rotate-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### invert Invert filter. Refer to [CSS invert()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/invert) filter. | Parameter Name | Type | Default Value | Description | |----------------|--------|---------------|-----------------------------------------------------| | amount | number | `1` | Inversion amount. Ranges from `[0-1]`, where `0` means no inversion and `1` means full inversion. | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 40, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `invert(${args.amount})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'invert', args: { amount: args.amount }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-invert-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### brightness Brightness filter. Refer to [CSS brightness()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/brightness) filter. | Parameter Name | Type | Default Value | Description | |----------------|--------|---------------|--------------------------------------------------| | amount | number | `1` | Brightness. Ranges from `[0-1]`, where `0` means completely dark and `1` means completely bright. | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 40, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `brightness(${args.amount})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'brightness', args: { amount: args.amount }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-brightness-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### contrast Contrast filter. Refer to [CSS contrast()](https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/contrast) filter. | Parameter Name | Type | Default Value | Description | |----------------|--------|---------------|--------------------------------------------------| | amount | number | `1` | Contrast. Ranges from `[0-1]`, where `0` means completely dark and `1` means completely bright. | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 40, width: 240, height: 80, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `contrast(${args.amount})` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'contrast', args: { amount: args.amount }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-contrast-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### highlight Highlight filter. | Parameter Name | Type | Default Value | Description | |----------------|--------|---------------|------------------------| | color | string | `red` | Highlight color. | | width | number | `1` | Width of the highlight border. | | blur | number | `0` | Blur radius. | | opacity | number | `1` | Opacity. | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 55, width: 400, height: 160, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `highlight\n\ncolor: ${args.color}\ndx: ${args.width} \ndy: ${args.blur} \nopacity: ${args.opacity} \n` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'highlight', args: { ...args }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-highlight-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### outline Outline filter. | Parameter Name | Type | Default Value | Description | |----------------|--------|---------------|------------------| | color | string | `blue` | Outline color. | | width | number | `1` | Outline width. | | margin | number | `2` | Margin. | | opacity | number | `1` | Opacity. | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Settings, State, defaults } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private node: Node componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.node = this.graph.addNode({ x: 40, y: 55, width: 400, height: 160, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.onChanged(defaults) } getText(args: State) { return `outline\n\ncolor: ${args.color}\nwidth: ${args.width} \nmargin: ${args.margin} \nopacity: ${args.opacity} \n` } onChanged = (args: State) => { this.node.attr({ label: { text: this.getText(args), fill: Color.invert(args.color, true), }, body: { stroke: args.color, fill: Color.lighten(args.color, 40), filter: { name: 'outline', args: { ...args }, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="filter-outline-app"> <div className="app-side"> <Settings onChange={this.onChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Custom Filters A filter definition is a function with the following signature that returns a `<filter>` tag string. ```ts export type Definition<T> = (args: T) => string ``` For example, the definition of the Gaussian blur filter is: ```ts export interface BlurArgs { x?: number y?: number } export function blur(args: BlurArgs = {}) { const x = getNumber(args.x, 2) const stdDeviation = args.y != null && isFinite(args.y) ? [x, args.y] : x return ` <filter> <feGaussianBlur stdDeviation="${stdDeviation}"/> </filter> `.trim() } ``` We can register a filter by calling the `Graph.registerFilter(...)` method. ```ts Graph.registerFilter('blur', blur) ``` ### docs/demos/api/filter/drop-shadow/index.less ```less .filter-drop-shadow-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/drop-shadow/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string dx: number dy: number blur: number opacity: number } export const defaults: State = { color: '#4943a3', dx: 10, dy: 10, blur: 10, opacity: 0.5, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onDxChanged = (dx: number) => { this.setState({ dx }, () => { this.notifyChange() }) } onDyChanged = (dy: number) => { this.setState({ dy }, () => { this.notifyChange() }) } onBlurChanged = (blur: number) => { this.setState({ blur }, () => { this.notifyChange() }) } onOpacityChanged = (opacity: number) => { this.setState({ opacity }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>color</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>dx</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.dx} onChange={this.onDxChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.dx.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>dy</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.dy} onChange={this.onDyChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.dy.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>blur</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.blur} onChange={this.onBlurChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.blur.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>opacity</Col> <Col span={14}> <Slider min={0.05} max={1} step={0.05} value={this.state.opacity} onChange={this.onOpacityChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.opacity.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/blur/index.less ```less .filter-blur-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/blur/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string x: number y: number } export const defaults: State = { color: '#4943a3', x: 10, y: 10, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onXChanged = (dx: number) => { this.setState({ x: dx }, () => { this.notifyChange() }) } onYChanged = (dy: number) => { this.setState({ y: dy }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>fill</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>x</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.x} onChange={this.onXChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.x.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>y</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.y} onChange={this.onYChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.y.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/gray-scale/index.less ```less .filter-gray-scale-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/gray-scale/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string amount: number } export const defaults: State = { color: '#4943a3', amount: 0.3, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onAmountChanged = (dx: number) => { this.setState({ amount: dx }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>fill</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>amount</Col> <Col span={14}> <Slider min={0} max={1} step={0.01} value={this.state.amount} onChange={this.onAmountChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.amount.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/sepia/index.less ```less .filter-sepia-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/sepia/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string amount: number } export const defaults: State = { color: '#4943a3', amount: 0.3, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onAmountChanged = (dx: number) => { this.setState({ amount: dx }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>fill</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>amount</Col> <Col span={14}> <Slider min={0} max={1} step={0.01} value={this.state.amount} onChange={this.onAmountChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.amount.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/saturate/index.less ```less .filter-saturate-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/saturate/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string amount: number } export const defaults: State = { color: '#4943a3', amount: 0.3, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onAmountChanged = (dx: number) => { this.setState({ amount: dx }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>color</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>amount</Col> <Col span={14}> <Slider min={0} max={1} step={0.01} value={this.state.amount} onChange={this.onAmountChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.amount.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/hue-rotate/index.less ```less .filter-hue-rotate-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/hue-rotate/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string angle: number } export const defaults: State = { color: '#4943a3', angle: 0, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onAngleChanged = (dx: number) => { this.setState({ angle: dx }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>fill</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>angle</Col> <Col span={14}> <Slider min={0} max={360} step={1} value={this.state.angle} onChange={this.onAngleChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.angle.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/invert/index.less ```less .filter-invert-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/invert/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string amount: number } export const defaults: State = { color: '#4943a3', amount: 0.3, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onAmountChanged = (dx: number) => { this.setState({ amount: dx }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>fill</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>amount</Col> <Col span={14}> <Slider min={0} max={1} step={0.01} value={this.state.amount} onChange={this.onAmountChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.amount.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/brightness/index.less ```less .filter-brightness-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/brightness/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string amount: number } export const defaults: State = { color: '#4943a3', amount: 0.3, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onAmountChanged = (dx: number) => { this.setState({ amount: dx }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>fill</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>amount</Col> <Col span={14}> <Slider min={0} max={1} step={0.01} value={this.state.amount} onChange={this.onAmountChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.amount.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/contrast/index.less ```less .filter-contrast-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/contrast/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string amount: number } export const defaults: State = { color: '#4943a3', amount: 0.3, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onAmountChanged = (dx: number) => { this.setState({ amount: dx }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>fill</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>amount</Col> <Col span={14}> <Slider min={0} max={1} step={0.01} value={this.state.amount} onChange={this.onAmountChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.amount.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/highlight/index.less ```less .filter-highlight-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/highlight/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string width: number blur: number opacity: number } export const defaults: State = { color: '#4943a3', width: 10, blur: 1, opacity: 0.5, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onWidthChanged = (width: number) => { this.setState({ width }, () => { this.notifyChange() }) } onBlurChanged = (blur: number) => { this.setState({ blur }, () => { this.notifyChange() }) } onOpacityChanged = (opacity: number) => { this.setState({ opacity }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>color</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>width</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.width} onChange={this.onWidthChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.width.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>blur</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.blur} onChange={this.onBlurChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.blur.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>opacity</Col> <Col span={14}> <Slider min={0.05} max={1} step={0.05} value={this.state.opacity} onChange={this.onOpacityChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.opacity.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### docs/demos/api/filter/outline/index.less ```less .filter-outline-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-left { bottom: 0; width: 336px; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } ``` ### docs/demos/api/filter/outline/settings.tsx ```tsx import React from 'react' import { Input, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (state: State) => void } export interface State { color: string width: number margin: number opacity: number } export const defaults: State = { color: '#4943a3', width: 2, margin: 2, opacity: 0.5, } export class Settings extends React.Component<Props, State> { state: State = defaults notifyChange() { this.props.onChange({ ...this.state, }) } onColorChanged = (e: any) => { this.setState({ color: e.target.value }, () => { this.notifyChange() }) } onWidthChanged = (width: number) => { this.setState({ width }, () => { this.notifyChange() }) } onMarginChanged = (margin: number) => { this.setState({ margin }, () => { this.notifyChange() }) } onOpacityChanged = (opacity: number) => { this.setState({ opacity }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>color</Col> <Col span={14}> <Input type="color" value={this.state.color} style={{ width: '100%' }} onChange={this.onColorChanged} /> </Col> </Row> <Row align="middle"> <Col span={6}>width</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.width} onChange={this.onWidthChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.width.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>margin</Col> <Col span={14}> <Slider min={0} max={20} step={1} value={this.state.margin} onChange={this.onMarginChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.margin.toFixed(2)}</div> </Col> </Row> <Row align="middle"> <Col span={6}>opacity</Col> <Col span={14}> <Slider min={0.05} max={1} step={0.05} value={this.state.opacity} onChange={this.onOpacityChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.opacity.toFixed(2)}</div> </Col> </Row> </Card> ) } } ``` ### tutorial #### Introduction - Canonical page: https://x6.antv.antgroup.com/en/tutorial/about/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/about.md - Description: Introduction --- title: "Introduction" description: "Introduction" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/about/" version: "3.x" --- X6 is a graph editing engine based on HTML and SVG, offering low-cost customization capabilities and out-of-the-box built-in extensions, making it easy for us to quickly build applications such as DAG diagrams, ER diagrams, flowcharts, and lineage graphs. If you haven't used X6 yet, we recommend experiencing its charm through the [Getting Started Guide](/en/tutorial/getting-started). <p align="left"> <a href="https://github.com/antvis/X6/actions/workflows/ci.yml"><img alt="build" src="https://img.shields.io/github/actions/workflow/status/antvis/x6/ci.yml?branch=master&logo=github&style=flat-square" /></a> <a href="https://www.npmjs.com/package/@antv/x6"><img alt="NPM Package" src="https://img.shields.io/npm/v/@antv/x6.svg?style=flat-square" /></a> <a href="https://www.npmjs.com/package/@antv/x6"><img alt="NPM Downloads" src="https://img.shields.io/npm/dm/@antv/x6?logo=npm&style=flat-square" /></a> </p> <p align="left"> <a href="https://github.com/antvis/X6/blob/master/LICENSE"><img src="https://img.shields.io/github/license/antvis/x6?style=flat-square" alt="MIT License" /></a> <a href="https://www.typescriptlang.org"><img alt="Language" src="https://img.shields.io/badge/language-TypeScript-blue.svg?style=flat-square" /></a> <a href="https://github.com/antvis/x6/pulls"><img alt="PRs Welcome" src="https://img.shields.io/badge/PRs-Welcome-brightgreen.svg?style=flat-square" /></a> <a href="https://x6.antv.antgroup.com"><img alt="website" src="https://img.shields.io/static/v1?label=&labelColor=505050&message=website&color=0076D6&style=flat-square&logo=google-chrome&logoColor=0076D6" /></a> </p> ## ✨ Features - 🌱 Highly Customizable: Supports customizing node styles and interactions using SVG/HTML/React/Vue/Angular. - 🚀 Ready to Use: Comes with 10+ built-in graph editing extensions, such as selection, alignment lines, mini-map, etc. - 🧲 Data-Driven: Based on MVC architecture, allowing users to focus more on data logic and business logic. - 💯 Event-Driven: Can listen to any events occurring within the graph. ## 🍉 Documentation - [Getting Started](/en/tutorial/getting-started) - [Basics](/en/tutorial/basic/graph) - [Advanced](/en/tutorial/intermediate/connection-point) - [Plugins](/en/tutorial/plugins/transform) - [API](/en/api/graph/graph) The X6 documentation is divided into two parts: documentation and API. It is recommended to read the documentation carefully, as it will help you get started with development. The API documentation is extensive, so you can refer to it when you encounter issues by searching for the relevant documents. ## ❤️ How to Communicate If you have any questions, suggestions, feedback, or wish to communicate, you can contact us through the following methods: - Official Recommendation: [GitHub issues](https://github.com/antvis/X6/issues/new/choose) - Yuque Column: [https://www.yuque.com/antv/blog](https://www.yuque.com/antv/blog) <img src="https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*T_A1Q7etdAUAAAAASHAAAAgAemJ7AQ/original" alt="X6 Graph Editing Community" width="375" /> ## 🤝 Contributing ### Bugs If you encounter any issues while using, please check [issues](https://github.com/antvis/x6/issues) to see if there are similar bugs or suggestions. Before reporting a bug, please ensure you have searched existing issues and read our [FAQ](https://www.yuque.com/antv/x6/tox1ukbz5cw57qfy). ### Code of Conduct We have a [Code of Conduct](https://github.com/antvis/X6/blob/master/CONTRIBUTING.md) that we hope all contributors will adhere to. Please take the time to read it to understand what is acceptable and what is not. ### Join the Community You can also refer to the contribution guidelines below to become a contributor to X6 step by step: - [How to Participate in X6 Open Source Development](https://www.yuque.com/antv/x6/gcinvi) - [How to Contribute Code Elegantly on GitHub](https://segmentfault.com/a/1190000000736629?u_atoken=b71f69b7-7d74-4e6c-a373-76e0a36e2c87&u_asession=01aGvG2P10Vrjamv5BFM7yX0X2_OcJ_XmHlitgQC_BVnNLlRLdwpnHYH8ma1b1UKRaX0KNBwm7Lovlpxjd_P_q4JsKWYrT3W_NKPr8w6oU7K93NVUbout2zcDySUWFprtJUe3R9QHfzEvknA4dzJmVTGBkFo3NEHBv0PZUm6pbxQU&u_asig=05FBplinh079EhmRTHTDgrLXp5aawipV_A-9VAsAs841tY8QeTTaaTvFKcH6odRhI4VX2pBdH5ae6FY2MiL2X_4yTqZp2jK-_nBOl2nesFZDM2RmF5JkBT_JWpU60Z6lY1hzgqVxFxj_uE1HnffLBmwa5Sl9NkdZ4_S8RH_A-AooP9JS7q8ZD7Xtz2Ly-b0kmuyAKRFSVJkkdwVUnyHAIJzZMNY1otqX6vcbPyd-A-Ld3WE-pEMt_G6ZtWjng8eWoZH_8T8uYGNepqxdb-gLe1IO3h9VXwMyh6PgyDIVSG1W-dzbV77H9pFSh5eWBVfcZZYGYDqHeX90h_yD6KfDquy8GWlAwW_v4wTa3IAdocwA0iaDksczFnALAG-4HaicdUmWspDxyAEEo4kbsryBKb9Q&u_aref=SU72jL%2FvYl46xrVouxNG%2FiEj5e0%3D) <a href="https://github.com/antvis/x6/graphs/contributors"> <img src="https://openomy.app/svg?repo=antvis/X6&chart=bubble&latestMonth=12" target="_blank" alt="Contribution Leaderboard" style="display: block; height: 400px" /> </a> #### Graph - Canonical page: https://x6.antv.antgroup.com/en/tutorial/basic/graph/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/basic/graph.md - Description: Graph --- title: "Graph" description: "Graph" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/basic/graph/" version: "3.x" --- > **This chapter mainly introduces knowledge related to the graph. By reading, you can learn about** - How to make the graph size responsive - Optimizing graph style by setting background and grid - How to make the graph draggable and resizable - Common graph size and position manipulation APIs ## Graph Size When instantiating a `Graph` object, you can set the `width` and `height` to fix the graph size. If not set, the graph will initialize based on the size of the graph container. In practical projects, you often encounter the following two scenarios: - The graph container has not finished rendering (at this point, its size is 0), leading to abnormal display of the graph elements when the graph object is instantiated. - The page's `resize` causes the graph container size to change, resulting in abnormal display of the graph elements. We can use the `autoResize` configuration to solve the above problems. ```html <!-- Note: When using the autoResize configuration, you need to wrap the graph container in an outer container that has a width and height of 100%. Listen for size changes on the outer container, and when the outer container size changes, the graph will automatically recalculate its width, height, and element positions. --> <div style="width:100%; height:100%"> <div id="container"></div> </div> ``` ```ts const graph = new Graph({ container: document.getElementById('container'), autoResize: true, }) ``` In the example below, you can drag the gray area with the mouse to change the container size. You can see through the background color that the sizes of the three graphs are adaptive. ```ts import './index.less' import { createRoot } from 'react-dom/client' /* eslint-disable no-new */ import React from 'react' import { Graph } from '@antv/x6' import { SplitBox } from '@antv/x6-react-components/es/split-box' export default class Example extends React.Component { private container1: HTMLDivElement private container2: HTMLDivElement private container3: HTMLDivElement componentDidMount() { new Graph({ container: this.container1, background: { color: '#F2F7FA', }, autoResize: true, }) new Graph({ container: this.container2, background: { color: '#F2F7FA', }, autoResize: true, }) new Graph({ container: this.container3, background: { color: '#F2F7FA', }, autoResize: true, }) } refContainer1 = (container: HTMLDivElement) => { this.container1 = container } refContainer2 = (container: HTMLDivElement) => { this.container2 = container } refContainer3 = (container: HTMLDivElement) => { this.container3 = container } render() { return ( <div className="auto-resize-app"> <SplitBox split="horizontal"> <div className="full"> <div ref={this.refContainer1} /> </div> <SplitBox split="vertical"> <div className="full"> <div ref={this.refContainer2} /> </div> <div className="full"> <div ref={this.refContainer3} /> </div> </SplitBox> </SplitBox> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Background and Grid You can set the graph background and grid using the `background` and `grid` configurations. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' const data = { nodes: [ { id: 'node1', shape: 'rect', x: 40, y: 40, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, { id: 'node2', shape: 'rect', x: 160, y: 180, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, ], edges: [ { shape: 'edge', source: 'node1', target: 'node2', label: 'x6', attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }, ], } export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, grid: { visible: true, type: 'doubleMesh', args: [ { color: '#eee', // 主网格线颜色 thickness: 1, // 主网格线宽度 }, { color: '#ddd', // 次网格线颜色 thickness: 1, // 次网格线宽度 factor: 4, // 主次网格线间隔 }, ], }, }) graph.fromJSON(data) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="backgournd-grid-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` > **Tip** In X6, the grid is the minimum unit for rendering/moving nodes, with a default size of 10px. This means that a node positioned at `{ x: 24, y: 38 }` will actually render at `{ x: 20, y: 40 }` on the graph. The background supports not only colors but also background images. For detailed configurations and methods, refer to the [API](/en/api/graph/background). Additionally, the grid supports four different types and allows configuration of grid line colors and widths. For detailed configurations and methods, refer to the [API](/en/api/graph/grid). ## Zooming and Panning Dragging and zooming the graph are also common operations. In Graph, these two functionalities are implemented through the `panning` and `mousewheel` configurations. When you press down on the graph and move the mouse, it will drag the graph, and scrolling the mouse wheel will zoom the graph. ```ts const graph = new Graph({ ..., panning: true, mousewheel: true }) ``` Let's experience this through an example: ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' const data = { nodes: [ { id: 'node1', shape: 'rect', x: 40, y: 40, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, { id: 'node2', shape: 'rect', x: 160, y: 180, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, ], edges: [ { shape: 'edge', source: 'node1', target: 'node2', label: 'x6', attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }, ], } export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, grid: { visible: true, type: 'doubleMesh', args: [ { color: '#eee', // 主网格线颜色 thickness: 1, // 主网格线宽度 }, { color: '#ddd', // 次网格线颜色 thickness: 1, // 次网格线宽度 factor: 4, // 主次网格线间隔 }, ], }, panning: true, mousewheel: { enabled: true, modifiers: 'Ctrl', maxScale: 4, minScale: 0.2, }, }) graph.fromJSON(data) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="panning-mousewheel-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` Of course, `panning` and `mousewheel` also support many other configurations, such as modifier keys (which must be pressed to trigger the corresponding behavior), zoom factors (rates), etc. Learn more from [Panning API](/en/api/graph/panning) and [Mousewheel API](/en/api/graph/mousewheel). ## Graph Plugins Beyond built-in background, grid, zoom, and pan, X6 also provides useful canvas plugins such as scrolling and alignment guides that you can enable as needed. ```ts import { Graph, Scroller, Snapline } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), height: 300, width: 800, }) // Snapline helps show alignment guides when dragging nodes near others graph.use( new Snapline({ enabled: true, }), ) // Scroller enables scrollable canvas (with optional drag-to-pan) graph.use( new Scroller({ enabled: true, pannable: true, }), ) ``` In the demo below, enabling `Scroller` allows scrolling the canvas, and enabling `Snapline` shows alignment lines when dragging nodes close to others. ```ts import { Graph, Scroller, Snapline } from '@antv/x6' const data = { nodes: [ { id: 'node1', shape: 'rect', x: 40, y: 160, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, { id: 'node2', shape: 'rect', x: 160, y: 180, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, ], } const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) graph.use(new Snapline({ enabled: true })) graph.use(new Scroller({ enabled: true, pannable: true })) graph.fromJSON(data) graph.centerContent() ``` For more details, see [Snapline](/en/tutorial/plugins/snapline) and [Scroller](/en/tutorial/plugins/scroller). ## Common APIs In addition to the configurations mentioned above, X6 also provides a rich set of APIs for manipulating graph size and position. Here are some commonly used APIs; for more detailed content, see [Transform API](/en/api/graph/transform). ```ts graph.resize(800, 600) // Resize the graph size graph.translate(20, 20) // Translate the graph in the x and y directions graph.zoom(0.2) // Increase the graph zoom level by 0.2 (default is 1) graph.zoom(-0.2) // Decrease the graph zoom level by 0.2 graph.zoomTo(1.2) // Set the graph zoom level to 1.2 // Scale the elements in the graph to fit all elements, with maxScale configuration for the maximum zoom level graph.zoomToFit({ maxScale: 1 }) graph.centerContent() // Center the elements in the graph for display ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Button } from 'antd' const data = { nodes: [ { id: 'node1', shape: 'rect', x: 40, y: 40, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, { id: 'node2', shape: 'rect', x: 160, y: 180, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, ], edges: [ { shape: 'edge', source: 'node1', target: 'node2', label: 'x6', attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }, ], } const commands = [ { key: 'zoomIn', label: 'ZoomIn(0.2)', }, { key: 'zoomOut', label: 'ZoomOut(-0.2)', }, { key: 'zoomTo', label: 'ZoomTo(1)', }, { key: 'zoomToFit', label: 'ZoomToFit', }, { key: 'centerContent', label: 'CenterContent', }, ] export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, grid: { visible: true, type: 'doubleMesh', args: [ { color: '#eee', // 主网格线颜色 thickness: 1, // 主网格线宽度 }, { color: '#ddd', // 次网格线颜色 thickness: 1, // 次网格线宽度 factor: 4, // 主次网格线间隔 }, ], }, panning: true, mousewheel: { enabled: true, modifiers: 'Ctrl', maxScale: 4, minScale: 0.2, }, }) graph.fromJSON(data) graph.centerContent() this.graph = graph } transform = (command: string) => { switch (command) { case 'translate': this.graph.translate(20, 20) break case 'zoomIn': this.graph.zoom(0.2) break case 'zoomOut': this.graph.zoom(-0.2) break case 'zoomTo': this.graph.zoomTo(1) break case 'zoomToFit': this.graph.zoomToFit() break case 'centerContent': this.graph.centerContent() break default: break } } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="transform-app"> <div className="app-btns"> <Button.Group> {commands.map((item) => ( <Button onClick={() => this.transform(item.key)} key={item.key}> {item.label} </Button> ))} </Button.Group> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` More APIs: - [Graph Options](/en/api/graph/graph) - [Grid](/en/api/graph/grid) - [Background](/en/api/graph/background) - [Panning](/en/api/graph/panning) - [Mousewheel](/en/api/graph/mousewheel) - [Transform](/en/api/graph/transform) - [Coordinate](/en/api/graph/coordinate) ### docs/demos/tutorial/basic/graph/auto-resize/index.less ```less .auto-resize-app { display: flex; width: 600px; height: 400px; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } .full { width: 100%; height: 100%; } } .x6-split-box-horizontal > .x6-split-box-resizer, .x6-split-box-vertical > .x6-split-box-resizer { background: #e3e6e9; } ``` ### docs/demos/tutorial/basic/graph/background-grid/index.less ```less .backgournd-grid-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` ### docs/demos/tutorial/basic/graph/panning-mousewheel/index.less ```less .panning-mousewheel-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` ### docs/demos/tutorial/basic/graph/transform/index.less ```less .transform-app { width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-btns { bottom: 0; padding: 0 8px; } .app-content { width: 100%; height: 240px; margin-top: 16px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` #### Connection Points - Canonical page: https://x6.antv.antgroup.com/en/tutorial/intermediate/connection-point/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/intermediate/connection-point.md - Description: Connection Points --- title: "Connection Points" description: "Connection Points" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/intermediate/connection-point/" version: "3.x" --- > **This chapter mainly introduces knowledge related to connection points. By reading, you can learn about** - Concepts of anchors and connection points - How to use anchors and connection points to customize special edges Let's start with an example: ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, 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-node', x: 40, y: 200, label: 'hello', }) const target = graph.addNode({ shape: 'custom-node', x: 300, y: 80, label: 'world', }) graph.addEdge({ source: { cell: source, anchor: { name: 'right', args: { dy: -10, }, }, connectionPoint: 'anchor', }, target: { cell: target, anchor: { name: 'left', args: { dy: -10, }, }, connectionPoint: 'anchor', }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.addEdge({ source: { cell: source, anchor: { name: 'right', }, connectionPoint: 'anchor', }, target: { cell: target, anchor: { name: 'left', }, connectionPoint: 'anchor', }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.addEdge({ source: { cell: source, anchor: { name: 'right', args: { dy: 10, }, }, connectionPoint: 'anchor', }, target: { cell: target, anchor: { name: 'left', args: { dy: 10, }, }, connectionPoint: 'anchor', }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="multi-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` When you move the node above, the edge connection points on the node remain fixed, and multiple edges keep a gap between them. This differs from the previous example. How is this achieved? Let's understand anchors and connection points with the following diagram. ## Introduction By default, the anchor is `center` (the element center). `connectionPoint` specifies how to compute intersection points; its default is `boundary` (intersection with the element boundary). An edge is drawn by connecting a reference line from the source anchor to the target anchor. The intersection of that line with the element, computed per `connectionPoint`, becomes the edge's start/end point. <img src="https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*RhX1SYh1K-QAAAAAAAAAAAAAARQnAQ" alt="Connection Point" width="500" /> ## Usage You can configure `anchor` and `connectionPoint` globally in `connecting`, or per edge via `source` and `target`. ```ts // Configuring in connecting const graph = new Graph({ connecting: { sourceAnchor: { name: 'right', // Offsets the anchor 10px upward from the center on the right side of the node args: { dy: -10, }, }, targetAnchor: { name: 'right', // Offsets the anchor 10px upward from the center on the right side of the node args: { dy: -10, }, }, connectionPoint: 'anchor', }, }) // Per-edge configuration takes precedence graph.addEdge({ source: { cell: source, anchor: { name: 'right', args: { dy: -10, }, }, connectionPoint: 'anchor', }, target: { cell: target, anchor: { name: 'left', args: { dy: -10, }, }, connectionPoint: 'anchor', }, }) ``` X6 also supports a wide variety of anchor and connection point types. To customize special edges, refer to [NodeAnchor](/en/api/registry/node-anchor) and [ConnectionPoint](/en/api/registry/connection-point). ### docs/demos/tutorial/intermediate/connection-point/multi/index.less ```less .multi-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` #### Graphic Transformations - Canonical page: https://x6.antv.antgroup.com/en/tutorial/plugins/transform/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/plugins/transform.md - Description: Graphic Transformations --- title: "Graphic Transformations" description: "Graphic Transformations" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/plugins/transform/" version: "3.x" --- > **This chapter mainly introduces knowledge related to the graphic transformation plugin. By reading, you can learn about** - How to adjust node size using the interactive plugin - How to adjust a node’s rotation angle using the interactive plugin ## Usage Using interactive UI components to adjust node size and angle is a common requirement. We provide the `transform` plugin to implement this. Use it like this: ```ts import { Graph, Transform } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use( new Transform({ resizing: resizingOptions, rotating: rotatingOptions, }), ) ``` ## Demo First, let's experience interactive resizing of nodes (clicking on a node brings up the operation component): ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Transform } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.initGraph({ enabled: true, minWidth: 1, maxWidth: 200, minHeight: 1, maxHeight: 150, orthogonal: false, restrict: false, preserveAspectRatio: false, }) } initGraph = (options: State) => { if (this.graph) { this.graph.dispose() } this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.graph.use( new Transform({ resizing: options, }), ) const source = this.graph.addNode({ shape: 'rect', x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 160, y: 180, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph.centerContent() } onSettingChanged = (options: State) => { this.initGraph(options) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="resizing-app"> <div className="app-side"> <Settings onChange={this.onSettingChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` Next, let's experience interactive rotation of nodes (clicking on a node brings up the operation component): ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Transform } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.initGraph({ enabled: true, grid: 15, }) } initGraph = (options: State) => { if (this.graph) { this.graph.dispose() } this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.graph.use( new Transform({ rotating: options, }), ) const source = this.graph.addNode({ x: 60, y: 130, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 260, y: 120, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph.centerContent() } onSettingChanged = (options: State) => { this.initGraph(options) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="rotating-app"> <div className="app-side"> <Settings onChange={this.onSettingChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Configuration ### Resizing | Property Name | Type | Default Value | Required | Description | |---------------------|---------|---------------|----------|-----------------------------------------------| | enabled | boolean | `false` | | Whether resizing of nodes is supported | | minWidth | number | 0 | | Minimum resizing width | | minHeight | number | 0 | | Minimum resizing height | | maxWidth | number | `Infinity` | | Maximum resizing width | | maxHeight | number | `Infinity` | | Maximum resizing height | | orthogonal | boolean | `true` | | Whether to show intermediate resizing points | | restrict | boolean | `false` | | Whether resizing boundaries can exceed canvas edges | | autoScroll | boolean | `true` | | Whether to automatically scroll the canvas when dragging exceeds its bounds | | preserveAspectRatio | boolean | `false` | | Whether to maintain the aspect ratio during resizing | | allowReverse | boolean | `true` | | Whether to allow control points to drag in reverse when reaching minimum width or height | The above configuration supports dynamic modification using functions as well: ```ts new Transform({ resizing: { enabled: true, orthogonal(node: Node) { const { enableOrthogonal } = node.getData() return enableOrthogonal }, }, }) ``` ### Rotating | Property Name | Type | Default Value | Required | Description | |---------------|---------|---------------|----------|---------------------------------| | enabled | boolean | `false` | | Whether rotating of nodes is supported | | grid | number | 15 | | Angle of rotation per step | The above configuration also supports dynamic modification using functions: ```ts new Transform({ rotating: { enabled: true, grid() { return 30 }, }, }) ``` ## API ### graph.createTransformWidget(node: Node) Creates a transform control for the node. ### graph.clearTransformWidgets() Clears all transform controls. ## Events | Event Name | Parameter Type | Description | |-------------------|-------------------------------------------------------------------------------------|-------------------------------| | `node:resize` | `{ e: Dom.MouseDownEvent; x: number; y: number; node: Node; view: NodeView }` | Triggered when resizing starts | | `node:resizing` | `{ e: Dom.MouseMoveEvent; x: number; y: number; node: Node; view: NodeView }` | Triggered during resizing | | `node:resized` | `{ e: Dom.MouseUpEvent; x: number; y: number; node: Node; view: NodeView }` | Triggered after resizing | | `node:rotate` | `{ e: Dom.MouseDownEvent; x: number; y: number; node: Node; view: NodeView }` | Triggered when rotation starts | | `node:rotating` | `{ e: Dom.MouseMoveEvent; x: number; y: number; node: Node; view: NodeView }` | Triggered during rotation | | `node:rotated` | `{ e: Dom.MouseUpEvent; x: number; y: number; node: Node; view: NodeView }` | Triggered after rotation | ```ts graph.on('node:rotated', ({ node }) => { console.log(node.angle()) }) // We can also listen to events on the plugin instance transform.on('node:rotated', ({ node }) => { console.log(node.angle()) }) ``` ### docs/demos/tutorial/plugins/transform/resizing/index.less ```less .resizing-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .x6-widget-transform { margin: -1px 0 0 -1px; padding: 0; border: 1px solid #239edd; border-radius: 6px; } .x6-widget-transform > div { border: 1px solid #239edd; } .x6-widget-transform > div:hover { background-color: #3dafe4; } .x6-widget-transform-active-handle { background-color: #3dafe4; } .x6-widget-transform-resize { border-radius: 0; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } } ``` ### docs/demos/tutorial/plugins/transform/resizing/settings.tsx ```tsx import React from 'react' import { Checkbox, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { enabled: true minWidth?: number maxWidth?: number minHeight?: number maxHeight?: number orthogonal?: boolean restrict?: boolean preserveAspectRatio?: boolean } export class Settings extends React.Component<Props, State> { state: State = { enabled: true, minWidth: 1, maxWidth: 200, minHeight: 1, maxHeight: 150, orthogonal: false, restrict: false, preserveAspectRatio: false, } notifyChange() { this.props.onChange({ ...this.state, }) } onEnableChanged = (e: any) => { this.setState({ enabled: e.target.checked }, () => { this.notifyChange() }) } onMinWidthChanged = (minWidth: number) => { this.setState({ minWidth }, () => { this.notifyChange() }) } onMaxWidthChanged = (maxWidth: number) => { this.setState({ maxWidth }, () => { this.notifyChange() }) } onMinHeightChanged = (minHeight: number) => { this.setState({ minHeight }, () => { this.notifyChange() }) } onMaxHeightChanged = (maxHeight: number) => { this.setState({ maxHeight }, () => { this.notifyChange() }) } onOrthogonalChanged = (e: any) => { this.setState({ orthogonal: e.target.checked }, () => { this.notifyChange() }) } onRestrictedChanged = (e: any) => { this.setState({ restrict: e.target.checked }, () => { this.notifyChange() }) } onPreserveAspectRatioChanged = (e: any) => { this.setState({ preserveAspectRatio: e.target.checked }, () => { this.notifyChange() }) } render() { return ( <Card title="Resizing Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.enabled} onChange={this.onEnableChanged} > Enabled </Checkbox> </Col> </Row> <Row align="middle"> <Col span={6}>minWidth</Col> <Col span={14}> <Slider min={1} max={100} step={1} value={this.state.minWidth} onChange={this.onMinWidthChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.minWidth}</div> </Col> </Row> <Row align="middle"> <Col span={6}>maxWidth</Col> <Col span={14}> <Slider min={100} max={200} step={1} value={this.state.maxWidth} onChange={this.onMaxWidthChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.maxWidth}</div> </Col> </Row> <Row align="middle"> <Col span={6}>minHeight</Col> <Col span={14}> <Slider min={1} max={75} step={1} value={this.state.minHeight} onChange={this.onMinHeightChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.minHeight}</div> </Col> </Row> <Row align="middle"> <Col span={6}>maxHeight</Col> <Col span={14}> <Slider min={75} max={150} step={1} value={this.state.maxHeight} onChange={this.onMaxHeightChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.maxHeight}</div> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.orthogonal} onChange={this.onOrthogonalChanged} > Orthogonal </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.restrict} onChange={this.onRestrictedChanged} > Restrict </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.preserveAspectRatio} onChange={this.onPreserveAspectRatioChanged} > PreserveAspectRatio </Checkbox> </Col> </Row> </Card> ) } } ``` ### docs/demos/tutorial/plugins/transform/rotating/index.less ```less .rotating-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .x6-widget-transform { margin: -1px 0 0 -1px; padding: 0; border: 1px solid #239edd; border-radius: 6px; } .x6-widget-transform > div { border: 1px solid #239edd; } .x6-widget-transform > div:hover { background-color: #3dafe4; } .x6-widget-transform-active-handle { background-color: #3dafe4; } .ant-card { min-height: 250px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } } ``` ### docs/demos/tutorial/plugins/transform/rotating/settings.tsx ```tsx import React from 'react' import { Checkbox, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { enabled: true grid?: number } export class Settings extends React.Component<Props, State> { state: State = { enabled: true, grid: 15, } notifyChange() { this.props.onChange({ ...this.state, }) } onEnableChanged = (e: any) => { this.setState({ enabled: e.target.checked }, () => { this.notifyChange() }) } onGridChanged = (grid: number) => { this.setState({ grid }, () => { this.notifyChange() }) } render() { return ( <Card title="Rotating Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.enabled} onChange={this.onEnableChanged} > Enabled </Checkbox> </Col> </Row> <Row align="middle"> <Col span={6}>grid</Col> <Col span={14}> <Slider min={1} max={180} step={1} value={this.state.grid} onChange={this.onGridChanged} /> </Col> <Col span={2}> <div className="slider-value">{this.state.grid}</div> </Col> </Row> </Card> ) } } ``` #### Nodes - Canonical page: https://x6.antv.antgroup.com/en/tutorial/basic/node/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/basic/node.md - Description: Nodes --- title: "Nodes" description: "Nodes" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/basic/node/" version: "3.x" --- > **This chapter mainly introduces knowledge related to nodes. By reading, you can learn about** - Node rendering methods supported by X6 - Methods for adding nodes - Built-in node types in X6 - How to customize nodes - How to modify nodes through the API ## Node Rendering Methods X6 is based on an `SVG` rendering engine, which allows for rendering nodes and edges using different SVG elements, making it particularly suitable for scenarios where node content is relatively simple. For more complex nodes, there is a special `foreignObject` element in `SVG` that can embed any XHTML elements. This element can be used to render HTML, React/Vue/Angular components at the desired location, greatly facilitating project development. When choosing a rendering method, we recommend: - If the node content is relatively simple and the requirements are fixed, use `SVG` nodes. - For other scenarios, it is recommended to use the framework currently employed in the project to render nodes. > **Note** The React/Vue/HTML rendering methods also have some limitations. Due to browser compatibility issues, there may occasionally be some abnormal rendering behaviors, primarily manifested as incomplete node content display or flickering of node content. This can be mitigated by avoiding the use of `position:absolute`, `position:relative`, `transform`, and `opacity` in the CSS styles of internal elements of the node. The following introduction is based on `SVG` nodes, but the usage of other rendering forms is very similar, and we will revisit this in the advanced tutorial. ## Adding Nodes Both nodes and edges share a common base class [Cell](/en/api/model/cell). In addition to inheriting properties from `Cell`, they also support the following options. | Property Name | Type | Default Value | Description | |---------------|--------|---------------|-----------------------------------| | x | number | 0 | Node position x coordinate, in px. | | y | number | 0 | Node position y coordinate, in px. | | width | number | 1 | Node width, in px. | | height | number | 1 | Node height, in px. | | angle | number | 0 | Node rotation angle. | ```ts graph.addNode({ shape: 'rect', x: 100, y: 40, width: 100, height: 40, }) ``` ## Built-in Nodes The above example uses `shape` to specify the node's graphic, with the default value of `shape` being `rect`. The correspondence between X6 built-in nodes and `shape` names is as follows: | Constructor | Shape Name | Description | |-------------------|------------|-----------------------------------------------------| | Shape.Rect | rect | Rectangle. | | Shape.Circle | circle | Circle. | | Shape.Ellipse | ellipse | Ellipse. | | Shape.Polygon | polygon | Polygon. | | Shape.Polyline | polyline | Polyline. | | Shape.Path | path | Path. | | Shape.Image | image | Image. | | Shape.HTML | html | HTML node, uses `foreignObject` to render HTML fragments. | ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' const commonAttrs = { body: { fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, label: { refX: 0.5, refY: '100%', refY2: 4, textAnchor: 'middle', textVerticalAnchor: 'top', }, } export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addNode({ shape: 'rect', x: 40, y: 40, width: 80, height: 40, label: 'rect', attrs: commonAttrs, }) graph.addNode({ shape: 'circle', x: 180, y: 40, width: 40, height: 40, label: 'circle', attrs: commonAttrs, }) graph.addNode({ shape: 'ellipse', x: 280, y: 40, width: 80, height: 40, label: 'ellipse', attrs: commonAttrs, }) graph.addNode({ shape: 'path', x: 420, y: 40, width: 40, height: 40, // https://www.svgrepo.com/svg/13653/like path: 'M24.85,10.126c2.018-4.783,6.628-8.125,11.99-8.125c7.223,0,12.425,6.179,13.079,13.543c0,0,0.353,1.828-0.424,5.119c-1.058,4.482-3.545,8.464-6.898,11.503L24.85,48L7.402,32.165c-3.353-3.038-5.84-7.021-6.898-11.503c-0.777-3.291-0.424-5.119-0.424-5.119C0.734,8.179,5.936,2,13.159,2C18.522,2,22.832,5.343,24.85,10.126z', attrs: commonAttrs, label: 'path', }) graph.addNode({ shape: 'polygon', x: 60, y: 150, width: 40, height: 40, points: '100,10 40,198 190,78 10,78 160,198', attrs: commonAttrs, label: 'polygon', }) graph.addNode({ shape: 'polyline', x: 180, y: 150, width: 40, height: 40, label: 'polyline', attrs: { body: { ...commonAttrs.body, refPoints: '0,0 0,10 10,10 10,0', }, label: commonAttrs.label, }, }) graph.addNode({ shape: 'image', x: 290, y: 150, width: 60, height: 40, imageUrl: 'https://gw.alipayobjects.com/os/s/prod/antv/assets/image/logo-with-text-73b8a.svg', label: 'image', attrs: commonAttrs, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="shapes-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Customizing Nodes We can customize the shape and style of nodes using `markup` and `attrs`, where `markup` is analogous to `HTML` and `attrs` is analogous to `CSS`. It is strongly recommended to read the documentation on [markup](/en/api/model/cell#markup) and [attrs](/en/api/model/cell#attrs) carefully. Next, we may encounter a problem: if the customized content needs to be used by multiple nodes, do we need to redefine it for each node? The answer is no. X6 provides a convenient way to allow different nodes to reuse configurations. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', // 继承于 rect 节点 width: 100, height: 40, markup: [ { tagName: 'rect', // 标签名称 selector: 'body', // 选择器 }, { tagName: 'image', selector: 'img', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, img: { 'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 16, height: 16, x: 12, y: 12, }, }, }, 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-node', // 可以直接使用上面注册过的 shape x: 40, y: 40, label: 'hello', }) const target = graph.addNode({ shape: 'custom-node', x: 160, y: 180, label: 'world', }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="registry-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Modifying Nodes After rendering is complete, we can also modify all properties of a node through the API. The two methods we commonly use are: - node.prop(path, value), for detailed usage see [prop](/en/api/model/cell#node-and-edge-properties). - node.attr(path, value), for detailed usage see [attr](/en/api/model/cell#element-attributes-attrs). First, let's look at `prop`. We will directly print the `prop` values of the default rect node in X6. ```ts const node = graph.addNode({ shape: 'rect', width: 100, height: 40, x: 100, y: 100, label: 'node', }) console.log(node.prop()) // Result { "angle": 0, "position": { "x": 100, "y": 100 }, "size": { "width": 100, "height": 40 }, "attrs": { "text": { "fontSize": 14, "fill": "#000000", "refX": 0.5, "refY": 0.5, "textAnchor": "middle", "textVerticalAnchor": "middle", "fontFamily": "Arial, helvetica, sans-serif", "text": "node" }, "rect": { "fill": "#ffffff", "stroke": "#333333", "strokeWidth": 2 }, "body": { "refWidth": "100%", "refHeight": "100%" } }, "visible": true, "shape": "rect", "id": "ab47cadc-4104-457c-971f-50fbb077508a", "zIndex": 1 } ``` From the above result, we can see that `prop` is a new configuration after processing, and its values can be updated through methods. After updating, the node will immediately refresh to the latest state. To modify the node's `attrs` more conveniently, X6 provides the `attr` method. ```ts source.prop('size', { width: 120, height: 50 }) // Modify size source.attr('rect/fill', '#ccc') // Modify fill color, equivalent to source.prop('attrs/rect/fill', '#ccc') ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Color } from '@antv/x6' import { Button } from 'antd' Graph.registerNode( 'custom-node', { inherit: 'rect', // 继承于 rect 节点 width: 100, height: 40, markup: [ { tagName: 'rect', // 标签名称 selector: 'body', // 选择器 }, { tagName: 'image', selector: 'img', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, img: { 'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 16, height: 16, x: 12, y: 12, }, }, }, true, ) const commands = [ { key: 'prop', label: 'prop', }, { key: 'attr', label: 'attr', }, ] export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const source = graph.addNode({ shape: 'custom-node', x: 40, y: 40, label: 'hello', }) const target = graph.addNode({ shape: 'custom-node', x: 160, y: 180, label: 'world', }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.centerContent() this.graph = graph } change = (command: string) => { const nodes = this.graph.getNodes() switch (command) { case 'prop': nodes.forEach((node) => { const width = 100 + Math.floor(Math.random() * 50) const height = 40 + Math.floor(Math.random() * 10) node.prop('size', { width, height }) }) break case 'attr': nodes.forEach((node) => { const color = Color.randomHex() node.attr('body/fill', color) }) break default: break } } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="node-prop-app "> <div className="app-btns"> <Button.Group> {commands.map((item) => ( <Button onClick={() => this.change(item.key)} key={item.key}> {item.label} </Button> ))} </Button.Group> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` In the above JSON output, we can see that some properties like `refWidth` and `refHeight` are not native SVG properties. They are actually special properties built into X6, such as `refWidth`, which represents relative width. For more detailed special properties, refer to [attrs](/en/api/model/attrs). ### docs/demos/tutorial/basic/node/shapes/index.less ```less .shapes-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` ### docs/demos/tutorial/basic/node/registry/index.less ```less .registry-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` ### docs/demos/tutorial/basic/node/prop/index.less ```less .node-prop-app { border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-btns { bottom: 0; margin: 0; padding: 0 8px; } .app-content { width: 100%; height: 240px; margin: 0; margin-top: 16px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` #### Quickstart - Canonical page: https://x6.antv.antgroup.com/en/tutorial/getting-started/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/getting-started.md - Description: Quickstart --- title: "Quickstart" description: "Quickstart" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/getting-started/" version: "3.x" --- ## Installation X6 is published on npm as @antv/x6. ```shell # npm $ npm install @antv/x6 --save # yarn $ yarn add @antv/x6 # pnpm $ pnpm add @antv/x6 ``` If using the UMD package, you can use any of the following CDN URLs. - [https://unpkg.com/@antv/x6/dist/index.js](https://unpkg.com/@antv/x6/dist/index.js) - [https://cdn.jsdelivr.net/npm/@antv/x6/dist/index.js](https://cdn.jsdelivr.net/npm/@antv/x6/dist/index.js) - [https://cdnjs.cloudflare.com/ajax/libs/antv-x6/2.0.0/index.js](https://cdnjs.cloudflare.com/ajax/libs/antv-x6/2.0.0/index.js) ## Basic Usage It's recommended to learn [SVG Basics](https://codepen.io/HunorMarton/full/PoGbgqj) before you begin, and with some basic SVG knowledge in mind, let's start with a simple example to play with X6. ### 1. Init Graph Creating a graph container on the page and then initializing the graph object, then you can set the graph style through configuration, such as the background color. ```html <div id="container"></div> ``` ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), width: 800, height: 600, background: { color: '#F2F7FA', }, }) ``` ### 2. Render nodes and edges X6 supports json data, you can also use the `attrs` attribute to customize the styles of nodes and edges (like `CSS`). ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' const data = { nodes: [ { id: 'node1', shape: 'rect', x: 40, y: 40, width: 100, height: 40, label: 'hello', attrs: { // body 是选择器名称,选中的是 rect 元素 body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, { id: 'node2', shape: 'rect', x: 160, y: 180, width: 100, height: 40, label: 'world', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, ], edges: [ { shape: 'edge', source: 'node1', target: 'node2', label: 'x6', attrs: { // line 是选择器名称,选中的边的 path 元素 line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }, ], } export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, // 设置画布背景颜色 background: { color: '#F2F7FA', }, }) graph.fromJSON(data) // 渲染元素 graph.centerContent() // 居中显示 } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="helloworld-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### 3. Using the React Node X6 supports using `SVG` and `HTML` to render node content. On this basis, we can also use `React` and `Vue` components to render nodes, which will be very convenient in the development process. For example, we have a new requirement: add a right-click menu to the node. It would be more complicated to implement using `SVG`, We can easily implement it with a React node. You can use the React render package `@antv/x6-react-shape` provided by the X6 ecosystem. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { register } from '@antv/x6-react-shape' import { Dropdown } from 'antd' const CustomComponent = ({ node }: { node: Node }) => { const label = node.prop('label') return ( <Dropdown menu={{ items: [ { key: 'copy', label: '复制', }, { key: 'paste', label: '粘贴', }, { key: 'delete', label: '删除', }, ], }} trigger={['contextMenu']} > <div className="custom-react-node">{label}</div> </Dropdown> ) } register({ shape: 'custom-react-node', width: 100, height: 40, component: CustomComponent, }) const data = { nodes: [ { id: 'node1', shape: 'custom-react-node', x: 40, y: 40, label: 'hello', }, { id: 'node2', shape: 'custom-react-node', x: 160, y: 180, label: 'world', }, ], edges: [ { shape: 'edge', source: 'node1', target: 'node2', label: 'x6', attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }, ], } export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.fromJSON(data) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="react-shape-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### 4. Using the Plugins In addition to the basic element rendering capabilities, X6 also comes with a large number of built-in plugins for graph editing. Using these mature plugins, we can improve the development efficiency. For example, we add a snapline plugin to the graph, when a moving node is aligned with other nodes, the snapline will automatically appear. ```ts import { Snapline } from '@antv/x6' graph.use( new Snapline({ enabled: true, }), ) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Snapline } from '@antv/x6' import { register } from '@antv/x6-react-shape' import { Dropdown } from 'antd' const CustomComponent = ({ node }: { node: Node }) => { const label = node.prop('label') return ( <Dropdown menu={{ items: [ { key: 'copy', label: '复制', }, { key: 'paste', label: '粘贴', }, { key: 'delete', label: '删除', }, ], }} trigger={['contextMenu']} > <div className="custom-react-node">{label}</div> </Dropdown> ) } register({ shape: 'custom-react-node', width: 100, height: 40, component: CustomComponent, }) const data = { nodes: [ { id: 'node1', shape: 'custom-react-node', x: 40, y: 40, label: 'hello', }, { id: 'node2', shape: 'custom-react-node', x: 160, y: 180, label: 'world', }, ], edges: [ { shape: 'edge', source: 'node1', target: 'node2', label: 'x6', attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }, ], } export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.use( new Snapline({ enabled: true, }), ) graph.fromJSON(data) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="use-plugin-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### 5. Export the Data In addition to using `fromJSON` to render JSON data to the graph, of course, there is also support for exporting the data from the graph with `toJSON`, so that we can serialize the graph data and store it to the server. ```ts graph.toJSON() ``` That's the end of our demo. If you want to continue learning about some capabilities of X6, you can start reading from the [Basic Tutorial](/en/tutorial/basic/graph). ### docs/demos/tutorial/getting-started/helloworld/index.less ```less .helloworld-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` ### docs/demos/tutorial/getting-started/react-shape/index.less ```less .react-shape-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } .custom-react-node { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; background-color: #fff; border: 1px solid #8f8f8f; border-radius: 6px; } .custom-react-node span { display: inline-block; width: 100%; height: 100%; } } ``` ### docs/demos/tutorial/getting-started/use-plugin/index.less ```less .use-plugin-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } .custom-react-node { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; background-color: #fff; border: 1px solid #8f8f8f; border-radius: 6px; } } ``` #### Snapline - Canonical page: https://x6.antv.antgroup.com/en/tutorial/plugins/snapline/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/plugins/snapline.md - Description: Snapline --- title: "Snapline" description: "Snapline" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/plugins/snapline/" version: "3.x" --- > **This section mainly introduces the Snapline plugin. By reading, you will learn:** - How to use snaplines on the canvas ## Usage Snaplines help align moving nodes. Enable the feature via the `Snapline` plugin: ```ts import { Graph, Snapline } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use( new Snapline({ enabled: true, }), ) ``` ## Demo ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Snapline } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.graph.use( new Snapline({ enabled: true, clean: false, }), ) this.graph.addNode({ x: 200, y: 100, width: 100, height: 40, label: 'Drag Me', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const source = this.graph.addNode({ x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 160, y: 180, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } onSnaplineChanged = (options: State) => { this.graph.setSnaplineTolerance(options.tolerance) this.graph.setSnaplineFilter(options.filter) if (options.sharp) { this.graph.enableSharpSnapline() } else { this.graph.disableSharpSnapline() } if (options.resizing) { this.graph.enableSnaplineOnResizing() } else { this.graph.disableSnaplineOnResizing() } } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="snapline-app"> <div className="app-side"> <Settings onChange={this.onSnaplineChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Configuration | Property Name | Type | Default Value | Required | Description | |---------------|---------|---------------|----------|-----------------------------------------------------------------------------------------------------------| | className | string | - | | Additional class name to customize snapline styles | | tolerance | number | 10 | | Snapline precision; a snapline is shown when the distance to the target position is less than `tolerance` | | sharp | boolean | `false` | | Whether to use short snaplines (truncated) | | resizing | boolean | `false` | | Whether to trigger snaplines when resizing nodes | | clean | boolean | `true` | | If `true`, snaplines are cleared after 3 seconds; if `false`, they are not cleared; if a number (ms), they are cleared after the specified duration | | filter | Filter | - | | Node filter | The above Filter type is relatively flexible and supports the following forms: - `string[]`: An array of node `shape`s; only nodes with those shapes participate in snapline calculations - `({ id: string })[]`: An array of node IDs; only the specified nodes participate in snapline calculations - `(this: Graph, node: Node) => boolean`: Only nodes for which the function returns `true` participate in snapline calculations ## API ### graph.isSnaplineEnabled() ```ts isSnaplineEnabled(): boolean ``` Returns whether snaplines are enabled. ### graph.enableSnapline() ```ts enableSnapline(): this ``` Enables snaplines. ### graph.disableSnapline() ```ts disableSnapline(): this ``` Disables snaplines. ### graph.toggleSnapline(...) ```ts toggleSnapline(enabled?: boolean): this ``` Toggles whether snaplines are enabled. Parameters are as follows: | Name | Type | Required | Default Value | Description | |---------|---------|:--------:|---------------|-----------------------------------------------------------------------------------------------------------------| | enabled | boolean | | - | Whether to enable snaplines; if omitted, toggles the current enabled state. | ### graph.hideSnapline() ```ts hideSnapline(): this ``` Hides snaplines. ### graph.isSnaplineOnResizingEnabled() ```ts isSnaplineOnResizingEnabled(): boolean ``` Whether to trigger snaplines when resizing nodes. ### graph.enableSnaplineOnResizing() ```ts enableSnaplineOnResizing(): this ``` Enables triggering snaplines during node resizing. ### graph.disableSnaplineOnResizing() ```ts disableSnaplineOnResizing(): this ``` Disables triggering snaplines during node resizing. ### graph.toggleSnaplineOnResizing(...) ```ts toggleSnaplineOnResizing(enabled?: boolean): this ``` Toggles whether to trigger snaplines during node resizing. Parameters are as follows: | Name | Type | Required | Default Value | Description | |---------|---------|:--------:|---------------|-----------------------------------------------------------------------------------------------------------------| | enabled | boolean | | - | Whether to enable snaplines; if omitted, toggles the current enabled state. | ### graph.isSharpSnapline() ```ts isSharpSnapline(): boolean ``` Whether to use short snaplines. ### graph.enableSharpSnapline() ```ts enableSharpSnapline(): this ``` Enables short snaplines; when enabled, snaplines are truncated at relevant node positions; otherwise they span across the canvas. ### graph.disableSharpSnapline() ```ts disableSharpSnapline(): this ``` Disables short snaplines; snaplines span the entire canvas. ### graph.toggleSharpSnapline() ```ts toggleSharpSnapline(enabled?: boolean): this ``` Toggles whether short snaplines are enabled. Parameters are as follows: | Name | Type | Required | Default Value | Description | |---------|---------|:--------:|---------------|-----------------------------------------------------------------------------------------------------------------| | enabled | boolean | | - | Whether to enable short snaplines; if omitted, toggles the current enabled state. | ### graph.getSnaplineTolerance() ```ts getSnaplineTolerance(): number ``` Gets snapline precision. ### graph.setSnaplineTolerance(...) ```ts setSnaplineTolerance(tolerance: number): this ``` Sets snapline precision. ### graph.setSnaplineFilter(...) ```ts setSnaplineFilter( filter?: | null | (string | { id: string })[] | ((this: Graph, node: Node) => boolean) ): this ``` Sets the filter; only nodes that meet the condition participate in snapline calculations. ### docs/demos/tutorial/plugins/snapline/index.less ```less .snapline-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } } ``` ### docs/demos/tutorial/plugins/snapline/settings.tsx ```tsx import React from 'react' import { Checkbox, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { tolerance: number sharp: boolean resizing: boolean filter?: string[] } export class Settings extends React.Component<Props, State> { state: State = { tolerance: 10, sharp: false, resizing: false, } tryToJSON(val: string) { try { return JSON.parse(val) } catch (error) { return val } } notifyChange() { this.props.onChange({ ...this.state, }) } onSharpChanged = (e: any) => { this.setState({ sharp: e.target.checked }, () => { this.notifyChange() }) } onResingChanged = (e: any) => { this.setState({ resizing: e.target.checked }, () => { this.notifyChange() }) } onToleranceChanged = (tolerance: number) => { this.setState({ tolerance }, () => { this.notifyChange() }) } onFilterChanged = (e: any) => { this.setState( { filter: e.target.checked ? ['circle'] : undefined, }, () => { this.notifyChange() }, ) } render() { return ( <Card title="Snapline Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>Tolerance</Col> <Col span={2} offset={1}> <div className="slider-value">{this.state.tolerance}</div> </Col> </Row> <Row align="middle"> <Col span={24}> <Slider min={1} max={30} step={1} value={this.state.tolerance} onChange={this.onToleranceChanged} /> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.sharp} onChange={this.onSharpChanged}> Sharp Line </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.resizing} onChange={this.onResingChanged} > Snap on Resizing </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.filter != null} onChange={this.onFilterChanged} > Add Filter(only circle) </Checkbox> </Col> </Row> </Card> ) } } ``` #### Edges - Canonical page: https://x6.antv.antgroup.com/en/tutorial/basic/edge/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/basic/edge.md - Description: Edges --- title: "Edges" description: "Edges" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/basic/edge/" version: "3.x" --- > **This chapter mainly introduces knowledge related to edges. By reading, you can learn about** - Methods to add edges - How to configure the shape of edges - How to add arrows to edges - How to customize edges - How to modify edges through the API ## Adding Edges Both nodes and edges share a common base class [Cell](/en/api/model/cell). In addition to inheriting properties from `Cell`, edges support the following options. | Property Name | Type | Default Value | Description | |----------------|--------------------|------------------------------------------------|----------------------| | source | TerminalData | - | Source node or starting point. | | target | TerminalData | - | Target node or endpoint. | | vertices | Point.PointLike[] | - | Path points. | | router | RouterData | - | Router. | | connector | ConnectorData | - | Connector. | | labels | Label[] | - | Labels. | | defaultLabel | Label | [Default Label](/en/api/model/edge#labels-and-default-label) | Default label. | ```ts graph.addEdge({ shape: 'edge', source: 'node1', target: 'node2', }) ``` ## Configuring Edges Let's take a look at how to use the configurations mentioned above. ### source/target The source and target nodes (points) of the edge. ```ts graph.addEdge({ source: rect1, // Source node target: rect2, // Target node }) graph.addEdge({ source: 'rect1', // Source node ID target: 'rect2', // Target node ID }) graph.addEdge({ source: { cell: rect1, port: 'out-port-1' }, // Source node and connection port ID target: { cell: 'rect2', port: 'in-port-1' }, // Target node ID and connection port ID }) graph.addEdge({ source: 'rect1', // Source node ID target: { x: 100, y: 120 }, // Target point }) ``` ### vertices Path points. The edge starts from the starting point, passes through the path points in order, and finally reaches the endpoint. ```ts graph.addEdge({ source: rect1, target: rect2, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', // 继承于 rect 节点 width: 100, height: 40, markup: [ { tagName: 'rect', // 标签名称 selector: 'body', // 选择器 }, { tagName: 'image', selector: 'img', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, img: { 'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 16, height: 16, x: 12, y: 12, }, }, }, 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-node', // 可以直接使用上面注册过的 shape x: 40, y: 40, label: 'hello', }) const target = graph.addNode({ shape: 'custom-node', x: 300, y: 220, label: 'world', }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="vertices-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### router The `router` will further process the `vertices`, adding additional points if necessary, and then return the processed points. For example, after processing with [orth routing](/en/api/registry/router#orth), each link segment of the edge will be horizontal or vertical. ```ts graph.addEdge({ source: rect1, target: rect2, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], // If there are no args parameters, it can be simplified to router: 'orth' router: { name: 'orth', args: {}, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', // 继承于 rect 节点 width: 100, height: 40, markup: [ { tagName: 'rect', // 标签名称 selector: 'body', // 选择器 }, { tagName: 'image', selector: 'img', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, img: { 'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 16, height: 16, x: 12, y: 12, }, }, }, 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-node', // 可以直接使用上面注册过的 shape x: 40, y: 40, label: 'hello', }) const target = graph.addNode({ shape: 'custom-node', x: 300, y: 220, label: 'world', }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], router: 'orth', }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="router-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` X6 provides the following routing options by default. Click the links below to see how each routing option is used. - [normal](/en/api/registry/router#normal) - [orth](/en/api/registry/router#orth) - [oneSide](/en/api/registry/router#oneside) - [manhattan](/en/api/registry/router#manhattan) - [metro](/en/api/registry/router#metro) - [er](/en/api/registry/router#er) Additionally, we can register custom routers. For more details, please refer to the [Custom Router](/en/api/registry/router#registry) tutorial. ### connector The `connector` processes the points returned by the `router` into the [pathData](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) needed for rendering the edge. For example, the `rounded` connector will round the corners between the lines. ```ts graph.addEdge({ source: rect1, target: rect2, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], router: 'orth', // If there are no args parameters, it can be simplified to connector: 'rounded' connector: { name: 'rounded', args: {}, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', // 继承于 rect 节点 width: 100, height: 40, markup: [ { tagName: 'rect', // 标签名称 selector: 'body', // 选择器 }, { tagName: 'image', selector: 'img', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, img: { 'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 16, height: 16, x: 12, y: 12, }, }, }, 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-node', // 可以直接使用上面注册过的 shape x: 40, y: 40, label: 'hello', }) const target = graph.addNode({ shape: 'custom-node', x: 300, y: 220, label: 'world', }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, vertices: [ { x: 100, y: 200 }, { x: 300, y: 120 }, ], router: 'orth', connector: 'rounded', }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="connector-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` X6 provides the following connector options by default. Click the links below to see how each connector is used. - [normal](/en/api/registry/connector#normal) - [rounded](/en/api/registry/connector#rounded) - [smooth](/en/api/registry/connector#smooth) - [jumpover](/en/api/registry/connector#jumpover) Additionally, we can register custom connectors. For more details, please refer to the [Custom Connector](/en/api/registry/connector) tutorial. ### labels Used to set label text, position, style, etc. Supports multiple labels in array form, and each item specified in `labels` will be used after being [merged](https://lodash.com/docs/4.17.15#merge) with the [defaultLabel](/en/api/model/edge#labels-and-default-label). ```ts const edge = graph.addEdge({ source: rect1, target: rect2, labels: [ { attrs: { label: { text: 'edge', }, }, }, ], }) // Or const edge = graph.addEdge({ source: rect1, target: rect2, labels: ['edge'], // Multiple labels can be set through labels, and when only setting label text, this syntax can be simplified }) // Or const edge = graph.addEdge({ source: rect1, target: rect2, label: 'edge', // A single label can be set through label, and when only setting label text, this syntax can be simplified }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', // 继承于 rect 节点 width: 100, height: 40, markup: [ { tagName: 'rect', // 标签名称 selector: 'body', // 选择器 }, { tagName: 'image', selector: 'img', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, img: { 'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 16, height: 16, x: 12, y: 12, }, }, }, 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-node', x: 40, y: 40, label: 'hello', }) const target = graph.addNode({ shape: 'custom-node', x: 300, y: 220, label: 'world', }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, labels: [ { attrs: { label: { text: '40%', stroke: '#aaa', }, }, position: 0.4, }, { attrs: { label: { text: '60%', stroke: '#aaa', }, }, position: 0.6, }, ], }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="labels-app "> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` In addition to setting text, you can also create complex shapes on the edge using Label, which we will detail in the [API](/en/api/model/labels). ### defaultLabel Default label. The default label can simplify the label configuration items, and each item specified in `labels` will be used after being merged with `defaultLabel`. ## Using Arrows We define two special properties, `sourceMarker` and `targetMarker`, to customize the starting and ending arrows of the edge. For example, for `Shape.Edge`, we can specify the starting and ending arrows using the `line` selector. ### Built-in Arrows X6 provides the following built-in arrows. When using them, you only need to specify the arrow name and parameters (optional). - [block](/en/api/model/marker#block) - [classic](/en/api/model/marker#classic) - [diamond](/en/api/model/marker#diamond) - [cross](/en/api/model/marker#cross) - [async](/en/api/model/marker#async) - [path](/en/api/model/marker#path) - [circle](/en/api/model/marker#circle) - [circlePlus](/en/api/model/marker#circleplus) - [ellipse](/en/api/model/marker#ellipse) ```ts graph.addEdge({ shape: 'edge', source: [100, 100], target: [500, 500], attrs: { line: { sourceMarker: 'block', // Solid arrow targetMarker: { name: 'ellipse', // Ellipse rx: 10, // X radius of the ellipse arrow ry: 6, // Y radius of the ellipse arrow }, }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' const markers = [ 'block', 'classic', 'diamond', 'circle', 'circlePlus', 'ellipse', 'cross', 'async', ] export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) markers.forEach((marker, i) => { graph.addEdge({ sourcePoint: [120, 20 + i * 40], targetPoint: [400, 20 + i * 40], label: marker, attrs: { line: { sourceMarker: marker, targetMarker: marker, stroke: '#8f8f8f', strokeWidth: 1, }, }, }) }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="native-marker-app "> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` > **Tip** By default, X6 edges come with a `classic` arrow. If you want to remove it, you can set `targetMarker` to `null`. ### Custom Arrows We can also render arrows using SVG elements specified by `tagName`. For example, below we use the `<path>` element to render the arrow, which inherits the edge's fill color `fill` and border color `stroke` by default. ```ts graph.addEdge({ shape: 'edge', source: [100, 100], target: [500, 500], attrs: { line: { sourceMarker: { tagName: 'path', d: 'M 20 -10 0 0 20 10 Z', }, targetMarker: { tagName: 'path', fill: 'yellow', // Use custom fill color stroke: 'green', // Use custom border color strokeWidth: 2, d: 'M 20 -10 0 0 20 10 Z', }, }, }, }) ``` > **Tip** Our starting and ending arrows use the same `d` attribute because we automatically calculate the arrow direction. In simple terms, when defining the arrow, we only need to define an arrow that points **towards the origin**. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ source: [100, 140], target: [400, 140], label: 'custom-marker', attrs: { line: { sourceMarker: { tagName: 'path', d: 'M 20 -10 0 0 20 10 Z', }, targetMarker: { tagName: 'path', stroke: '#D94111', strokeWidth: 2, fill: '#90C54C', d: 'M 20 -10 0 0 20 10 Z', }, stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="custom-marker-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` For more examples and customization tips for arrows, please refer to the [API](/en/api/model/marker). ## Customizing Edges Like nodes, we can customize the shape and style of edges using `markup` and `attrs`, and we can also register custom edges for reuse. The default edge `Shape.Edge` in X6 defines two selectors: `line` (representing the path element) and `wrap` (representing a transparent path element for interaction). We can define the style of the edge as shown below. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerEdge('double-edge', { inherit: 'edge', markup: [ { tagName: 'path', selector: 'outline', attrs: { fill: 'none', }, }, { tagName: 'path', selector: 'line', attrs: { fill: 'none', cursor: 'pointer', }, }, ], attrs: { line: { connection: true, stroke: '#dddddd', strokeWidth: 4, strokeLinejoin: 'round', targetMarker: { tagName: 'path', stroke: '#000000', d: 'M 10 -3 10 -10 -2 0 10 10 10 3', }, }, outline: { connection: true, stroke: '#000000', strokeWidth: 6, strokeLinejoin: 'round', }, }, }) Graph.registerEdge('shadow-edge', { inherit: 'edge', markup: [ { tagName: 'path', selector: 'shadow', attrs: { fill: 'none', }, }, { tagName: 'path', selector: 'line', attrs: { fill: 'none', cursor: 'pointer', }, }, ], attrs: { line: { connection: true, stroke: '#dddddd', strokeWidth: 20, strokeLinejoin: 'round', targetMarker: { name: 'path', stroke: 'none', d: 'M 0 -10 -10 0 0 10 z', offsetX: -5, }, sourceMarker: { name: 'path', stroke: 'none', d: 'M -10 -10 0 0 -10 10 0 10 0 -10 z', offsetX: -5, }, }, shadow: { connection: true, refX: 3, refY: 6, stroke: '#000000', strokeOpacity: 0.2, strokeWidth: 20, strokeLinejoin: 'round', targetMarker: { name: 'path', d: 'M 0 -10 -10 0 0 10 z', stroke: 'none', offsetX: -5, }, sourceMarker: { name: 'path', stroke: 'none', d: 'M -10 -10 0 0 -10 10 0 10 0 -10 z', offsetX: -5, }, }, }, }) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addEdge({ shape: 'double-edge', source: [100, 100], target: [400, 100], }) graph.addEdge({ shape: 'shadow-edge', source: [100, 180], target: [400, 180], }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="registry-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Modifying Edges Similar to nodes, after rendering is complete, we can modify all properties of edges through the API. We commonly use the following two methods: - edge.prop(path, value), for detailed usage see [prop](/en/api/model/cell#node-and-edge-properties). - edge.attr(path, value), for detailed usage see [attr](/en/api/model/cell#element-attributes-attrs). Let's take a look at the `prop` of the default edge provided by X6. ```ts const edge = graph.addEdge({ source: [200, 140], target: [500, 140], label: 'edge', }) console.log(edge.prop()) // Output { "shape": "edge", "attrs": { "lines": { "connection": true, "strokeLinejoin": "round" }, "wrap": { "strokeWidth": 10 }, "line": { "stroke": "#333", "strokeWidth": 2, "targetMarker": "classic" } }, "id": "9d5e4f54-1ed3-429e-8d8c-a1526cff2cd8", "source": { "x": 200, "y": 140 }, "target": { "x": 500, "y": 140 }, "labels": [{ "attrs": { "label": { "text": "edge" } } }], "zIndex": 1 } ``` From the output above, we can see that `prop` is a new configuration after processing, and its values can be updated through methods. After updating, the edge will immediately refresh to the latest state. To modify the edge's `attrs` more conveniently, X6 provides the `attr` method. ```ts edge.prop('target', { x: 300, y: 300 }) // Modify the endpoint edge.attr('line/stroke', '#ccc') // Modify the edge color, equivalent to edge.prop('attrs/line/stroke', '#ccc') ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Color } from '@antv/x6' import { Button } from 'antd' const commands = [ { key: 'prop', label: 'prop', }, { key: 'attr', label: 'attr', }, ] export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const source = graph.addNode({ shape: 'rect', x: 40, y: 100, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode({ shape: 'rect', x: 340, y: 100, width: 100, height: 40, label: 'hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph = graph } change = (command: string) => { const edges = this.graph.getEdges() switch (command) { case 'prop': edges.forEach((edge) => { const x = Math.floor(Math.random() * 600) const y = Math.floor(Math.random() * 200) edge.prop('vertices', [[x, y]]) }) break case 'attr': edges.forEach((edge) => { const color = Color.randomHex() edge.attr('line/stroke', color) }) break default: break } } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="prop-app"> <div className="app-btns"> <Button.Group> {commands.map((item) => ( <Button onClick={() => this.change(item.key)} key={item.key}> {item.label} </Button> ))} </Button.Group> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/tutorial/basic/edge/vertices/index.less ```less .vertices-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/edge/router/index.less ```less .router-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/edge/connector/index.less ```less .connector-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/edge/labels/index.less ```less .labels-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/edge/native-marker/index.less ```less .native-marker-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/edge/custom-marker/index.less ```less .custom-marker-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/edge/registry/index.less ```less .registry-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` ### docs/demos/tutorial/basic/edge/prop/index.less ```less .prop-app { border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-btns { bottom: 0; margin: 0; padding: 0 8px; } .app-content { width: 100%; height: 240px; margin: 0; margin-top: 16px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` #### Tools - Canonical page: https://x6.antv.antgroup.com/en/tutorial/intermediate/tools/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/intermediate/tools.md - Description: Tools --- title: "Tools" description: "Tools" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/intermediate/tools/" version: "3.x" --- > **This chapter mainly introduces knowledge related to tools. By reading, you can learn about** - How to add tools for nodes or edges - Common tools built into X6 ## Using Tools When creating nodes/edges, you can add tools through the [`tools`](/en/api/model/cell#tools) option: ```ts graph.addNode({ tools: [ { name: 'button-remove', // Tool name args: { // Tool parameters x: 10, y: 10, }, }, ], }) // If no parameters are needed, you can abbreviate as: graph.addNode({ tools: ['button-remove'], }) graph.addEdge({ source, target, vertices: [ { x: 90, y: 160, }, { x: 210, y: 160, }, ], tools: ['vertices', 'segments'], }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-tools-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, 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-tools-node', x: 40, y: 40, label: 'Source', tools: ['button-remove'], }) const target = graph.addNode({ shape: 'custom-tools-node', x: 160, y: 240, label: 'Target', tools: ['button-remove'], }) graph.addEdge({ source, target, vertices: [ { x: 90, y: 160 }, { x: 210, y: 160 }, ], tools: ['vertices', 'segments'], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="tools-basic-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` After nodes/edges are created, you can call methods like [hasTool(name)](/en/api/model/cell#hastool), [addTools(...)](/en/api/model/cell#addtools), and [removeTools()](/en/api/model/cell#removetools) to add or remove tools. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-tools-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, 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-tools-node', x: 40, y: 40, label: 'Source', }) const target = graph.addNode({ shape: 'custom-tools-node', x: 160, y: 240, label: 'Target', }) graph.addEdge({ source, target, vertices: [ { x: 90, y: 160 }, { x: 210, y: 160 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.on('cell:mouseenter', ({ cell }) => { if (cell.isNode()) { cell.addTools([ { name: 'boundary', args: { attrs: { fill: '#7c68fc', stroke: '#333', 'stroke-width': 1, 'fill-opacity': 0.2, }, }, }, { name: 'button-remove', args: { x: 0, y: 0, offset: { x: 10, y: 10 }, }, }, ]) } else { cell.addTools(['vertices', 'segments']) } }) graph.on('cell:mouseleave', ({ cell }) => { cell.removeTools() }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="tools-onhover-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Built-in Tools Tools are widgets rendered on nodes/edges to enhance interactivity. We provide the following built-in tools for nodes and edges: ### Nodes - [button](/en/api/registry/node-tool#button) Renders a button at a specified position; supports custom click interactions. - [button-remove](/en/api/registry/node-tool#button-remove) Renders a delete button at a specified position; clicking deletes the node. - [boundary](/en/api/registry/node-tool#boundary) Renders a rectangle around the node based on its bounding box. Note: visualization only; no interaction. ### Edges - [vertices](/en/api/registry/edge-tool#vertices) Renders dots at path points. Drag to move; double-click to delete; click on the edge to add. - [segments](/en/api/registry/edge-tool#segments) Renders a handle at each segment’s center; drag to adjust the adjacent path points. - [boundary](/en/api/registry/edge-tool#boundary) Renders a rectangle around the edge based on its bounding box. Note: visualization only; no interaction. - [button](/en/api/registry/edge-tool#button) Renders a button at a specified position; supports custom click interactions. - [button-remove](/en/api/registry/edge-tool#button-remove) Renders a delete button at a specified position; clicking deletes the edge. - [source-arrowhead and target-arrowhead](/en/api/registry/edge-tool#source-arrowhead-and-target-arrowhead) Renders a shape (arrow by default) at the source/target terminal; drag to adjust the terminal. ### docs/demos/tutorial/intermediate/tools/basic/index.less ```less .tools-basic-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/intermediate/tools/onhover/index.less ```less .tools-onhover-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### Clipboard - Canonical page: https://x6.antv.antgroup.com/en/tutorial/plugins/clipboard/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/plugins/clipboard.md - Description: Clipboard --- title: "Clipboard" description: "Clipboard" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/plugins/clipboard/" version: "3.x" --- > **This section mainly introduces the Clipboard plugin. By reading, you will learn:** - How to use copy and paste ## Usage The clipboard is used for copying/pasting nodes and edges. Enable the feature via the `Clipboard` plugin: ```ts import { Graph, Clipboard } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use( new Clipboard({ enabled: true, }), ) ``` ## Demo - After selecting a node, click the copy button to copy the node. - Set different `offset` values to observe the effect on the node's position when pasting. - After enabling `localStorage`, copy a node, refresh the page or reopen the browser, then click the paste button. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Button, message } from 'antd' import { Graph, Clipboard, Selection } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph private options: State componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.graph.use( new Selection({ enabled: true, showNodeSelectionBox: true, }), ) this.graph.use( new Clipboard({ enabled: true, useLocalStorage: true, }), ) this.graph.addNode({ x: 280, y: 80, width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const source = this.graph.addNode({ x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 180, y: 160, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } onSettingsChanged = (options: State) => { this.options = { ...options } } refContainer = (container: HTMLDivElement) => { this.container = container } onCopy = () => { const cells = this.graph.getSelectedCells() if (cells && cells.length) { this.graph.copy(cells, this.options) message.success('复制成功') } else { message.info('请先选中节点再复制') } } onPaste = () => { if (this.graph.isClipboardEmpty()) { message.info('剪切板为空,不可粘贴') } else { const cells = this.graph.paste(this.options) this.graph.cleanSelection() this.graph.select(cells) message.success('粘贴成功') } } render() { return ( <div className="clipboard-app"> <div className="app-side"> <Settings onChange={this.onSettingsChanged} /> <div className="app-btns"> <Button onClick={this.onCopy} type="primary"> Copy Selected Cells </Button> <Button onClick={this.onPaste} type="ghost"> Paste </Button> </div> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Configuration | Property Name | Type | Default Value | Required | Description | |---------------------|---------|---------------|----------|---------------------------------------------------------------------------------------------------| | useLocalStorage | boolean | `false` | | When enabled, the copied nodes/edges are also saved to `localStorage`, allowing copy/paste to work after refreshing or reopening the browser. | ## API ### graph.copy(...) ```ts copy(cells: Cell[], options: CopyOptions = {}): this ``` Copy nodes/edges. Parameters are as follows: | Name | Type | Required | Default Value | Description | |-------------------------|---------|:--------:|---------------|-----------------------------------------------| | cells | Cell[] | ✓ | | The nodes/edges to be copied. | | options.deep | boolean | | - | Whether to recursively copy all child nodes/edges. | | options.useLocalStorage | boolean | | - | Whether to save the copied nodes/edges in `localStorage`. | ### graph.cut(...) ```ts cut(cells: Cell[], options: CopyOptions = {}): this ``` Cut nodes/edges. Parameters are as follows: | Name | Type | Required | Default Value | Description | |-------------------------|---------|:--------:|---------------|-----------------------------------------------| | cells | Cell[] | ✓ | | The nodes/edges to be cut. | | options.deep | boolean | | - | Whether to recursively copy all child nodes/edges. | | options.useLocalStorage | boolean | | - | Whether to save the copied nodes/edges in `localStorage`. | ### graph.paste(...) ```ts paste(options?: PasteOptions, graph?: Graph): Cell[] ``` Paste and return the nodes/edges pasted onto the canvas. Parameters are as follows: | Name | Type | Required | Default Value | Description | |-------------------------|----------------------------------------|:--------:|---------------|---------------------------------------------| | options.useLocalStorage | boolean | | - | Whether to use nodes/edges from `localStorage`. | | options.offset | number \| `{ dx: number; dy: number }` | | `20` | The offset for the nodes/edges pasted onto the canvas. | | options.nodeProps | Node.Properties | | - | Additional properties for the nodes pasted onto the canvas. | | options.edgeProps | Edge.Properties | | - | Additional properties for the edges pasted onto the canvas. | | graph | Graph | | `this` | The target canvas for pasting, defaults to the current canvas. | ### graph.getCellsInClipboard() ```ts getCellsInClipboard(): Cell[] ``` Get the nodes/edges in the clipboard. ### graph.cleanClipboard() ```ts cleanClipboard(): this ``` Clear the clipboard. ### graph.isClipboardEmpty() ```ts isClipboardEmpty(): boolean ``` Return whether the clipboard is empty. ### graph.isClipboardEnabled() ```ts isClipboardEnabled(): boolean ``` Return whether the clipboard is enabled. ### graph.enableClipboard() ```ts enableClipboard(): this ``` Enable the clipboard. ### graph.disableClipboard() ```ts disableClipboard(): this ``` Disable the clipboard. ### graph.toggleClipboard(...) ```ts toggleClipboard(enabled?: boolean): this ``` Toggle the clipboard's enabled state. Parameters are as follows: | Name | Type | Required | Default Value | Description | |---------|---------|:--------:|---------------|-----------------------------------------------| | enabled | boolean | | - | Whether to enable the clipboard; if omitted, toggles the current enabled state. | ## Events | Event Name | Parameter Type | Description | |-----------------------|---------------------------|----------------------------------| | `clipboard:changed` | `{ cells: Cell[] }` | Triggered when copying, cutting, or clearing the clipboard. | ```ts graph.on('clipboard:changed', ({ cells }) => { console.log(cells) }) // We can also listen to events on the plugin instance clipboard.on('clipboard:changed', ({ cells }) => { console.log(cells) }) ``` ### docs/demos/tutorial/plugins/clipboard/index.less ```less .clipboard-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } .app-btns { display: flex; margin-top: 16px; } } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-btn { flex: 1; margin-right: 8px; } .ant-btn:last-child { margin-right: 0; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .x6-widget-selection-box { border: 2px dashed #239edd; } .x6-widget-selection-inner { border: 1px solid #239edd; } } ``` ### docs/demos/tutorial/plugins/clipboard/settings.tsx ```tsx import React from 'react' import { Checkbox, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { offset: number useLocalStorage: boolean } export class Settings extends React.Component<Props, State> { state: State = { offset: 30, useLocalStorage: true, } componentDidMount() { this.notifyChange() } notifyChange() { this.props.onChange({ ...this.state, }) } onUseLocalStorageChanged = (e: any) => { this.setState({ useLocalStorage: e.target.checked }, () => { this.notifyChange() }) } onOffsetChanged = (offset: number) => { this.setState({ offset }, () => { this.notifyChange() }) } render() { return ( <Card title="Clipboard Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={8}>Paste Offset</Col> <Col span={2} offset={1}> <div className="slider-value">{this.state.offset}</div> </Col> </Row> <Row align="middle"> <Col span={24}> <Slider min={10} max={80} step={1} value={this.state.offset} onChange={this.onOffsetChanged} /> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.useLocalStorage} onChange={this.onUseLocalStorageChanged} > Use LocalStorage </Checkbox> </Col> </Row> </Card> ) } } ``` #### Ports - Canonical page: https://x6.antv.antgroup.com/en/tutorial/basic/port/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/basic/port.md - Description: Ports --- title: "Ports" description: "Ports" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/basic/port/" version: "3.x" --- > **This chapter mainly introduces knowledge related to ports. By reading, you can learn about** - How to configure ports in nodes - Adding, deleting, and modifying ports - How to configure port positions - How to configure label positions on ports ## Configuring Connection Ports First, we group connection ports that have the same behavior and appearance into the same group, and set the grouping using the `groups` option, which is an object `{ [groupName: string]: PortGroupMetadata }`. The group name is the key, and the value is the default options for each group of connection ports. The supported options are as follows: ```ts interface PortGroupMetadata { markup?: Markup // Definition of the connection port DOM structure. attrs?: Attr.CellAttrs // Attributes and styles. zIndex?: number | 'auto' // The DOM layer level of the connection port; the higher the value, the higher the level. // Layout of connection ports in the group. position?: [number, number] | string | { name: string; args?: object } label?: { // Connection port label markup?: Markup position?: { // Layout of the connection port label name: string // Layout name args?: object // Layout parameters } } } ``` Next, we configure `items`, which is an array `PortMetadata[]`. Each item in the array represents a connection port, and the supported options for connection ports are as follows: ```ts interface PortMetadata { id?: string // Unique ID for the connection port, automatically generated by default. group?: string // Group name; specifying a group will inherit the options of the connection ports in that group. args?: object // Parameters for the layout algorithm specified for the connection ports in the group. We cannot specify a layout algorithm for a single connection port, but we can provide different parameters for the layout algorithm specified for the group. markup?: Markup // Definition of the connection port's DOM structure. Specifying this option will override the default options provided by the group referred to by `group`. attrs?: Attr.CellAttrs // Element's attribute styles. Specifying this option will override the default options provided by the group referred to by `group`. zIndex?: number | 'auto' // The DOM layer level of the connection port; the higher the value, the higher the level. Specifying this option will override the default options provided by the group referred to by `group`. label?: { // Label for the connection port. Specifying this option will override the default options provided by the group referred to by `group`. markup?: Markup // Label DOM structure position?: { // Label position name: string // Name of the label position calculation method args?: object // Parameters for the label position calculation method } } } ``` The following example code clearly shows how to define connection ports. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node-with-port', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { top: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, bottom: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, }, }, 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-node-with-port', x: 40, y: 40, label: 'hello', ports: { items: [ { id: 'port_1', group: 'bottom', }, { id: 'port_2', group: 'bottom', }, ], }, }) const target = graph.addNode({ shape: 'custom-node-with-port', x: 160, y: 180, label: 'world', ports: { items: [ { id: 'port_3', group: 'top', }, { id: 'port_4', group: 'top', }, ], }, }) graph.addEdge({ source: { cell: source, port: 'port_2' }, target: { cell: target, port: 'port_3' }, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="config-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Modifying Connection Ports There is a rich [API](/en/api/model/node#connection-ports) for adding, deleting, and modifying connection ports on nodes. ```ts // Add a connection port node.addPort({ group: 'top', attrs: { text: { text: 'xx', }, }, }) // Remove a connection port node.removePort(portId) // Update a connection port node.portProp(portId, 'attrs/circle/stroke', color) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node, Color } from '@antv/x6' import { Button } from 'antd' Graph.registerNode( 'custom-node-with-port', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { top: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, label: { position: 'top', }, }, bottom: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, label: { position: 'top', }, }, }, }, }, true, ) const commands = [ { key: 'addPort', label: 'addPort', }, { key: 'removePort', label: 'removePort', }, { key: 'updatePort', label: 'updatePort', }, ] export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addNode({ id: '1', shape: 'custom-node-with-port', x: 150, y: 100, label: 'hello', }) graph.centerContent() this.graph = graph } change = (command: string) => { const node = this.graph.getCellById('1') as Node const ports = node.getPorts() let color: string switch (command) { case 'addPort': node.addPort({ group: 'top', attrs: { text: { text: `${ports.length + 1}`, }, }, }) break case 'removePort': if (ports.length) { node.removePortAt(ports.length - 1) } break case 'updatePort': color = Color.randomHex() ports.forEach((port) => { node.portProp(port.id!, 'attrs/circle/stroke', color) }) break default: break } } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="dynamic-app"> <div className="app-btns"> <Button.Group> {commands.map((item) => ( <Button onClick={() => this.change(item.key)} key={item.key}> {item.label} </Button> ))} </Button.Group> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Connection Port Position The layout algorithm for connection ports can only be specified through the `position` option in `groups`, as the layout algorithm needs to consider all connection ports in the group when calculating their positions. We can influence the layout result of a single connection port through the `args` option. We provide the following layout algorithms for connection ports by default, and also support [custom connection port layout algorithms and registration](/en/api/registry/port-layout). Click the links below to learn how to use each layout algorithm. - [`absolute`](/en/api/registry/port-layout#absolute) Absolute positioning. - [`left`](/en/api/registry/port-layout#left-right-top-bottom) Evenly distributed on the left side of rectangular nodes. - [`right`](/en/api/registry/port-layout#left-right-top-bottom) Evenly distributed on the right side of rectangular nodes. - [`top`](/en/api/registry/port-layout#left-right-top-bottom) Evenly distributed on the top of rectangular nodes. - [`bottom`](/en/api/registry/port-layout#left-right-top-bottom) Evenly distributed on the bottom of rectangular nodes. - [`line`](/en/api/registry/port-layout#line) Evenly distributed along a specified line. - [`ellipse`](/en/api/registry/port-layout#ellipse) Distributed along an elliptical arc. - [`ellipseSpread`](/en/api/registry/port-layout#ellipsespread) Evenly distributed along an ellipse. ## Connection Port Label Position The label position can be specified in `groups.label.position` and in the node's `ports.items.label.position` option. We provide the following label positions by default, and also support [custom label positions and registration](/en/api/registry/port-label-layout). Click the links below to learn how to use each label position. - [`left`](/en/api/registry/port-label-layout#side) The label is located on the left side of the connection port. - [`right`](/en/api/registry/port-label-layout#side) The label is located on the right side of the connection port. - [`top`](/en/api/registry/port-label-layout#side) The label is located above the connection port. - [`bottom`](/en/api/registry/port-label-layout#side) The label is located below the connection port. - [`inside`](/en/api/registry/port-label-layout#insideoutside) The label is located inside the node (close to the edge). - [`outside`](/en/api/registry/port-label-layout#insideoutside) The label is located outside the node (close to the edge). - [`insideOriented`](/en/api/registry/port-label-layout#insideoutside) The label is located inside the node and automatically adjusts the text direction based on its position. - [`outsideOriented`](/en/api/registry/port-label-layout#insideoutside) The label is located outside the node and automatically adjusts the text direction based on its position. - [`radial`](/en/api/registry/port-label-layout#radial) The label is located outside circular or elliptical nodes. - [`radialOriented`](/en/api/registry/port-label-layout#radial) The label is located outside circular or elliptical nodes and the label text automatically rotates along the arc direction. ### docs/demos/tutorial/basic/ports/config/index.less ```less .config-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` ### docs/demos/tutorial/basic/ports/dynamic/index.less ```less .dynamic-app { border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-btns { bottom: 0; margin: 0; padding: 0 8px; } .app-content { width: 100%; height: 240px; margin: 0; margin-top: 16px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` #### Group - Canonical page: https://x6.antv.antgroup.com/en/tutorial/intermediate/group/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/intermediate/group.md - Description: Group --- title: "Group" description: "Group" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/intermediate/group/" version: "3.x" --- > **This chapter mainly introduces knowledge related to grouping. By reading, you can learn about** - How to group nodes - How to group nodes via interaction - How to restrict child node movement in a group - How to automatically expand the parent node - How to expand and collapse the parent node ## Group Nodes You can build groups via parent–child relationships, with [methods](/en/api/model/cell#parentchildren-relationship) to get and set them. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-group-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, 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-group-node', x: 60, y: 100, label: 'Child\n(inner)', zIndex: 2, }) const target = graph.addNode({ shape: 'custom-group-node', x: 420, y: 80, label: 'Child\n(outer)', zIndex: 2, }) const parent = graph.addNode({ shape: 'custom-group-node', x: 40, y: 40, width: 360, height: 160, zIndex: 1, label: 'Parent\n(try to move me)', }) parent.addChild(source) parent.addChild(target) graph.addEdge({ source, target, vertices: [ { x: 120, y: 60 }, { x: 200, y: 100 }, ], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="embed-edge-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` From the example above: - Moving the parent node also moves its children, even if they are outside. - By default, the edge’s parent is the common parent of its terminals. Moving the parent moves the edge’s vertices. ## Group via Interaction Sometimes you need to drag one node into another to make it a child. Enable `embedding` and implement `findParent` to return the parent on move. See the [API](/en/tutorial/basic/interacting#embedding) for details. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, embedding: { enabled: true, findParent({ node }) { const bbox = node.getBBox() return this.getNodes().filter((node) => { const data = node.getData<{ parent: boolean }>() if (data && data.parent) { const targetBBox = node.getBBox() return bbox.isIntersectWithRect(targetBBox) } return false }) }, }, }) graph.addNode({ shape: 'custom-node', x: 40, y: 140, width: 120, height: 60, label: 'Child\n(unembed)', zIndex: 10, }) graph.addNode({ shape: 'custom-node', x: 500, y: 100, width: 120, height: 60, label: 'Child\n(unembed)', zIndex: 10, }) graph.addNode({ shape: 'custom-node', x: 200, y: 80, width: 240, height: 160, zIndex: 1, label: 'Parent', data: { parent: true, }, }) graph.on('node:change:parent', ({ node }) => { node.attr({ label: { text: 'Child\n(embed)', }, }) }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="embedding-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Restrict Child Movement Limit the movement range of child nodes within the parent by using the `translating.restrict` option when creating the `Graph` instance. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-group-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, translating: { restrict(view) { if (view) { const cell = view.cell if (cell.isNode()) { const parent = cell.getParent() if (parent) { return parent.getBBox() } } } return null }, }, }) const child = graph.addNode({ shape: 'custom-group-node', x: 100, y: 60, label: 'Child', zIndex: 2, }) const parent = graph.addNode({ shape: 'custom-group-node', x: 40, y: 40, width: 240, height: 160, zIndex: 1, label: 'Parent\n(try to move me)', }) parent.addChild(child) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="restrict-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Auto-expand Parent Node Listen to `node:change:position` to automatically expand/shrink the parent when a child moves, ensuring it fully encompasses the children. The example code is a bit involved; click the demo’s `CodeSandbox` link to view the full implementation. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Dom } from '@antv/x6' import { Settings, State } from './settings.tsx' Graph.registerNode( 'custom-group-node', { inherit: 'rect', width: 80, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement private embedPadding = 20 componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, embedding: { enabled: true, }, }) const source = graph.addNode({ shape: 'custom-group-node', x: 80, y: 100, width: 80, height: 40, label: 'Child', zIndex: 2, }) const target = graph.addNode({ shape: 'custom-group-node', x: 280, y: 80, label: 'Child', zIndex: 2, }) const parent = graph.addNode({ shape: 'custom-group-node', x: 40, y: 40, width: 360, height: 160, zIndex: 1, label: 'Parent', }) parent.addChild(source) parent.addChild(target) let ctrlPressed = false graph.on('node:embedding', ({ e }: { e: Dom.MouseMoveEvent }) => { ctrlPressed = e.metaKey || e.ctrlKey }) graph.on('node:embedded', () => { ctrlPressed = false }) graph.on('node:change:size', ({ node, options }) => { if (options.skipParentHandler) { return } const children = node.getChildren() if (children && children.length) { node.prop('originSize', node.getSize()) } }) graph.on('node:change:position', ({ node, options }) => { if (options.skipParentHandler || ctrlPressed) { return } const children = node.getChildren() if (children && children.length) { node.prop('originPosition', node.getPosition()) } const parent = node.getParent() if (parent && parent.isNode()) { let originSize = parent.prop('originSize') if (originSize == null) { originSize = parent.getSize() parent.prop('originSize', originSize) } let originPosition = parent.prop('originPosition') if (originPosition == null) { originPosition = parent.getPosition() parent.prop('originPosition', originPosition) } let x = originPosition.x let y = originPosition.y let cornerX = originPosition.x + originSize.width let cornerY = originPosition.y + originSize.height let hasChange = false const children = parent.getChildren() if (children) { children.forEach((child) => { const bbox = child.getBBox().inflate(this.embedPadding) const corner = bbox.getCorner() if (bbox.x < x) { x = bbox.x hasChange = true } if (bbox.y < y) { y = bbox.y hasChange = true } if (corner.x > cornerX) { cornerX = corner.x hasChange = true } if (corner.y > cornerY) { cornerY = corner.y hasChange = true } }) } if (hasChange) { parent.prop( { position: { x, y }, size: { width: cornerX - x, height: cornerY - y }, }, // Note that we also pass a flag so that we know we shouldn't // adjust the `originPosition` and `originSize` in our handlers. { skipParentHandler: true }, ) } } }) } refContainer = (container: HTMLDivElement) => { this.container = container } onSettingsChanged = (state: State) => { this.embedPadding = state.padding } render() { return ( <div className="expand-shrink-app"> <div className="app-side"> <Settings onChange={this.onSettingsChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Expand/Collapse Parent Node First, define a custom `Group` node that renders an expand/collapse button at the top-left corner and sets a custom event `node:collapse` on that button: ```ts import { Node } from '@antv/x6' export class Group extends Node { private collapsed: boolean = false private expandSize: { width: number; height: number } protected postprocess() { this.toggleCollapse(false) } isCollapsed() { return this.collapsed } toggleCollapse(collapsed?: boolean) { const target = collapsed == null ? !this.collapsed : collapsed if (target) { this.attr('buttonSign', { d: 'M 1 5 9 5 M 5 1 5 9' }) this.expandSize = this.getSize() this.resize(100, 32) } else { this.attr('buttonSign', { d: 'M 2 5 8 5' }) if (this.expandSize) { this.resize(this.expandSize.width, this.expandSize.height) } } this.collapsed = target } } Group.config({ markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'text', selector: 'label', }, { tagName: 'g', selector: 'buttonGroup', children: [ { tagName: 'rect', selector: 'button', }, { tagName: 'path', selector: 'buttonSign', }, ], }, ], attrs: { body: { ... }, label: { ... }, buttonGroup: { ... }, button: { ... // Custom event event: 'node:collapse', }, buttonSign: { ... }, }, }) ``` Then listen for `node:collapse` on the `graph`, and show/hide the corresponding child nodes when the parent node is expanded/collapsed: ```ts graph.on('node:collapse', ({ node }: { node: Group }) => { node.toggleCollapse() const collapsed = node.isCollapsed() const cells = node.getDescendants() cells.forEach((node) => { if (collapsed) { node.hide() } else { node.show() } }) }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Group } from './shape.ts' Graph.registerNode( 'custom-group-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const createGroup = ( id: string, x: number, y: number, width: number, height: number, fill: string, ) => { const group = new Group({ id, x, y, width, height, attrs: { body: { fill }, label: { text: id }, }, }) graph.addNode(group) return group } const createNode = ( id: string, x: number, y: number, width: number, height: number, ) => { return graph.addNode({ shape: 'custom-group-node', id, x, y, width, height, label: id, }) } const createEdge = ( id: string, source: string, target: string, vertices?: { x: number; y: number }[], ) => { return graph.addEdge({ id, source, target, vertices, label: id, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } const a = createGroup('a', 100, 40, 480, 280, '#91d5ff') const aa = createGroup('aa', 180, 100, 160, 140, '#47C769') const aaa = createGroup('aaa', 200, 160, 120, 40, '#0491e4') const b = createNode('b', 450, 200, 50, 50) a.addChild(aa) aa.addChild(aaa) a.addChild(b) createNode('c', 680, 80, 50, 50) createEdge('edge1', 'aa', 'b') createEdge('edge3', 'b', 'c') aa.addChild( createEdge('edge2', 'aa', 'aaa', [ { x: 60, y: 140 }, { x: 60, y: 220 }, ]), ) graph.on('node:collapse', ({ node }: { node: Group }) => { node.toggleCollapse() const collapsed = node.isCollapsed() const collapse = (parent: Group) => { const cells = parent.getChildren() if (cells) { cells.forEach((cell) => { if (collapsed) { cell.hide() } else { cell.show() } if (cell instanceof Group) { if (!cell.isCollapsed()) { collapse(cell) } } }) } } collapse(node) }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="collapsable-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/tutorial/intermediate/group/embed-edge/index.less ```less .embed-edge-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/interacting/embedding/index.less ```less .embedding-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/intermediate/group/restrict/index.less ```less .restrict-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/intermediate/group/expand-shrink/index.less ```less .expand-shrink-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } } ``` ### docs/demos/tutorial/intermediate/group/expand-shrink/settings.tsx ```tsx import React from 'react' import { Card, Row, Col, Slider } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { padding: number } export class Settings extends React.Component<Props, State> { state: State = { padding: 20, } notifyChange() { this.props.onChange({ ...this.state, }) } onPaddingChanged = (padding: number) => { this.setState({ padding }, () => { this.notifyChange() }) } render() { return ( <Card title="Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={6}>Padding</Col> <Col span={12}> <Slider min={0} max={40} step={1} value={this.state.padding} onChange={this.onPaddingChanged} /> </Col> <Col span={1} offset={1}> <div className="slider-value">{this.state.padding}</div> </Col> </Row> <Row align="middle" style={{ borderTop: '1px solid #f0f0f0' }}> <Col style={{ padding: '24px 0 8px 0', color: '#666' }}> Press and hold on{' '} <strong style={{ color: '#faad14' }}>Ctrl or Command</strong> key, then move the child node to remove it from parent node. </Col> </Row> </Card> ) } } ``` ### docs/demos/tutorial/intermediate/group/collapsable/index.less ```less .collapsable-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 360px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/intermediate/group/collapsable/shape.ts ```ts import { Node } from '@antv/x6' export class Group extends Node { private collapsed = false private expandSize: { width: number; height: number } protected postprocess() { this.toggleCollapse(false) } isCollapsed() { return this.collapsed } toggleCollapse(collapsed?: boolean) { const target = collapsed == null ? !this.collapsed : collapsed if (target) { this.attr('buttonSign', { d: 'M 1 5 9 5 M 5 1 5 9' }) this.expandSize = this.getSize() this.resize(100, 32) } else { this.attr('buttonSign', { d: 'M 2 5 8 5' }) if (this.expandSize) { this.resize(this.expandSize.width, this.expandSize.height) } } this.collapsed = target } } Group.config({ markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'text', selector: 'label', }, { tagName: 'g', selector: 'buttonGroup', children: [ { tagName: 'rect', selector: 'button', attrs: { 'pointer-events': 'visiblePainted', }, }, { tagName: 'path', selector: 'buttonSign', attrs: { fill: 'none', 'pointer-events': 'none', }, }, ], }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', stroke: 'none', fill: '#fff', }, buttonGroup: { refX: 8, refY: 8, }, button: { height: 14, width: 16, rx: 2, ry: 2, fill: '#f5f5f5', stroke: '#ccc', cursor: 'pointer', event: 'node:collapse', }, buttonSign: { refX: 3, refY: 2, stroke: '#808080', }, label: { fontSize: 12, fill: '#fff', refX: 32, refY: 10, }, }, }) ``` #### Keyboard - Canonical page: https://x6.antv.antgroup.com/en/tutorial/plugins/keyboard/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/plugins/keyboard.md - Description: Keyboard --- title: "Keyboard" description: "Keyboard" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/plugins/keyboard/" version: "3.x" --- > **This section mainly introduces the Keyboard plugin. By reading, you will learn:** - How to bind keyboard shortcuts on the canvas ## Usage Enable keyboard shortcuts via the `Keyboard` plugin: ```ts import { Graph, Keyboard } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use( new Keyboard({ enabled: true, }), ) ``` ## Demo ```ts import './index.less' import { createRoot } from 'react-dom/client' import * as React from 'react' import { Graph, Clipboard, Keyboard, Selection } from '@antv/x6' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.graph.use( new Clipboard({ enabled: true, }), ) this.graph.use( new Selection({ enabled: true, showNodeSelectionBox: true, }), ) this.graph.use( new Keyboard({ enabled: true, global: true, }), ) this.graph.addNode({ x: 280, y: 100, width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const source = this.graph.addNode({ x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 160, y: 180, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph.bindKey('ctrl+c', () => { const cells = this.graph.getSelectedCells() if (cells.length) { this.graph.copy(cells) } return false }) this.graph.bindKey('ctrl+v', () => { if (!this.graph.isClipboardEmpty()) { const cells = this.graph.paste({ offset: 32 }) this.graph.cleanSelection() this.graph.select(cells) } return false }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="keyboard-app"> <div className="app-info"> <p> <span>Ctrl + C</span>Copy Cell <span style={{ marginLeft: 24 }}>Ctrl + V</span>Paste Cell </p> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Configuration | Property Name | Type | Default Value | Required | Description | |---------------|-------------------------------------------|---------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------| | global | boolean | `false` | | Whether to use global keyboard events. When set to `true`, keyboard events are bound to `document`; otherwise they are bound to the canvas container. When bound to the canvas container, it must be focused to trigger keyboard events. | | format | `(this: Graph, key: string) => string` | - | | Format the key string when binding or unbinding keyboard events. | | guard | `(this: Graph, e: KeyboardEvent) => boolean` | - | | Determine whether a keyboard event should be processed. If it returns `false`, the corresponding keyboard event is ignored. | The `format` and `guard` configurations are used as follows: ```ts graph.use( new Keyboard({ enabled: true, format(key) { return key.replace(/\s/g, '').replace('cmd', 'command') }, }), ) // The statement below is equivalent to graph.bindKey('command', (e) => { }) graph.bindKey('cmd', (e) => {}) graph.use( new Keyboard({ enabled: true, guard(this: Graph, e: KeyboardEvent) { if (e.altKey) { // Ignore all keyboard events when the alt key is pressed return false } return true }, }), ) ``` ## API ### graph.bindKey(...) ```ts bindKey( keys: string | string[], callback: (e: KeyboardEvent) => void, action?: 'keypress' | 'keydown' | 'keyup', ): this ``` Bind keyboard shortcuts. ### graph.unbindKey(...) ```ts unbindKey( keys: string | string[], action?: 'keypress' | 'keydown' | 'keyup', ): this ``` Unbind keyboard shortcuts. ### graph.clearKeys() ```ts clearKeys(): this ``` Clear all keyboard shortcuts. ### graph.triggerKey() ```ts triggerKey( keys: string, action?: 'keypress' | 'keydown' | 'keyup', ): this ``` Manually trigger keyboard shortcuts. ### graph.isKeyboardEnabled() ```ts isKeyboardEnabled(): boolean ``` Returns whether keyboard events are enabled. ### graph.enableKeyboard() ```ts enableKeyboard(): this ``` Enable keyboard events. ### graph.disableKeyboard() ```ts disableKeyboard(): this ``` Disable keyboard events. ### graph.toggleKeyboard(...) ```ts toggleKeyboard(enabled?: boolean): this ``` Toggle the enabled state of keyboard events. The parameters are as follows: | Name | Type | Required | Default Value | Description | |----------|---------|:--------:|---------------|------------------------------------------------------| | enabled | boolean | | - | Whether to enable keyboard events; if omitted, toggles the current enabled state. | ### docs/demos/tutorial/plugins/keyboard/index.less ```less .keyboard-app { border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-info { bottom: 0; margin: 0; padding: 0 8px; } .app-info span { display: inline-block; margin-right: 8px; padding: 2px 8px; font-size: 12px; line-height: 18px; background: #e9e9e9; border-radius: 4px; } .app-content { width: 100%; height: 280px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .x6-widget-selection-box { border: 2px dashed #239edd; } .x6-widget-selection-inner { border: 1px solid #239edd; } } ``` #### Interaction - Canonical page: https://x6.antv.antgroup.com/en/tutorial/basic/interacting/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/basic/interacting.md - Description: Interaction --- title: "Interaction" description: "Interaction" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/basic/interacting/" version: "3.x" --- > **This chapter mainly introduces knowledge related to element interaction. By reading, you can learn about** - How to set connection rules - How to embed nodes - How to configure highlight styles - How to disable or enable interactions ## Connection Connection interaction rules are all completed through the `connecting` configuration. For a complete configuration, refer to [API](/en/api/model/interaction#connecting). Below, we introduce some commonly used functions. ### allowXXX You can use the `allowXXX` configuration to define whether a connection can be connected to a corresponding position. The default supports the following items: - `allowBlank`: Whether to allow connection to a blank position on the canvas, default is `true`. - `allowLoop`: Whether to allow creating a loop connection, i.e., the starting node and ending node are the same node, default is `true`. - `allowNode`: Whether to allow connecting to a node (non-port region), default is `true`. - `allowEdge`: Whether to allow connection to another edge, default is `true`. - `allowPort`: Whether to allow connection to a connection point, default is `true`. - `allowMulti`: Whether to allow creating multiple edges between the same starting node and ending node, default is `true`. Each accepts either a boolean or a function: ```ts new Graph({ connecting: { allowNode: true, // boolean }, }) // Function form, often used for dynamic control of connection restrictions new Graph({ connecting: { allowNode(args) { return true }, }, }) ``` > **Tip** `allowMulti` supports being set to the string `withPort`, representing that only one edge can be created between the same connection points of the starting and ending nodes (i.e., multiple edges can be created between the starting and ending nodes, but they must be connected to different connection points). ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.initGraph({ allowBlank: true, allowMulti: true, allowLoop: true, allowNode: true, allowEdge: true, allowPort: true, }) } initGraph = (options: State) => { if (this.graph) { this.graph.dispose() } const graph = new Graph({ container: this.container, width: 800, height: 300, background: { color: '#F2F7FA', }, connecting: { ...options, createEdge() { return this.createEdge({ attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) }, }, }) graph.addNode({ x: 60, y: 50, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { in: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, out: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, items: [ { id: 'port1', group: 'in', }, { id: 'port2', group: 'in', }, { id: 'port3', group: 'in', }, { id: 'port4', group: 'out', }, { id: 'port5', group: 'out', }, ], }, }) graph.addNode({ x: 160, y: 200, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { in: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, out: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, items: [ { id: 'port1', group: 'in', }, { id: 'port2', group: 'in', }, { id: 'port3', group: 'in', }, { id: 'port4', group: 'out', }, { id: 'port5', group: 'out', }, ], }, }) graph.addEdge({ source: [320, 80], target: [500, 200], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) this.graph = graph } onSettingChanged = (options: State) => { this.initGraph(options) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="connecting-app"> <div className="app-side"> <Settings onChange={this.onSettingChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### router/connector In the [edge tutorial](/en/tutorial/basic/edge#router), we know that we can specify `router` and `connector` when adding an edge. If most edges in the entire canvas have the same `router` or `connector`, we can configure them directly in `connecting`, which can avoid repeated configuration in the edge. ```ts new Graph({ connecting: { router: 'orth', connector: 'rounded', }, }) ``` ### createEdge In the above demo, we can drag out a connection from a node or port. You may ask: what elements can start a connection? In X6, any element with `magnet: true` can initiate a connection. Moreover, in `connecting`, we can configure the style of the new connection through the `createEdge` method. ```ts new Graph({ connecting: { createEdge() { return this.createEdge({ shape: 'edge', attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) }, }, }) ``` ### validateXXX We can also define whether to create a connection or whether the connection is valid through the `validateXXX` method. Compared to `allowXXX`, `validateXXX` is more flexible. The default supports the following items: - `validateMagnet`: When clicking on an element with `magnet=true`, judge whether to create a new edge according to the return value of `validateMagnet`. If it returns `false`, there will be no reaction; if it returns `true`, a new edge will be created at the current element. - `validateConnection`: When moving an edge, judge whether the connection is valid according to the return value of `validateConnection`. If it returns `false`, the connection will not be connected to the current element when the mouse is released. - `validateEdge`: When stopping the edge drag, judge whether the edge is valid according to the return value of `validateEdge`. If it returns `false`, the edge will be cleared. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node-with-port', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { top: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, bottom: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, connecting: { allowNode: false, allowBlank: false, highlight: true, createEdge() { return this.createEdge({ attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) }, validateMagnet({ magnet }) { // 节点上方的连接桩无法创建连线 return magnet.getAttribute('port-group') !== 'top' }, validateConnection({ sourceCell, targetCell, sourceMagnet, targetMagnet, }) { // 不能连接自身 if (sourceCell === targetCell) { return false } // 只能从 bottom 连接桩开始连接,连接到 top 连接桩 if ( !sourceMagnet || sourceMagnet.getAttribute('port-group') === 'top' ) { return false } if ( !targetMagnet || targetMagnet.getAttribute('port-group') !== 'top' ) { return false } // 不能重复连线 const edges = this.getEdges() const portId = targetMagnet.getAttribute('port') if (edges.find((edge) => edge.getTargetPortId() === portId)) { return false } return true }, }, highlighting: { // 连接桩可以被连接时在连接桩外围围渲染一个包围框 magnetAvailable: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#A4DEB1', strokeWidth: 4, }, }, }, // 连接桩吸附连线时在连接桩外围围渲染一个包围框 magnetAdsorbed: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#31d0c6', strokeWidth: 4, }, }, }, }, }) graph.addNode({ shape: 'custom-node-with-port', x: 40, y: 40, label: 'hello', ports: { items: [ { id: 'port_1', group: 'top', }, { id: 'port_2', group: 'top', }, { id: 'port_3', group: 'bottom', }, { id: 'port_4', group: 'bottom', }, ], }, }) graph.addNode({ shape: 'custom-node-with-port', x: 160, y: 180, label: 'world', ports: { items: [ { id: 'port_5', group: 'top', }, { id: 'port_6', group: 'top', }, { id: 'port_7', group: 'bottom', }, { id: 'port_8', group: 'bottom', }, ], }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="validate-app "> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Embedding Sometimes we need to drag a node into another node, making it a child node of the other node. At this time, we can enable embedding through the `embedding` option, and specify the parent node through the `findParent` method when the node is moved. For more detailed configuration, refer to [API](/en/api/model/interaction#embedding). ```ts const graph = new Graph({ embedding: { enabled: true, findParent({ node }) { // Get the bounding box of the moved node const bbox = node.getBBox() // Find the node with `parent: true` in the data and intersect with the moved node's bounding box return this.getNodes().filter((node) => { const data = node.getData<{ parent: boolean }>() if (data && data.parent) { const targetBBox = node.getBBox() return bbox.isIntersectWithRect(targetBBox) } return false }) }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, embedding: { enabled: true, findParent({ node }) { const bbox = node.getBBox() return this.getNodes().filter((node) => { const data = node.getData<{ parent: boolean }>() if (data && data.parent) { const targetBBox = node.getBBox() return bbox.isIntersectWithRect(targetBBox) } return false }) }, }, }) graph.addNode({ shape: 'custom-node', x: 40, y: 140, width: 120, height: 60, label: 'Child\n(unembed)', zIndex: 10, }) graph.addNode({ shape: 'custom-node', x: 500, y: 100, width: 120, height: 60, label: 'Child\n(unembed)', zIndex: 10, }) graph.addNode({ shape: 'custom-node', x: 200, y: 80, width: 240, height: 160, zIndex: 1, label: 'Parent', data: { parent: true, }, }) graph.on('node:change:parent', ({ node }) => { node.attr({ label: { text: 'Child\n(embed)', }, }) }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="embedding-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Highlighting We can specify the highlighting style when triggering certain interactions through the `highlighting` option, such as: ```ts new Graph({ highlighting: { // When the connection point can be connected, render a surrounding box around the connection point magnetAvailable: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#A4DEB1', strokeWidth: 4, }, }, }, // When the connection point is adsorbed to the edge, render a surrounding box around the connection point magnetAdsorbed: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#31d0c6', strokeWidth: 4, }, }, }, }, }) ``` Supported `highlighting` configuration items include: - `default` Default highlighting option, used when the following highlighting configurations are missing. - `embedding` Highlighting option used when dragging a node to embed it into another node. - `nodeAvailable` Highlighting option used when a node can be connected during the connection process. - `magnetAvailable` Highlighting option used when a connection point can be connected during the connection process. - `magnetAdsorbed` Highlighting option used when the connection point is automatically adsorbed to the edge during the connection process. The `magnetAvailable.name` above is actually the name of the highlighter, and X6 has built-in `stroke` and `className` highlighters. For more information, refer to [Highlighter](/en/api/registry/highlighter). ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node-with-port', { inherit: 'rect', width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { top: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, bottom: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, }, }, true, ) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, connecting: { allowNode: false, allowBlank: false, highlight: true, createEdge() { return this.createEdge({ attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) }, }, highlighting: { // 连接桩可以被连接时在连接桩外围围渲染一个包围框 magnetAvailable: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#A4DEB1', strokeWidth: 4, }, }, }, // 连接桩吸附连线时在连接桩外围围渲染一个包围框 magnetAdsorbed: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#31d0c6', strokeWidth: 4, }, }, }, }, }) graph.addNode({ shape: 'custom-node-with-port', x: 40, y: 40, label: 'hello', ports: { items: [ { id: 'port_1', group: 'bottom', }, { id: 'port_2', group: 'bottom', }, ], }, }) graph.addNode({ shape: 'custom-node-with-port', x: 160, y: 180, label: 'world', ports: { items: [ { id: 'port_3', group: 'top', }, { id: 'port_4', group: 'top', }, ], }, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="highlighting-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Interaction Limitation We can enable or disable some interaction behaviors of elements through the `interacting` configuration. If the elements on the canvas are purely for preview and cannot be interacted with, we can set it to `false` directly. ```ts new Graph({ interacting: false, }) ``` If we need to define more detailed interaction limitations, we can configure them according to different property values. Supported properties include: - `nodeMovable` Whether nodes can be moved. - `magnetConnectable` Whether to trigger connection interaction when clicking on an element with the `magnet` property. - `edgeMovable` Whether edges can be moved. - `edgeLabelMovable` Whether edge labels can be moved. - `arrowheadMovable` Whether edge arrowheads (after using the arrowhead tool) can be moved. - `vertexMovable` Whether edge vertices can be moved. - `vertexAddable` Whether edge vertices can be added. - `vertexDeletable` Whether edge vertices can be deleted. Their values all support the following two types: ```ts // Directly set to a boolean value new Graph({ interacting: { nodeMovable: false, edgeMovable: true, }, }) // Function form, often used for dynamic control of interaction behaviors new Graph({ interacting: { nodeMovable(view) { const node = view.cell const { enableMove } = node.getData() return enableMove }, }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.initGraph({ nodeMovable: false, magnetConnectable: false, edgeMovable: false, edgeLabelMovable: false, }) } initGraph = (options: State) => { if (this.graph) { this.graph.dispose() } const graph = new Graph({ container: this.container, width: 800, height: 300, background: { color: '#F2F7FA', }, interacting: { ...options, }, connecting: { createEdge() { return this.createEdge({ attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) }, }, }) graph.addNode({ x: 60, y: 50, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { in: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, out: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, items: [ { id: 'port1', group: 'in', }, { id: 'port2', group: 'in', }, { id: 'port3', group: 'in', }, { id: 'port4', group: 'out', }, { id: 'port5', group: 'out', }, ], }, }) graph.addNode({ x: 160, y: 200, width: 100, height: 40, attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, ports: { groups: { in: { position: 'top', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, out: { position: 'bottom', attrs: { circle: { magnet: true, stroke: '#8f8f8f', r: 5, }, }, }, }, items: [ { id: 'port1', group: 'in', }, { id: 'port2', group: 'in', }, { id: 'port3', group: 'in', }, { id: 'port4', group: 'out', }, { id: 'port5', group: 'out', }, ], }, }) graph.addEdge({ source: [320, 80], target: [500, 200], attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, label: 'Move Me', }) this.graph = graph } onSettingChanged = (options: State) => { this.initGraph(options) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="interacting-app"> <div className="app-side"> <Settings onChange={this.onSettingChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/tutorial/basic/interacting/connecting/index.less ```less .connecting-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; height: 100%; padding: 0 8px; } .app-content { flex: 1; margin-top: 0; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/interacting/connecting/settings.tsx ```tsx import React from 'react' import { Checkbox, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { allowBlank: boolean allowMulti: boolean allowLoop: boolean allowNode: boolean allowEdge: boolean allowPort: boolean } export class Settings extends React.Component<Props, State> { state: State = { allowBlank: true, allowMulti: true, allowLoop: true, allowNode: true, allowEdge: true, allowPort: true, } notifyChange() { this.props.onChange({ ...this.state, }) } onAllowTypeChanged = (type: string, flag: boolean) => { const s = { [type]: flag } as any this.setState(s, () => { this.notifyChange() }) } render() { return ( <Card title="Connecting Settings" size="small" bordered={false} style={{ width: 240 }} > <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.allowBlank} onChange={(e) => this.onAllowTypeChanged('allowBlank', e.target.checked) } > AllowBlank </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.allowMulti} onChange={(e) => this.onAllowTypeChanged('allowMulti', e.target.checked) } > AllowMulti </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.allowLoop} onChange={(e) => this.onAllowTypeChanged('allowLoop', e.target.checked) } > AllowLoop </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.allowNode} onChange={(e) => this.onAllowTypeChanged('allowNode', e.target.checked) } > AllowNode </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.allowEdge} onChange={(e) => this.onAllowTypeChanged('allowEdge', e.target.checked) } > AllowEdge </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.allowPort} onChange={(e) => this.onAllowTypeChanged('allowPort', e.target.checked) } > AllowPort </Checkbox> </Col> </Row> </Card> ) } } ``` ### docs/demos/tutorial/basic/interacting/validate/index.less ```less .validate-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` ### docs/demos/tutorial/basic/interacting/embedding/index.less ```less .embedding-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 320px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` ### docs/demos/tutorial/basic/interacting/highlighting/index.less ```less .highlighting-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 280px; margin-right: 8px; margin-left: 8px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } } ``` ### docs/demos/tutorial/basic/interacting/interacting/index.less ```less .interacting-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; height: 100%; padding: 0 8px; } .app-content { flex: 1; margin-top: 0; box-shadow: 0 0 10px 1px #e9e9e9; } } .ant-card { min-height: 250px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } ``` ### docs/demos/tutorial/basic/interacting/interacting/settings.tsx ```tsx import React from 'react' import { Checkbox, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { nodeMovable: boolean magnetConnectable: boolean edgeMovable: boolean edgeLabelMovable: boolean } export class Settings extends React.Component<Props, State> { state: State = { nodeMovable: false, magnetConnectable: false, edgeMovable: false, edgeLabelMovable: false, } notifyChange() { this.props.onChange({ ...this.state, }) } onAllowTypeChanged = (type: string, flag: boolean) => { const s = { [type]: flag } as any this.setState(s, () => { this.notifyChange() }) } render() { return ( <Card title="Connecting Settings" size="small" bordered={false} style={{ width: 240 }} > <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.nodeMovable} onChange={(e) => this.onAllowTypeChanged('nodeMovable', e.target.checked) } > nodeMovable </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.magnetConnectable} onChange={(e) => this.onAllowTypeChanged('magnetConnectable', e.target.checked) } > magnetConnectable </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.edgeMovable} onChange={(e) => this.onAllowTypeChanged('edgeMovable', e.target.checked) } > edgeMovable </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.edgeLabelMovable} onChange={(e) => this.onAllowTypeChanged('edgeLabelMovable', e.target.checked) } > edgeLabelMovable </Checkbox> </Col> </Row> </Card> ) } } ``` #### React Nodes - Canonical page: https://x6.antv.antgroup.com/en/tutorial/intermediate/react/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/intermediate/react.md - Description: React Nodes --- title: "React Nodes" description: "React Nodes" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/intermediate/react/" version: "3.x" --- > **This chapter mainly introduces knowledge related to React nodes. By reading, you can learn** - How to use React components to render node content - How to update node content ## Rendering Nodes We provide a standalone package `@antv/x6-react-shape` for rendering nodes with React. > **Note** Version compatibility: X6 3.x must use x6-react-shape 3.x. Since 2.0.8, x6-react-shape supports React 18+ only; if your project uses React < 18, lock x6-react-shape to 2.0.8 and use X6 2.x. ```ts import { register } from '@antv/x6-react-shape' const NodeComponent = () => { return ( <div className="react-node"> <Progress type="circle" percent={30} width={80} /> </div> ) } register({ shape: 'custom-basic-react-node', width: 100, height: 100, component: NodeComponent, }) graph.addNode({ shape: 'custom-basic-react-node', x: 60, y: 100, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { register } from '@antv/x6-react-shape' import { Progress } from 'antd' const NodeComponent = () => { return ( <div className="react-node"> <Progress type="circle" percent={30} width={80} /> </div> ) } register({ shape: 'custom-basic-react-node', width: 100, height: 100, component: NodeComponent, }) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addNode({ shape: 'custom-basic-react-node', x: 60, y: 100, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="react-basic-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Updating Nodes Similar to `HTML`, register a node with an `effect` field—an array of the node’s `props`. When any listed prop changes, the React component re-renders. ```ts register({ shape: 'custom-update-react-node', width: 100, height: 100, effect: ['data'], component: NodeComponent, }) const node = graph.addNode({ shape: 'custom-update-react-node', x: 60, y: 100, data: { progress: 30, }, }) setInterval(() => { const { progress } = node.getData<{ progress: number }>() node.setData({ progress: (progress + 10) % 100, }) }, 1000) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Node } from '@antv/x6' import { register } from '@antv/x6-react-shape' import { Progress } from 'antd' const NodeComponent = ({ node }: { node: Node }) => { const { progress } = node.getData() return ( <div className="react-node"> <Progress type="circle" percent={progress} width={80} /> </div> ) } register({ shape: 'custom-update-react-node', width: 100, height: 100, effect: ['data'], component: NodeComponent, }) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const node = graph.addNode({ shape: 'custom-update-react-node', x: 60, y: 100, data: { progress: 30, }, }) setInterval(() => { const { progress } = node.getData<{ progress: number }>() node.setData({ progress: (progress + 10) % 100, }) }, 1000) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="react-update-app "> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Portal Mode The approach above renders the component directly into the node’s DOM: ```ts import { createRoot, Root } from 'react-dom/client' const root = createRoot(container) // container is the node container root.render(component) ``` This detaches the component from the normal React tree, so it cannot access external `Context`. Use `Portal` mode when you need access to app-level context. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import { register, getProvider } from '@antv/x6-react-shape' import { Progress, Button } from 'antd' const X6ReactPortalProvider = getProvider() // 注意,一个 graph 只能申明一个 portal provider const ProgressContext = React.createContext(30) const NodeComponent = () => { const progress = React.useContext(ProgressContext) return ( <div className="react-node"> <Progress type="circle" percent={progress} width={80} /> </div> ) } register({ shape: 'custom-portal-react-node', width: 100, height: 100, component: NodeComponent, }) export default class Example extends React.Component { private container: HTMLDivElement state = { progress: 30, } componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addNode({ shape: 'custom-portal-react-node', x: 60, y: 100, }) graph.centerContent() } changeProgress = () => { this.setState({ progress: (this.state.progress + 10) % 100, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="react-portal-app"> <ProgressContext.Provider value={this.state.progress}> <X6ReactPortalProvider /> </ProgressContext.Provider> <div className="app-btns"> <Button onClick={this.changeProgress}>Add</Button> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/tutorial/intermediate/react/basic/index.less ```less .react-basic-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .react-node { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; border: 1px solid #8f8f8f; border-radius: 6px; } } ``` ### docs/demos/tutorial/intermediate/react/update/index.less ```less .react-update-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .react-node { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; border: 1px solid #8f8f8f; border-radius: 6px; } } ``` ### docs/demos/tutorial/intermediate/react/portal/index.less ```less .react-portal-app { border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-btns { bottom: 0; margin: 0; padding: 0 8px; } .app-content { width: 100%; height: 240px; margin: 0; margin-top: 16px; border-radius: 5px; box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); } .react-node { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; border: 1px solid #8f8f8f; border-radius: 6px; } } ``` #### History - Canonical page: https://x6.antv.antgroup.com/en/tutorial/plugins/history/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/plugins/history.md - Description: History --- title: "History" description: "History" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/plugins/history/" version: "3.x" --- > **This chapter mainly introduces knowledge related to undo and redo plugin. By reading, you can learn about** - How to implement undo and redo for element operations - How to configure whether to record additions, removals, and property changes - How to merge multiple changes into a single history entry ## Usage You can enable undo/redo with the `History` plugin, for example: ```ts import { Graph, History } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use( new History({ enabled: true, }), ) ``` ## Demo - After moving a node freely, the Undo button becomes available. - Clicking the Undo button restores the node's position, and then the Redo button becomes available. - Clicking the Redo button updates the node's position. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Button } from 'antd' import { Graph, History } from '@antv/x6' interface Props {} interface State { canUndo: boolean canRedo: boolean } export default class App extends React.Component<Props, State> { private container: HTMLDivElement private graph: Graph state: State = { canRedo: false, canUndo: false, } componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.use( new History({ enabled: true, }), ) graph.addNode({ x: 100, y: 60, width: 100, height: 40, label: 'Drag Me', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.on('history:change', () => { this.setState({ canRedo: graph.canRedo(), canUndo: graph.canUndo(), }) }) this.graph = graph } onUndo = () => { this.graph.undo() } onRedo = () => { this.graph.redo() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="history-app"> <div className="app-btns"> <Button.Group> <Button onClick={this.onUndo} disabled={!this.state.canUndo}> Undo </Button> <Button onClick={this.onRedo} disabled={!this.state.canRedo}> Redo </Button> </Button.Group> </div> <div ref={this.refContainer} className="app-content" /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<App />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Configuration | Property Name | Type | Default Value | Required | Description | |--------------------|---------------------------------|---------------|----------|------------------------------------------------------------------------------------------------------| | stackSize | number | `0` | | A `stackSize` of 0 means no limit; otherwise only that many entries are kept. | | ignoreAdd | boolean | `false` | | If `ignoreAdd` is `true`, adding elements will not be recorded in the history. | | ignoreRemove | boolean | `false` | | If `ignoreRemove` is `true`, removing elements will not be recorded in the history. | | ignoreChange | boolean | `false` | | If `ignoreChange` is `true`, changes to element properties will not be recorded in the history. | | beforeAddCommand | `(event, args) => any` | - | | Called before a command is added to the Undo queue; if this method returns `false`, the command will not be added to the Undo queue. | | afterAddCommand | `(event, args, cmd) => any` | - | | Called after a command is added to the Undo queue. | | executeCommand | `(cmd, revert, options) => any` | - | | Called when a command is undone or redone; `revert` is `true` if the command is undone, otherwise it indicates the command is redone. | > **Tip** In actual projects, we often need to undo or redo multiple changes at once. X6 provides the concept of `batch`, which allows multiple changes to be merged into a single history entry. Here’s how to use it: ```ts // Method 1 graph.startBatch('custom-batch-name') // Changing the border color of the node and modifying its position will be merged into a single record, allowing for a single undo. node.attr('body/stroke', 'red') node.position(30, 30) graph.stopBatch('custom-batch-name') // Method 2 graph.batchUpdate(() => { node.prop('zIndex', 10) node.attr('label/text', 'hello') node.attr('label/fill', '#ff0000') }) ``` ## API ### graph.undo(...) ```ts undo(options?: KeyValue): this ``` Undo. `options` will be passed to the event callback. ### graph.undoAndCancel(...) ```ts undoAndCancel(options?: KeyValue): this ``` Undo and do not add to the redo queue; the command cannot be redone. `options` will be passed to the event callback. ### graph.redo(...) ```ts redo(options?: KeyValue): this ``` Redo. `options` will be passed to the event callback. ### graph.canUndo() ```ts canUndo(): boolean ``` Returns whether undo is available. ### graph.canRedo() ```ts canRedo(): boolean ``` Returns whether redo is available. ### graph.cleanHistory(...) ```ts cleanHistory(options?: KeyValue): this ``` Clear the history queue. `options` will be passed to the event callback. ### graph.getHistoryStackSize(...) ```ts getHistoryStackSize(): number ``` Get the size of the history stack. ### graph.getUndoRemainSize(...) ```ts getUndoRemainSize(): number ``` Get the remaining size of the history undo stack. ### graph.getUndoStackSize(...) ```ts getUndoStackSize(): number ``` Get the size of the history undo stack. ### graph.getRedoStackSize(...) ```ts getRedoStackSize(): number ``` Get the size of the history redo stack. ### graph.isHistoryEnabled() ```ts isHistoryEnabled(): boolean ``` Returns whether history is enabled. ### graph.enableHistory() ```ts enableHistory(): this ``` Enable history state. ### graph.disableHistory() ```ts disableHistory(): this ``` Disable history state. ### graph.toggleHistory(...) ```ts toggleHistory(enabled?: boolean): this ``` Toggle the enabled state of history. Parameters are as follows: | Name | Type | Required | Default Value | Description | |---------|---------|:--------:|---------------|-----------------------------------------------| | enabled | boolean | | - | Whether to enable history state; defaults to toggling the enabled state of history. | ## Events | Event Name | Parameter Type | Description | |-------------------|----------------------------------------------------|------------------------------| | `history:undo` | `{ cmds: Command[], options: KeyValue }` | Triggered when a command is undone. | | `history:redo` | `{ cmds: Command[], options: KeyValue }` | Triggered when a command is redone. | | `history:cancel` | `{ cmds: Command[], options: KeyValue }` | Triggered when a command is canceled. | | `history:add` | `{ cmds: Command[], options: KeyValue }` | Triggered when a command is added to the queue. | | `history:clean` | `{ cmds: Command[] \| null, options: KeyValue }` | Triggered when the history queue is cleared. | | `history:change` | `{ cmds: Command[] \| null, options: KeyValue }` | Triggered when the history queue changes. | | `history:batch` | `{ cmds: Command, options: KeyValue }` | Triggered when a batch command is received. | ```ts graph.on('history:undo', ({ cmds }) => { console.log(cmds) }) // We can also listen to events on the plugin instance history.on('undo', ({ cmds }) => { console.log(cmds) }) ``` ### docs/demos/tutorial/plugins/history/index.less ```less .history-app { border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-btns { bottom: 0; margin: 0; padding: 0 8px; } .app-content { width: 100%; height: 240px; margin: 0; margin-top: 16px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### Events - Canonical page: https://x6.antv.antgroup.com/en/tutorial/basic/events/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/basic/events.md - Description: Events --- title: "Events" description: "Events" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/basic/events/" version: "3.x" --- > **This chapter mainly introduces knowledge related to events. By reading, you can learn about** - Which event categories you can listen to - How to listen to events ## View Interaction Events Events triggered when interacting with the application through mouse, keyboard, or various interactive components. ### Mouse Events | Event | Cell | Node | Port | Edge | Graph | |------------|---------------------|-------------------|---------------------------|-------------------|--------------------| | Click | `cell:click` | `node:click` | `node:port:click` | `edge:click` | `blank:click` | | Double Click | `cell:dblclick` | `node:dblclick` | `node:port:dblclick` | `edge:dblclick` | `blank:dblclick` | | Right Click | `cell:contextmenu` | `node:contextmenu` | `node:port:contextmenu` | `edge:contextmenu` | `blank:contextmenu` | | Mouse Down | `cell:mousedown` | `node:mousedown` | `node:port:mousedown` | `edge:mousedown` | `blank:mousedown` | | Mouse Move | `cell:mousemove` | `node:mousemove` | `node:port:mousemove` | `edge:mousemove` | `blank:mousemove` | | Mouse Up | `cell:mouseup` | `node:mouseup` | `node:port:mouseup` | `edge:mouseup` | `blank:mouseup` | | Mouse Wheel| `cell:mousewheel` | `node:mousewheel` | - | `edge:mousewheel` | `blank:mousewheel` | | Mouse Enter| `cell:mouseenter` | `node:mouseenter` | `node:port:mouseenter` | `edge:mouseenter` | `graph:mouseenter` | | Mouse Leave| `cell:mouseleave` | `node:mouseleave` | `node:port:mouseleave` | `edge:mouseleave` | `graph:mouseleave` | > **Note** It is important to note that the `mousemove` event here differs from the usual mouse move event; it requires the mouse to be moved after being pressed down to trigger. Except for `mouseenter` and `mouseleave`, the parameters of the event callback functions include the mouse position relative to the canvas `x`, `y`, and the mouse event object `e`, among other parameters. ```ts 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 }) => {}) ``` ### Custom Click Events We can add custom attributes `event` or `data-event` to the DOM elements of nodes/edges to listen for click events on that element, for example: ```ts node.attr({ // Represents a delete button, which deletes the node when clicked image: { event: 'node:delete', xlinkHref: 'trash.png', width: 20, height: 20, }, }) ``` You can listen for the bound event name `node:delete` or the generic `cell:customevent`, `node:customevent`, `edge:customevent` event names. ```ts 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() } }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' 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> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### Canvas Zoom/Pan | Event Name | Callback Parameters | Description | |-------------|----------------------------------------------------------|---------------------------------------------------------------| | `scale` | `{ sx: number; sy: number; ox: number; oy: number }` | Triggered when zooming the canvas; `sx` and `sy` are the scale factors, `ox` and `oy` are the zoom center. | | `resize` | `{ width: number; height: number }` | Triggered when changing the canvas size; `width` and `height` are the canvas dimensions. | | `translate` | `{ tx: number; ty: number }` | Triggered when panning the canvas; `tx` and `ty` are the offsets on the X and Y axes. | ```ts graph.on('scale', ({ sx, sy, ox, oy }) => {}) graph.on('resize', ({ width, height }) => {}) graph.on('translate', ({ tx, ty }) => {}) ``` ### Node/Edge Movement | Event Name | Callback Parameters | Description | |---------------|-------------------------------------------------------------------------------------|----------------------------| | `node:move` | `{ e: Dom.MouseDownEvent; x: number; y: number; node: Node; view: NodeView }` | Triggered when starting to move a node. | | `node:moving` | `{ e: Dom.MouseMoveEvent; x: number; y: number; node: Node; view: NodeView }` | Triggered while moving a node. | | `node:moved` | `{ e: Dom.MouseUpEvent; x: number; y: number; node: Node; view: NodeView }` | Triggered after moving a node. | | `edge:move` | `{ e: Dom.MouseDownEvent; x: number; y: number; node: Node; view: NodeView }` | Triggered when starting to move an edge. | | `edge:moving` | `{ e: Dom.MouseMoveEvent; x: number; y: number; node: Node; view: NodeView }` | Triggered while moving an edge. | | `edge:moved` | `{ e: Dom.MouseUpEvent; x: number; y: number; node: Node; view: NodeView }` | Triggered after moving an edge. | The `x` and `y` parameters are the coordinates of the mouse relative to the canvas. ```ts graph.on('node:moved', ({ e, x, y, node, view }) => {}) ``` ### Node Embedding | Event Name | Callback Parameters | Description | |------------------|---------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------| | `node:embed` | `{ e: Dom.MouseDownEvent; x: number; y: number; node: Node; view: NodeView, currentParent: Node }` | Triggered when starting to embed a node. | | `node:embedding` | `{ e: Dom.MouseMoveEvent; x: number; y: number; node: Node; view: NodeView, currentParent: Node, candidateParent: Node }` | Triggered while searching for the target node. | | `node:embedded` | `{ e: Dom.MouseUpEvent; x: number; y: number; node: Node; view: NodeView, previousParent: Node, currentParent: Node }` | Triggered after completing node embedding. | ### Edge Connection/Disconnection The `edge:connected` event fires when dragging an edge terminal to connect/disconnect it to/from a node or edge. The callback parameters are as follows. ```ts interface Args { e: Dom.MouseUpEvent // Mouse event object edge: Edge // Edge view: EdgeView // Edge view isNew: boolean // Whether it is a newly created edge type: Edge.TerminalType // Whether the operation is on the source or target arrow ('source' | 'target') previousCell?: Cell | null // The node/edge connected before the interaction previousView?: CellView | null // The view of the node/edge connected before the interaction previousPort?: string | null // The port ID connected before the interaction previousPoint?: Point.PointLike | null // The point connected before the interaction (records the position of the start terminal when dragging the edge terminal from blank to node/edge) previousMagnet?: Element | null // The element connected before the interaction currentCell?: Cell | null // The node/edge connected after the interaction currentView?: CellView | null // The view of the node/edge connected after the interaction currentPort?: string | null // The port ID connected after the interaction currentPoint?: Point.PointLike | null // The point connected after the interaction (records the position of the terminal after dragging from node/edge to blank) currentMagnet?: Element | null // The element connected after the interaction } ``` We can use `isNew` to determine whether the corresponding edge is newly created after the connection is completed. For example, if an edge is created starting from a port and connected to another node/port, `isNew` will be `true`. ```ts graph.on('edge:connected', ({ isNew, edge }) => { if (isNew) { // Perform database insertion or other persistence operations for the newly created edge } }) ``` It is particularly important to note that the `previous...` parameters record the state of the terminal before the connection/disconnection operation, and do not refer to `sourceCell`. When obtaining `sourceCell` after creating a new edge, do not use `previousCell`; the correct usage is: ```ts graph.on('edge:connected', ({ isNew, edge }) => { if (isNew) { const source = edge.getSourceCell() } }) ``` ## Node/Edge ### Add/Delete/Modify When a node/edge is added to the canvas, the following events are triggered: - `added` - `cell:added` - `node:added` (only triggered when the cell is a node) - `edge:added` (only triggered when the cell is an edge) When a node/edge is removed, the following events are triggered: - `removed` - `cell:removed` - `node:removed` (only triggered when the cell is a node) - `edge:removed` (only triggered when the cell is an edge) When a node/edge undergoes any changes, the following events are triggered: - `changed` - `cell:changed` - `node:changed` (only triggered when the cell is a node) - `edge:changed` (only triggered when the cell is an edge) You can listen on the node/edge: ```ts cell.on('added', ({ cell, index, options }) => {}) cell.on('removed', ({ cell, index, options }) => {}) cell.on('changed', ({ cell, options }) => {}) ``` Or listen on the Graph: ```ts 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 When calling `setXxx(val, options)` and `removeXxx(options)` methods to change the data of a node/edge, and `options.silent` is not `true`, the corresponding `change` event will be triggered, and the node/edge will be redrawn. For example: ```ts cell.setZIndex(2) cell.setZIndex(2, { silent: false }) cell.setZIndex(2, { anyKey: 'anyValue' }) ``` This will trigger the following events on the Cell: - `change:*` - `change:zIndex` And the following events on the Graph: - `cell:change:*` - `node:change:*` (only triggered when the cell is a node) - `edge:change:*` (only triggered when the cell is an edge) - `cell:change:zIndex` - `node:change:zIndex` (only triggered when the cell is a node) - `edge:change:zIndex` (only triggered when the cell is an edge) You can listen on the node/edge: ```ts // Triggered when any change occurs on the cell, can determine the changed item through key cell.on( 'change:*', (args: { cell: Cell key: string // Determine the changed item through key current: any // Current value previous: any // Value before change options: any // Pass-through options }) => { if (key === 'zIndex') { // } }, ) cell.on( 'change:zIndex', (args: { cell: Cell current?: number // Current value previous?: number // Value before change options: any // Pass-through options }) => {}, ) ``` Or listen on the Graph: ```ts graph.on( 'cell:change:zIndex', (args: { cell: Cell current?: number // Current value previous?: number // Value before change options: any // Pass-through options }) => {}, ) // Triggered when the cell is a node graph.on( 'node:change:zIndex', (args: { cell: Cell node: Node current?: number // Current value previous?: number // Value before change options: any // Pass-through options }) => {}, ) // Triggered when the cell is an edge graph.on( 'edge:change:zIndex', (args: { cell: Cell edge: Edge current?: number // Current value previous?: number // Value before change options: any // Pass-through options }) => {}, ) ``` Other `change` events are listed below, and the callback function parameters have the same structure as the parameters mentioned for `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` In addition to the built-in keys mentioned above, we also support listening to custom keys, for example: ```ts cell.on('change:custom', ({ cell, current, previous, options }) => { console.log(current) }) ``` When modifying the value of the `custom` property using the `cell.prop('custom', 'any data')` method, the `change:custom` event will be triggered. ### Animation The animation system supports the following events: - `cell:animation:finish` Triggered when the animation finish. - `cell:animation:cancel` Triggered when the animation cancel. - `node:animation:finish` Triggered when the animation finish. (only triggered when the cell is a node) - `node:animation:cancel` Triggered when the animation cancel. (only triggered when the cell is a node) - `edge:animation:finish` Triggered when the animation finish. (only triggered when the cell is a edge) - `edge:animation:cancel` Triggered when the animation cancel. (only triggered when the cell is a edge) ## View Since X6 implements an asynchronous rendering scheduling algorithm, adding a node does not necessarily mean it is mounted on the canvas. Separate events are triggered when a node is mounted to or unmounted from the canvas. | Event Name | Callback Parameters | Description | |------------------|-----------------------------|----------------------------------| | `view:mounted` | `{ view: CellView }` | Triggered when a node is mounted to the canvas. | | `view:unmounted` | `{ view: CellView }` | Triggered when a node is unmounted from the canvas. | ```ts graph.on('view:mounted', ({ view }) => {}) graph.on('view:unmounted', ({ view }) => {}) ``` You may also often need to listen for the completion of rendering events after calling `fromJSON` or `resetCells`. In this case, you can use the `render:done` event to listen (added in version 2.15.1). ```typescript graph.on('render:done', () => { // pass }) graph.fromJSON([...]) ``` ### docs/demos/tutorial/basic/event/custom-click/index.less ```less .custom-click-app { display: flex; width: 100%; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 200px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### Vue Nodes - Canonical page: https://x6.antv.antgroup.com/en/tutorial/intermediate/vue/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/intermediate/vue.md - Description: Vue Nodes --- title: "Vue Nodes" description: "Vue Nodes" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/intermediate/vue/" version: "3.x" --- > **This chapter mainly introduces knowledge related to Vue nodes. By reading, you can learn** - How to use Vue components to render node content - How to update node content ## Rendering Nodes We provide a standalone package `@antv/x6-vue-shape` for rendering nodes with Vue components. > **Note** Version compatibility: X6 3.x must use x6-vue-shape 3.x. ```html <template> <div class="app-content"> <div id="container"></div> <TeleportContainer /> </div> </template> <script lang="ts"> import { defineComponent } from 'vue' import ProgressNode from './components/ProgressNode.vue' import { Graph } from '@antv/x6' import { register, getTeleport } from '@antv/x6-vue-shape' register({ shape: 'custom-vue-node', width: 100, height: 100, component: ProgressNode, }) const TeleportContainer = getTeleport() export default defineComponent({ name: 'App', components: { TeleportContainer, }, mounted() { const graph = new Graph({ container: document.getElementById('container')!, background: { color: '#F2F7FA', }, autoResize: true, }) graph.addNode({ shape: 'custom-vue-node', x: 100, y: 60, }) }, }) </script> ``` The content of the node component is as follows: ```html <template> <el-progress type="dashboard" :percentage="percentage" :width="80"> <template #default="{ percentage }"> <span class="percentage-value">{{ percentage }}%</span> </template> </el-progress> </template> <script lang="ts"> import { defineComponent } from 'vue' export default defineComponent({ name: 'ProgressNode', inject: ['getNode'], data() { return { percentage: 80, } }, mounted() { const node = (this as any).getNode() console.log(node) }, }) </script> ``` The result is as follows: <iframe src="/demos/vue/basic/index.html" style="width: 100%; height: 260px; border: 0px; overflow: hidden;"></iframe> ## Updating Nodes To update the content of a `Vue` node, there are two methods: - Listen for node events within the component and trigger events externally. - Use state management tools like `Vuex` (not elaborated here, but used in the same way as regular `Vue` components with `Vuex` data). Below is an introduction to dynamically updating node content using events: ```ts const node = graph.addNode({ shape: 'custom-vue-node', x: 100, y: 60, data: { progress: 80, }, }) setInterval(() => { const { progress } = node.getData() node.setData({ progress: (progress + 10) % 100, }) }, 2000) ``` Inside the node component, we can listen for changes to the node's `data`: ```ts export default defineComponent({ name: 'ProgressNode', inject: ['getNode'], data() { return { percentage: 80, } }, mounted() { const node = (this as any).getNode() as Node node.on('change:data', ({ current }) => { const { progress } = current this.percentage = progress }) }, }) ``` <iframe src="/demos/vue/update/index.html" style="width: 100%; height: 260px; border: 0px; overflow: hidden;"></iframe> ## Using in Vue 2 In the above example, we used `Teleport`, a feature in `Vue 3`. How can we use it in `Vue 2`? ```html <template> <div id="app"></div> </template> <script lang="ts"> import Vue from 'vue' import CustomNode from './components/CustomNode.vue' import { Graph } from '@antv/x6' import { register } from '@antv/x6-vue-shape' register({ shape: 'custom-vue-node', width: 100, height: 100, component: CustomNode, }) export default Vue.extend({ name: 'App', mounted() { const graph = new Graph({ container: document.getElementById('app'), width: 1000, height: 1000, }) graph.addNode({ shape: 'custom-vue-node', x: 100, y: 100, }) }, }) </script> ``` The node component is written in the same way as above. > **Note** In Vue 2, node components have some limitations, such as the inability to use Vuex, i18n, and Element UI. #### Selection - Canonical page: https://x6.antv.antgroup.com/en/tutorial/plugins/selection/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/plugins/selection.md - Description: Selection --- title: "Selection" description: "Selection" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/plugins/selection/" version: "3.x" --- > **This chapter mainly introduces knowledge related to selection and rubberband plugin. By reading, you can learn about** - How to enable selection and rubberband - How to configure multi-selection and strict rubberband - How to set modifier keys and trigger events (`eventTypes`) ## Usage You can enable selection and rubberband with the `Selection` plugin, for example: ```ts import { Graph, Selection } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use( new Selection({ enabled: true, }), ) ``` ## Demo - Click to select nodes. - Enable multi-selection by holding down Ctrl/Command and clicking on nodes. - Enable moving by dragging the selection box to move nodes. - Enable rubberband selection by pressing the left mouse button on a blank area of the canvas and dragging the selection box to select nodes. - Enable strict rubberband mode and observe its effect on selection. - Use modifier keys in conjunction with selection, such as the `alt` key. Hold down the `alt` key and press the left mouse button on a blank area of the canvas to drag the selection box to select nodes. - Apply a custom filter (exclude circle nodes), so that circular nodes cannot be selected. - Apply custom additional content (display the number of selected nodes). Select two or more nodes to trigger the display of custom content. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Selection } from '@antv/x6' import { Settings, State } from './settings.tsx' export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) this.graph.use( new Selection({ enabled: true, multiple: true, rubberband: true, movable: true, showNodeSelectionBox: true, }), ) this.graph.addNode({ x: 320, y: 100, width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const source = this.graph.addNode({ x: 80, y: 50, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 240, y: 200, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } onSettingChanged = (options: State) => { this.graph.toggleMultipleSelection(options.multiple) this.graph.toggleSelectionMovable(options.movable) this.graph.toggleRubberband(options.rubberband) this.graph.toggleStrictRubberband(options.strict) this.graph.setSelectionFilter(options.filter) this.graph.setRubberbandModifiers(options.modifiers as any) this.graph.setSelectionDisplayContent( options.content ? (selection) => { return `${selection.length} node${ selection.length > 1 ? 's' : '' } selected.` } : null, ) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="selection-app"> <div className="app-side"> <Settings onChange={this.onSettingChanged} /> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Configuration | Property Name | Type | Default Value | Required | Description | |-------------------------|----------------------|---------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | enabled | boolean | `true` | | Whether to enable the Selection plugin | | className | string | - | | Additional style name for customizing styles | | multiple | boolean | `true` | | Whether to enable multi-selection; when enabled, hold down the `ctrl` or `command` key to click nodes for multi-selection | | multipleSelectionModifiers | ModifierKey | `['ctrl', 'meta']` | | Used to set the modifier keys for multi-selection | | rubberband | boolean | `false` | | Whether to enable the rubberband selection feature | | rubberNode | boolean | `true` | | Whether nodes are included in rubberband hit testing | | rubberEdge | boolean | `false` | | Whether edges are included in rubberband hit testing | | modifiers | ModifierKey | - | | Used to set the modifier keys for rubberband selection | | strict | boolean | `false` | | Whether the selection box needs to completely enclose nodes to select them | | movable | boolean | `true` | | Whether the selected nodes move together when dragging the selection box | | content | string | - | | Set additional display content | | filter | Filter | - | | Node filter | | showNodeSelectionBox | boolean | `false` | | Whether to show the selection box for nodes | | showEdgeSelectionBox | boolean | `false` | | Whether to show the selection box for edges | | pointerEvents | `'none' \| 'auto'` | `auto` | | When `showNodeSelectionBox` is enabled, an element overlays the node and its events may not respond; set `pointerEvents: none` to resolve this. Default is `auto`. | | eventTypes | SelectionEventType[] | `['leftMouseDown', 'mouseWheelDown']` | | Used to set the trigger event types for rubberband selection | | movingRouterFallback | string | - | | During batch dragging of multiple nodes, temporarily downgrade the routing of edges connected to the selected nodes to the specified router, and automatically restore to the original routers after idle (recommended to set `'orth'` when dragging many nodes and edges originally use `'manhattan'` to improve dragging smoothness) | The type definition for `Filter` is as follows: ```ts type Filter = string[] | { id: string }[] | (this: Graph, cell: Cell) => boolean ``` - `string[]`: An array of node shapes; only specified node/edge shapes can be selected. - `({ id: string })[]`: An array of node IDs; only specified nodes/edges can be selected. - `(this: Graph, cell: Cell) => boolean`: Only nodes/edges that return true can be selected. The type definition for `ModifierKey` is as follows: ```ts type ModifierKey = string | ('alt' | 'ctrl' | 'meta' | 'shift' | 'space')[] | null ``` The modifier keys in X6 include `alt`, `ctrl`, `meta`, `shift`, and `space`. When modifiers are configured, clicking the mouse while holding a modifier triggers the corresponding behavior. If rubberband selection and canvas panning have identical triggers (same `eventTypes` and same `modifiers`), rubberband selection takes precedence and the default panning is disabled; if they differ, they work independently. When both start on `leftMouseDown` over blank areas, configure different modifiers for selection and panning to avoid conflicts. You can configure a single key (e.g., `alt`) or multiple keys (e.g., `['alt', 'ctrl']`). Multiple keys in an array are treated as OR. For more flexible configurations, use the following forms: - `alt` indicates pressing `alt`. - `[alt, ctrl]` indicates pressing `alt` or `ctrl`. - `alt|ctrl` indicates pressing `alt` or `ctrl`. - `alt&ctrl` indicates pressing `alt` and `ctrl` simultaneously. - `alt|ctrl&shift` indicates pressing `alt` and `shift` simultaneously or pressing `ctrl` and `shift` simultaneously. The type definition for `SelectionEventType` is as follows: ```ts type SelectionEventType = 'leftMouseDown' | 'mouseWheelDown'; ``` Interaction methods that trigger rubberband selection. Supports two forms or combinations of them: - `leftMouseDown`: Pressing the left mouse button to drag. - `mouseWheelDown`: Pressing the mouse wheel to drag. ## API ### graph.select(...) ```ts select(cells: Cell | string | (Cell | string)[]): this ``` Selects the specified nodes/edges. Note that this method does not unselect currently selected nodes/edges; it appends the specified nodes/edges to the selection. If you also need to unselect currently selected nodes/edges, please use the [resetSelection(...)](#graphresetselection) method. ### graph.unselect(...) ```ts unselect(cells: Cell | string | (Cell | string)[]): this ``` Unselects the specified nodes/edges. ### graph.isSelected(...) ```ts isSelected(cell: Cell | string): boolean ``` Returns whether the specified node/edge is selected. ### graph.resetSelection(...) ```ts resetSelection(cells?: Cell | string | (Cell | string)[]): this ``` Clears the selection first, then selects the provided nodes/edges. ### graph.getSelectedCells() ```ts getSelectedCells(): Cell[] ``` Gets the selected nodes/edges. ### graph.cleanSelection() ```ts cleanSelection(): this ``` Clears the selection. ### graph.isSelectionEmpty() ```ts isSelectionEmpty(): boolean ``` Returns whether the selection is empty. ### graph.isSelectionEnabled() ```ts isSelectionEnabled(): boolean ``` Returns whether selection is enabled. ### graph.enableSelection() ```ts enableSelection(): this ``` Enables selection capability. ### graph.disableSelection() ```ts disableSelection(): this ``` Disables selection capability. ### graph.toggleSelection(...) ```ts toggleSelection(enabled?: boolean): this ``` Toggles the enabled state of selection. Parameters are as follows: | Name | Type | Required | Default Value | Description | |---------|---------|:--------:|---------------|-----------------------------------------------| | enabled | boolean | | - | Whether to enable selection capability; defaults to toggling the enabled state. | ### graph.isMultipleSelection() ```ts isMultipleSelection(): boolean ``` Returns whether multi-selection is enabled. ### graph.enableMultipleSelection() ```ts enableMultipleSelection(): this ``` Enables multi-selection. ### graph.disableMultipleSelection() ```ts disableMultipleSelection(): this ``` Disables multi-selection. ### graph.toggleMultipleSelection(...) ```ts toggleMultipleSelection(multiple?: boolean): this ``` Toggles the enabled state of multi-selection. Parameters are as follows: | Name | Type | Required | Default Value | Description | |----------|---------|:--------:|---------------|-----------------------------------------------| | multiple | boolean | | - | Whether to enable multi-selection; defaults to toggling the enabled state. | ### graph.isSelectionMovable() ```ts isSelectionMovable(): boolean ``` Returns whether the selected nodes/edges can be moved. ### graph.enableSelectionMovable() ```ts enableSelectionMovable(): this ``` Enables moving of selected nodes/edges. ### graph.disableSelectionMovable() ```ts disableSelectionMovable(): this ``` Disables moving of selected nodes/edges. ### graph.toggleSelectionMovable(...) ```ts toggleSelectionMovable(enabled?: boolean): this ``` Toggles whether selected nodes/edges can be moved. Parameters are as follows: | Name | Type | Required | Default Value | Description | |---------|---------|:--------:|---------------|-------------------------------------------------------| | enabled | boolean | | - | Whether to enable moving of selected nodes/edges; defaults to toggling the enabled state. | ### graph.isRubberbandEnabled() ```ts isRubberbandEnabled(): boolean ``` Returns whether rubberband selection is enabled. ### graph.enableRubberband() ```ts enableRubberband(): this ``` Enables rubberband selection. ### graph.disableRubberband() ```ts disableRubberband(): this ``` Disables rubberband selection. ### graph.toggleRubberband(...) ```ts toggleRubberband(enabled?: boolean): this ``` Toggles the enabled state of rubberband selection. Parameters are as follows: | Name | Type | Required | Default Value | Description | |---------|---------|:--------:|---------------|----------------------------------------| | enabled | boolean | | - | Whether to enable rubberband selection; defaults to toggling the enabled state. | ### graph.isStrictRubberband() ```ts isStrictRubberband(): boolean ``` Returns whether strict rubberband selection is enabled. When strict rubberband selection is enabled, only nodes/edges that are completely enclosed by the selection box will be selected. ### graph.enableStrictRubberband() ```ts enableStrictRubberband(): this ``` Enables strict rubberband selection. When strict rubberband selection is enabled, only nodes/edges that are completely enclosed by the selection box will be selected. ### graph.disableStrictRubberband() ```ts disableStrictRubberband(): this ``` Disables strict rubberband selection. When strict rubberband selection is disabled, nodes/edges can be selected as long as the selection box intersects with their bounding box. ### graph.toggleStrictRubberband(...) ```ts toggleStrictRubberband(enabled?: boolean): this ``` Toggles the enabled state of strict rubberband selection. Parameters are as follows: | Name | Type | Required | Default Value | Description | |---------|---------|:--------:|---------------|---------------------------------------------| | enabled | boolean | | - | Whether to enable strict rubberband selection; defaults to toggling the enabled state. | ### graph.setSelectionFilter(...) ```ts setSelectionFilter( filter?: | null | (string | { id: string })[] | ((this: Graph, cell: Cell) => boolean) ): this ``` Sets the filtering criteria for selection; only nodes/edges that meet the filtering criteria can be selected. When `filter` is enabled, the `nodes` and `edges` returned by `box:mousemove` and `box:mouseup` during blank-area rubberband selection also apply the same filtering rule, so the preview result stays consistent with the final selection. ### graph.setRubberbandModifiers(...) ```ts setRubberbandModifiers(modifiers?: string | ModifierKey[] | null): this ``` Sets the modifier keys for rubberband selection; rubberband selection can only be triggered when the modifier keys are pressed simultaneously. ### graph.setSelectionDisplayContent(...) ```ts setSelectionDisplayContent( content?: | null | false | string | ((this: Graph, selection: Selection, contentElement: HTMLElement) => string) ): this ``` Sets additional display content for selected nodes/edges. ## Events | Event Name | Parameter Type | Description | |-----------------------|---------------------------------------------------------------------------------------|------------------------------------------| | `cell:selected` | `{ cell: Cell; options: Model.SetOptions }` | Triggered when a node/edge is selected | | `node:selected` | `{ node: Node; options: Model.SetOptions }` | Triggered when a node is selected | | `edge:selected` | `{ edge: Edge; options: Model.SetOptions }` | Triggered when an edge is selected | | `cell:unselected` | `{ cell: Cell; options: Model.SetOptions }` | Triggered when a node/edge is unselected | | `node:unselected` | `{ node: Node; options: Model.SetOptions }` | Triggered when a node is unselected | | `edge:unselected` | `{ edge: Edge; options: Model.SetOptions }` | Triggered when an edge is unselected | | `box:mousedown` | `{ e: Dom.MouseDownEvent; view: CellView \| null; cell: Cell \| null; x: number; y: number; nodes: Node[]; edges: Edge[] }` | Triggered when pressing a selection box, or when starting rubberband selection on a blank area | | `box:mousemove` | `{ e: Dom.MouseMoveEvent; view: CellView \| null; cell: Cell \| null; x: number; y: number; nodes: Node[]; edges: Edge[] }` | Triggered while dragging a selection box, or continuously during rubberband selection | | `box:mouseup` | `{ e: Dom.MouseUpEvent; view: CellView \| null; cell: Cell \| null; x: number; y: number; nodes: Node[]; edges: Edge[] }` | Triggered when finishing selection-box dragging, or when finishing rubberband selection | | `selection:changed` | `{added: Cell[]; removed: Cell[]; selected: Cell[]; options: Model.SetOptions}` | Triggered when the selected nodes/edges change (add/remove) | The parameters of `box:*` events are: - `x` and `y`: the current graph coordinates of the pointer. - `nodes` and `edges`: the current node/edge collection represented by the box. - `view` and `cell`: the related view and cell; they can be `null` during blank-area rubberband interactions. `nodes` and `edges` mean slightly different things depending on the interaction mode: - During blank-area rubberband selection, `nodes` and `edges` are the nodes and edges currently hit by the rubberband box in real time. The `strict` and `filter` options affect this result. - During selection-box dragging, `nodes` and `edges` are the nodes and edges currently contained in the selection. ```ts graph.on('node:selected', ({ node }) => { console.log(node) }) graph.on('box:mousemove', ({ nodes, edges, cell }) => { console.log(nodes, edges, cell) }) // We can also listen to events on the plugin instance selection.on('node:selected', ({ node }) => { console.log(node) }) ``` ### docs/demos/tutorial/plugins/selection/index.less ```less .selection-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } .x6-widget-selection-box { border: 2px dashed #239edd; } .x6-widget-selection-inner { border: 1px solid #239edd; } } ``` ### docs/demos/tutorial/plugins/selection/settings.tsx ```tsx import React from 'react' import { Checkbox, Switch, Card, Row, Col } from 'antd' import type { CheckboxChangeEvent } from 'antd/es/checkbox' export interface Props { onChange: (res: State) => void } export interface State { multiple: boolean rubberband: boolean strict: boolean movable: boolean modifiers: string[] filter?: string[] content: boolean } export class Settings extends React.Component<Props, State> { state: State = { multiple: true, rubberband: true, movable: true, strict: false, modifiers: [], content: false, } notifyChange() { this.props.onChange({ ...this.state, }) } onMultipleChanged = (multiple: boolean) => { this.setState({ multiple }, () => { this.notifyChange() }) } onMovableChanged = (movable: boolean) => { this.setState({ movable }, () => { this.notifyChange() }) } onRubberbandChanged = (rubberband: boolean) => { this.setState({ rubberband }, () => { this.notifyChange() }) } onStrictChanged = (strict: boolean) => { this.setState({ strict }, () => { this.notifyChange() }) } onModifiersChange = (modifiers: string[]) => { this.setState({ modifiers }, () => { this.notifyChange() }) } onFilterChanged = (e: CheckboxChangeEvent) => { this.setState( { filter: e.target.checked ? ['circle'] : undefined, }, () => { this.notifyChange() }, ) } onContentChanged = (e: CheckboxChangeEvent) => { this.setState({ content: e.target.checked }, () => { this.notifyChange() }) } render() { return ( <Card title="Selection Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={14}>Enable Multiple Select</Col> <Col span={10}> <Switch checked={this.state.multiple} onChange={this.onMultipleChanged} /> </Col> </Row> <Row align="middle"> <Col span={14}>Selection Movable</Col> <Col span={10}> <Switch checked={this.state.movable} onChange={this.onMovableChanged} /> </Col> </Row> <Row align="middle" style={{ margin: '0 -12px', }} > <Col span={24} style={{ borderBottom: '1px solid #e9e9e9' }} /> </Row> <Row align="middle"> <Col span={14}>Enable Rubberband</Col> <Col span={10}> <Switch checked={this.state.rubberband} onChange={this.onRubberbandChanged} /> </Col> </Row> <Row align="middle"> <Col span={14}>Is Strict Contains</Col> <Col span={10}> <Switch checked={this.state.strict} onChange={this.onStrictChanged} /> </Col> </Row> <Row align="middle"> <Col span={8}>Modifier Key</Col> <Col span={16}> <Checkbox.Group options={['alt', 'ctrl', 'shift']} value={this.state.modifiers} onChange={this.onModifiersChange} /> </Col> </Row> <Row align="middle" style={{ margin: '0 -12px', }} > <Col span={24} style={{ borderBottom: '1px solid #e9e9e9' }} /> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.filter != null} onChange={this.onFilterChanged} > Add filter (only circle) </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.content} onChange={this.onContentChanged} > Add content(display selected count) </Checkbox> </Col> </Row> </Card> ) } } ``` #### Upgrade to 3.x - Canonical page: https://x6.antv.antgroup.com/en/tutorial/update/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/update.md - Description: Upgrade to 3.x --- title: "Upgrade to 3.x" description: "Upgrade to 3.x" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/update/" version: "3.x" --- Compared to 2.x, 3.x mainly focuses on plugin consolidation (unified export), optimized default interactions, introduces animation capabilities, and adds virtual rendering support. The upgrade cost is low. ## Overview - Plugin and subpackage consolidation: `@antv/x6-plugin-xxxx`, `@antv/x6-common`, and `@antv/x6-geometry` are merged into the main `@antv/x6` package and exported from there. - Default interaction tweak: canvas `panning` is enabled by default. - New animation capability: supports imperative, declarative, and custom Shape-based animations. - With `Scroller`, virtual rendering is available via `virtual: true` for large graphs. ## Upgrade Guide ### package.json 3.x merges commonly used plugins and subpackages into `@antv/x6`, so you no longer need to install separate `@antv/x6-plugin-xxxx` packages. Additionally, the utility package `@antv/x6-common` and geometry package `@antv/x6-geometry` are now part of the main package, and their APIs should be imported from `@antv/x6`. ```json { "@antv/x6": "^3.0.0" } ``` Remove the following dependencies if present: - `@antv/x6-plugin-selection` - `@antv/x6-plugin-transform` - `@antv/x6-plugin-scroller` - `@antv/x6-plugin-keyboard` - `@antv/x6-plugin-history` - `@antv/x6-plugin-clipboard` - `@antv/x6-plugin-snapline` - `@antv/x6-plugin-dnd` - `@antv/x6-plugin-minimap` - `@antv/x6-plugin-stencil` - `@antv/x6-plugin-export` - `@antv/x6-common` - `@antv/x6-geometry` ### Import Path Changes Starting from 3.x, the plugins above are exported from `@antv/x6`. Usage remains the same; simply replace the import path: ```ts // 2.x import { Scroller } from '@antv/x6-plugin-scroller' import { Selection } from '@antv/x6-plugin-selection' graph.use(new Scroller()) graph.use(new Selection()) // 3.x import { Scroller, Selection } from '@antv/x6' graph.use(new Scroller()) graph.use(new Selection()) ``` In 3.x, utilities and geometry types previously imported from `@antv/x6-common` and `@antv/x6-geometry` are now exported directly from `@antv/x6`. You only need to update the import path: ```ts // 2.x import { Dom, FunctionExt } from '@antv/x6-common' import { Point, Rectangle } from '@antv/x6-geometry' // 3.x import { Dom, FunctionExt, Point, Rectangle } from '@antv/x6' ``` ### New Animation Capability 3.x introduces `animate` and removes the 2.x `transition` API, making it easier to add effects to nodes and edges. See more: [Animation Docs](/en/tutorial/basic/animation). ### Configuration Changes - Graph panning `panning` is enabled by default (`enabled: true`). To disable, set `panning: false`. - When graph `panning` conflicts with Selection rubberband triggers, Selection takes precedence. - With `Scroller`, graph `panning` is disabled by default to avoid interaction conflicts, and virtual rendering can be enabled via `virtual: true`. ### Shape Packages Upgrade (React/Vue/Angular) - X6 3.x must use the 3.x versions of the shape packages: `@antv/x6-react-shape@^3.x`, `@antv/x6-vue-shape@^3.x`, and `@antv/x6-angular-shape@^3.x`. ```json { "@antv/x6-react-shape": "^3.0.0", "@antv/x6-vue-shape": "^3.0.0", "@antv/x6-angular-shape": "^3.0.0" } ``` #### React Provider API Change Change 2.x `Portal.getProvider()` to 3.x `getProvider()`: ```ts // 2.x import { Portal } from '@antv/x6-react-shape' const Provider = Portal.getProvider() // 3.x import { getProvider } from '@antv/x6-react-shape' const Provider = getProvider() ``` #### Data Serialization - Canonical page: https://x6.antv.antgroup.com/en/tutorial/basic/serialization/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/basic/serialization.md - Description: Data Serialization --- title: "Data Serialization" description: "Data Serialization" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/basic/serialization/" version: "3.x" --- > **This chapter mainly introduces knowledge related to canvas data serialization. By reading, you can learn about** - How to export data - How to import data ## Export Call `graph.toJSON()` to export nodes and edges. It returns an object like `{ cells: [] }`, where the `cells` array stores nodes and edges **in render order**. Exported node structure: ```ts { id: string, shape: string, position: { x: number, y: number }, size: { width: number, height: number }, attrs: object, zIndex: number, } ``` Exported edge structure: ```ts { id: string, shape: string, source: object, target: object, attrs: object, zIndex: number, } ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph } from '@antv/x6' import hljs from 'highlight.js/lib/core' import json from 'highlight.js/lib/languages/json' hljs.registerLanguage('json', json) export default class Example extends React.Component { private container: HTMLDivElement private code: HTMLElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const source = graph.addNode({ x: 40, y: 40, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode({ x: 240, y: 180, width: 100, height: 40, shape: 'ellipse', label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, label: 'X6', attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) const parse = () => { this.code.innerText = JSON.stringify(graph.toJSON(), null, 2) hljs.highlightBlock(this.code) } parse() graph.on('cell:change:*', parse) } refContainer = (container: HTMLDivElement) => { this.container = container } refCode = (code: HTMLElement) => { this.code = code } render() { return ( <div className="to-json-app"> <div className="app-side"> <pre> <code className="language-json" ref={this.refCode} /> </pre> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Import Supports an array of node/edge metadata `graph.fromJSON(cells: (Node.Metadata | Edge.Metadata)[])`. ```ts 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', }, ]) ``` Alternatively, provide an object containing `cells`, `nodes`, and `edges`, rendered in the order of `[...cells, ...nodes, ...edges]`. ```ts graph.fromJSON({ nodes: [], edges: [], }) ``` Typically, we render the data exported by `graph.toJSON()` using `graph.fromJSON(...)`. > **Tip** When the data does not provide a `zIndex`, the rendering is done according to the order of nodes/edges in the array, meaning that nodes/edges that appear earlier have a smaller `zIndex`, resulting in a lower layer in the canvas. ### docs/demos/tutorial/basic/serialization/to-json/index.less ```less .to-json-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { width: 320px; height: 280px; overflow: auto; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } } ``` #### Developer Tools - Canonical page: https://x6.antv.antgroup.com/en/tutorial/devtool/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/devtool.md - Description: Developer Tools --- title: "Developer Tools" description: "Developer Tools" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/devtool/" version: "3.x" --- We provide a plugin for inspecting page elements to help developers more easily develop applications. ### Installation ![image](https://user-images.githubusercontent.com/1826685/238003455-d341f598-1b35-4d8c-bb7c-0320cad6a4cb.png) ### Usage First, initialize in your project code: ```javascript // init window hook window.__x6_instances__ = [] const graph = new Graph({ ...blablabla }) window.__x6_instances__.push(graph) ``` Then you can see the AntV X6 section in the developer panel. ![image](https://user-images.githubusercontent.com/1826685/238013980-2d6018f8-7d85-473c-a043-98b1f03b6674.png) Here, we can inspect the graph object and the elements within the graph: ![image](https://user-images.githubusercontent.com/1826685/238014156-e65ec2b0-f719-410e-9a10-89cdb836acde.png) It also supports modifying element properties: ![image](https://user-images.githubusercontent.com/1826685/238014353-124feb8e-2049-499d-a13d-3d26f485bab6.png) #### Angular Nodes - Canonical page: https://x6.antv.antgroup.com/en/tutorial/intermediate/angular/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/intermediate/angular.md - Description: Angular Nodes --- title: "Angular Nodes" description: "Angular Nodes" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/intermediate/angular/" version: "3.x" --- > **This chapter mainly introduces knowledge related to Angular nodes. By reading, you can learn** - How to use Angular to render node content - How to update node content - FAQ ## Rendering Nodes We provide a standalone package `@antv/x6-angular-shape` for rendering Angular components/templates as nodes. > **Note** Version compatibility: X6 3.x must use x6-angular-shape 3.x. ### Component Rendering ```ts @Component({ selector: 'app-node', templateUrl: './node.component.html', styleUrls: ['./node.component.scss'], }) export class NodeComponent implements AfterViewInit, OnChanges { @Input() value: string; } ``` ```ts import { register } from '@antv/x6-angular-shape'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) export class AppComponent implements AfterViewInit { ngAfterViewInit(): void { register({ shape: 'custom-angular-component-node', width: 120, height: 20, content: NodeComponent, injector: this.injector, }); this.graph.addNode({ shape: 'custom-angular-component-node', x: 100, y: 100, data: { // Input parameters must be placed here ngArguments: { value: 'Oh my god, what a mess', }, }, }); } } ``` ### TemplateRef Rendering ```html <ng-template #template let-data="ngArguments"> <section class="template-container"> <span class="value">{{ data.value }}</span> </section> </ng-template> ``` ```ts import { register } from '@antv/x6-angular-shape'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) export class AppComponent implements AfterViewInit { @ViewChild('template') template: TemplateRef<{}>; ngAfterViewInit(): void { register({ shape: 'custom-angular-template-node', width: 120, height: 20, content: this.template, injector: this.injector, }); this.graph.addNode({ shape: 'custom-angular-template-node', x: 100, y: 100, data: { ngArguments: { value: 'Why is the magic failing?', }, }, }); } } ``` ## Updating Nodes Whether using Component or TemplateRef, the update method is the same. ```ts node.setData({ ngArguments: { value: 'A few frames from the past in the evening breeze', }, }); ``` ## Is there a demo? Yes, because the rendering of nodes in X6 is decoupled from the framework, the `x6-angular-shape` package is not directly modified in the source code but developed in a separate Angular environment. The demo also provides performance tests for various node types. For more details, please refer to [Eve-Sama/x6-angular-shape](https://github.com/Eve-Sama/x6-angular-shape) and [Performance comparison between X6 and G6, as well as discussions on FPS thresholds for multiple node types in X6](https://github.com/antvis/X6/issues/3266). ## FAQ ### Why can't input properties be placed directly in data and must be placed in ngArguments? And why is it not called ngInput? Not all properties in `node.data` are input properties, so it is inappropriate to iterate over all properties in `data` for assignment. The reason it is called `ngArguments` is mainly due to two considerations: - 1. The 1.x version has already used this, and maintaining this API can reduce the upgrade cost for users. - The concept of `Input` actually comes from `Component`, while in `TemplateRef` it is `context`. Abstracting a concept of `Arguments` based on the two is more general. ### Are there any new features in version 2.x of x6-angular-shape compared to version 1.x? The implementation approach is quite similar to before, but there are indeed a few points worth mentioning. #### More Focused Demo The demo in version 1.x included a series of cases such as rendering components, drawing connections, clearing, etc. While it seemed like an extension, it was actually overwhelming. The demo for version 2.x of `x6-angular-shape` is more focused, concentrating on the usage and performance testing of shapes. For unrelated content, please refer to the X6 official website. #### More Stable Functionality In version 1.x, although functionality was implemented, the consideration of usage scenarios was not comprehensive. For example, changes to `ngArguments` did not affect the `TemplateRef` scenario. While it worked for `Component`, it could not trigger `ngOnChanges`. In the new version, these issues will no longer exist. ### Version Requirements Your Angular version must be at least 14 or above. Below version 14, some features need to be implemented using hacks, which can be cumbersome. This is not currently provided, but if needed, you can raise an issue, and I will explain how to implement it. #### Scroller - Canonical page: https://x6.antv.antgroup.com/en/tutorial/plugins/scroller/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/plugins/scroller.md - Description: Scroller --- title: "Scroller" description: "Scroller" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/plugins/scroller/" version: "3.x" --- > **In this chapter, we mainly introduce knowledge related to the scroller plugin. By reading, you can learn about** - How to enable scrolling capabilities for the canvas > **Note** The `Scroller` plugin disables the graph's built-in `panning` by default to avoid interaction conflicts. ## Usage You can enable canvas scrolling with the `Scroller` plugin. For example: ```ts import { Graph, Scroller } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', } }) graph.use( new Scroller({ enabled: true, }), ) ``` ## Demo ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Button } from 'antd' import { Graph, Scroller } from '@antv/x6' import { Settings, State } from './settings.tsx' const data = { hello: { id: 'hello', x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, world: { id: 'world', shape: 'circle', x: 160, y: 180, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }, rect: { id: 'rect', x: -60, y: 100, width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }, } export default class Example extends React.Component { private container: HTMLDivElement private graph: Graph componentDidMount() { this.renderGraph() } renderGraph(options: State | null = null) { if (this.graph) { this.graph.dispose() } const graph = new Graph({ container: this.container, panning: false, background: { color: '#F2F7FA', }, }) graph.use( new Scroller({ enabled: true, pageVisible: true, pageBreak: true, pannable: true, ...options, }), ) graph.fromJSON({ nodes: Object.keys(data).map((key) => data[key]) }) graph.on('node:change:position', ({ node, current }) => { const item = data[node.id] item.x = current!.x item.y = current!.y }) graph.centerContent() this.graph = graph } onSettingsChanged = (options: State) => { this.renderGraph(options) } onCenter = () => { this.graph.center() } onCenterContent = () => { this.graph.centerContent() } onCenterCircle = () => { const circle = this.graph.getCellById('world') if (circle) { this.graph.centerCell(circle) } } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="scroller-app"> <div className="app-side"> <Settings onChange={this.onSettingsChanged} /> </div> <div className="app-main"> <div className="app-btns"> <Button onClick={this.onCenter}>Center Graph</Button> <Button onClick={this.onCenterContent}>Center Whole Content</Button> <Button onClick={this.onCenterCircle}>Center The Circle</Button> </div> <div className="app-content" ref={this.refContainer} /> </div> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Options | Property Name | Type | Default | Required | Description | |-------------------|---------------------|----------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | pannable | boolean | `false` | | Whether to enable canvas panning capability (dragging the canvas after pressing the mouse on a blank area) | | className | string | - | | Additional class name for custom styling | | width | number | - | | Width of the `Scroller`, defaults to the width of the canvas container | | height | number | - | | Height of the `Scroller`, defaults to the height of the canvas container | | modifiers | ModifierKey | - | | Set modifier keys that need to be pressed along with the mouse click to trigger canvas dragging | | pageWidth | number | - | | Width of each page, defaults to the width of the canvas container | | pageHeight | number | - | | Height of each page, defaults to the height of the canvas container | | pageVisible | boolean | `false` | | Whether to enable pagination | | pageBreak | boolean | `false` | | Whether to show page breaks | | autoResize | boolean | `true` | | Whether to automatically expand/shrink the canvas. When enabled, moving nodes/edges will automatically calculate the required canvas size, expanding it according to `pageWidth` and `pageHeight` when exceeding the current size, and shrinking it otherwise. | | minVisibleWidth | number | - | | Effective when `padding` is empty, sets the minimum visible width of the canvas during scrolling | | minVisibleHeight | number | - | | Effective when `padding` is empty, sets the minimum visible height of the canvas during scrolling | | padding | `number \| Padding` | - | | Sets the padding around the canvas. Defaults to being automatically calculated based on `minVisibleWidth` and `minVisibleHeight`, ensuring that at least `minVisibleWidth` and `minVisibleHeight` of the canvas is visible during scrolling. | The `Padding` type is defined as follows: ```ts type Padding = { top: number; right: number; bottom: number; left: number } ``` The `ModifierKey` type is defined as follows: ```ts type ModifierKey = string | ('alt' | 'ctrl' | 'meta' | 'shift' | 'space')[] | null ``` Supports the following forms: - `alt` means pressing `alt`. - `[alt, ctrl]` means pressing either `alt` or `ctrl`. - `alt|ctrl` means pressing either `alt` or `ctrl`. - `alt&ctrl` means pressing both `alt` and `ctrl` simultaneously. - `alt|ctrl&shift` means pressing both `alt` and `shift` simultaneously or both `ctrl` and `shift` simultaneously. ## API ### graph.lockScroller() Disables scrolling. ### graph.unlockScroller() Enables scrolling. ### graph.getScrollbarPosition() Gets the scrollbar position. ### graph.setScrollbarPosition(left?: number, top?: number) Sets the scrollbar position. - `left?: number` The position of the horizontal scrollbar; defaults to no horizontal scrolling. - `top?: number` The position of the vertical scrollbar; defaults to no vertical scrolling. For example: ```ts graph.setScrollbarPosition(100) graph.setScrollbarPosition(100, null) graph.setScrollbarPosition(null, 200) graph.setScrollbarPosition(100, 200) ``` ### docs/demos/tutorial/plugins/scroller/index.less ```less .scroller-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-side { bottom: 0; padding: 0 8px; .slider-value { display: inline-block; margin-left: 8px; padding: 3px 7px; color: #333; font-size: 12px; line-height: 1.25; background: #eee; border-radius: 10px; } } .app-main { display: flex; flex-direction: column; width: 100%; margin-right: 8px; margin-left: 8px; .app-btns { display: flex; padding-top: 4px; padding-bottom: 12px; } .app-btns .ant-btn { margin-right: 16px; } .app-content { flex: 1; margin-right: 8px; margin-left: 8px; } } .x6-graph-scroller { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card { box-shadow: 0 0 10px 1px #e9e9e9; } .ant-card-head-title { text-align: center; } .ant-row { margin: 16px 0; text-align: left; } } ``` ### docs/demos/tutorial/plugins/scroller/settings.tsx ```tsx import React from 'react' import { Checkbox, Slider, Card, Row, Col } from 'antd' export interface Props { onChange: (res: State) => void } export interface State { pannable: boolean pageVisible: boolean pageBreak: boolean autoResize: boolean minVisibleWidth: number minVisibleHeight: number } export class Settings extends React.Component<Props, State> { state: State = { pannable: true, pageVisible: true, pageBreak: true, autoResize: true, minVisibleWidth: 50, minVisibleHeight: 50, } notifyChange() { this.props.onChange({ ...this.state, }) } onPanningChanged = (e: any) => { this.setState({ pannable: e.target.checked }, () => { this.notifyChange() }) } onAutoResizeChanged = (e: any) => { this.setState({ autoResize: e.target.checked }, () => { this.notifyChange() }) } onPageVisibleChanged = (e: any) => { this.setState({ pageVisible: e.target.checked }, () => { this.notifyChange() }) } onPageBreakChanged = (e: any) => { this.setState({ pageBreak: e.target.checked }, () => { this.notifyChange() }) } onMinVisibleWidthChanged = (minVisibleWidth: number) => { this.setState({ minVisibleWidth }, () => { this.notifyChange() }) } onMinVisibleHeightChanged = (minVisibleHeight: number) => { this.setState({ minVisibleHeight }, () => { this.notifyChange() }) } render() { return ( <Card title="Scroller Settings" size="small" bordered={false} style={{ width: 320 }} > <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.pannable} onChange={this.onPanningChanged} > Enable Panning </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.autoResize} onChange={this.onAutoResizeChanged} > Enable Auto Resize </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.pageVisible} onChange={this.onPageVisibleChanged} > Show Page </Checkbox> </Col> </Row> <Row align="middle"> <Col span={24}> <Checkbox checked={this.state.pageBreak} onChange={this.onPageBreakChanged} > Show Page Break </Checkbox> </Col> </Row> <Row align="middle" style={{ borderTop: '1px solid #f0f0f0', paddingTop: 16, marginBottom: 0, }} > <Col span={10} offset={1}> Min Visible Width </Col> <Col span={2} offset={1}> <div className="slider-value">{this.state.minVisibleWidth}</div> </Col> </Row> <Row align="middle" style={{ marginTop: 0 }}> <Col span={22} offset={1}> <Slider min={10} max={200} step={1} value={this.state.minVisibleWidth} onChange={this.onMinVisibleWidthChanged} /> </Col> </Row> <Row align="middle" style={{ borderTop: '1px solid #f0f0f0', paddingTop: 16, marginBottom: 0, }} > <Col span={10} offset={1}> Min Visible Height </Col> <Col span={2} offset={1}> <div className="slider-value">{this.state.minVisibleHeight}</div> </Col> </Row> <Row align="middle" style={{ marginTop: 0 }}> <Col span={22} offset={1}> <Slider min={10} max={200} step={1} value={this.state.minVisibleHeight} onChange={this.onMinVisibleHeightChanged} /> </Col> </Row> </Card> ) } } ``` #### Animation - Canonical page: https://x6.antv.antgroup.com/en/tutorial/basic/animation/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/basic/animation.md - Description: Animation --- title: "Animation" description: "Animation" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/basic/animation/" version: "3.x" --- ## Adding Animations In X6, adding animations to nodes or edges can significantly enhance the visual effects and interactive experience of the canvas. It supports multiple animation implementations including the `animate` API, CSS animations, and SMIL animations. Among these, the `animate` API is the most powerful, and this article will focus on its usage. Here's a basic animation example: ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) const node = graph.addNode({ shape: 'rect', label: 'hello X6', x: 100, y: 140, width: 100, height: 50, attrs: { body: { strokeWidth: 1, rx: 6, ry: 6, }, }, }) node.animate( // Move the node's position from its original location to (300, 140) via animation { 'position/x': 300 }, // Animation duration is 1000ms, when reaching the end point it will alternate back, with infinite iterations { duration: 1000, direction: 'alternate', iterations: Infinity }, ) ``` For developers familiar with the Web Animations API, it's clear that X6's `animate` API is based on the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) standard. Therefore, its usage is largely compatible with the Web Animations API and implements most of its core features. The `animate` API accepts two key parameters: 1. **Keyframe configuration**: Specifies the properties to animate and their target values. In this example, configured as an object where: - **key** (position/x): The property path to animate - **value** (300): The target value, moving the node from its current position to x-coordinate 300 2. **Animation configuration**: Parameters controlling animation behavior, including: - `duration`: 1000 means each animation cycle lasts 1000ms - `direction`: 'alternate' means the animation will alternate between start and end points - `iterations`: Infinity means the animation will repeat indefinitely ### Animation Properties Some developers might wonder why we use `position/x` instead of directly using the `x` property. This is because X6's animation system is implemented based on the `cell.setPropByPath()` method, which modifies property values via property paths. A node's property structure typically looks like this (can also be obtained via `cell.prop()` method): ```js { // ... position: {x: 370, y: 180}, size: {width: 100, height: 60}, attrs: { // ... } } ``` Therefore, animating the position is done through `position/x`, with child properties concatenated using `/`. Other animation properties follow the same pattern. For example, to animate node size: ```js // Set node width to 200 with animation node.animate({'size/width': 200 },1000) ``` Understanding this property mechanism means you can animate not just built-in node properties but also custom properties. For example, animating a custom `ratio` property in the node's `data`: ```ts import { Dom, Graph, Shape } from '@antv/x6' Shape.HTML.register({ shape: 'custom-html', width: 160, height: 80, effect: ['data'], html(cell) { // Custom HTML node receives an area ratio to control div size, which can be animated const { ratio } = cell.getData() ?? {} const div = document.createElement('div') const area = 12000 const width = Math.sqrt(area / ratio) const height = width * ratio Dom.css(div, { width, height, background: '#fff', borderRadius: 10, border: "1px solid #000" }) return div }, }) const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) graph.addNode({ shape: 'custom-html', x: 80, y: 80, data: { ratio: 1 }, animation: [ // Animate the custom node's aspect ratio [{ 'data/ratio': 3 / 5 }, { duration: 1000, iterations: Infinity }], ], }) ``` ### Declarative Animation Besides using the API to add animations, the `animate` API also supports adding animations through node's `animation` configuration, eliminating an extra step which can be practical in many scenarios. The following example achieves the same animation as above: ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) graph.addNode({ shape: 'rect', label: 'hello X6', x: 100, y: 140, width: 100, height: 50, attrs: { body: { strokeWidth: 1, rx: 6, ry: 6, }, }, // Write animation configuration directly in node config, animation starts automatically when node is added to canvas animation: [ [ { 'position/x': 300 }, { duration: 1000, direction: 'alternate', iterations: Infinity }, ], ], }) ``` The `animation` configuration is an array: - Each item is an animation parameter array (corresponding to `animate` parameters) - After the node is added to the canvas, animations will automatically play according to the configuration ### Registering Animated Shapes A common scenario is adding the same animation to multiple nodes. Whether through repeated `animate` API calls or configuration, each node requires repetitive operations. X6 inherently supports reusing nodes by registering `Shape`, and animations can also be registered into `Shape` to enable animations for a group of nodes: ```ts import { Graph } from '@antv/x6' Graph.registerNode( 'animated-rect', { inherit: 'rect', width: 150, height: 60, attrs: { body: { strokeWidth: 1, rx: 6, ry: 6, }, }, // Pre-configure animation when registering Shape animation: [ [ { 'position/x': 300 }, { duration: 1000, direction: 'alternate', iterations: Infinity }, ], ], }, true, ) const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) graph.addNode({ shape: 'animated-rect', x: 100, y: 50, label: "hello X6" }) graph.addNode({ shape: 'animated-rect', x: 100, y: 150, label: "hello Animation" }) ``` ## Animation Control X6's `animate` API is designed based on the Web Animations API, so it similarly supports flexible animation control capabilities like pause, resume, cancel, etc. Here's a simple animation control example: ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) const node = graph.addNode({ shape: 'rect', label: 'Click me to pause', x: 100, y: 140, width: 150, height: 60, attrs: { body: { strokeWidth: 1, rx: 6, ry: 6, }, }, animation: [ [ { 'position/x': 300 }, { duration: 1000, direction: 'alternate', iterations: Infinity }, ], ], }) graph.on('node:click', ({ cell }) => { if (cell === node) { const [animation] = cell.getAnimations() animation.pause() } }) ``` Besides pausing animations, the `animate` API supports the following capabilities: - `pause()` Pause animation - `play()` Play/resume animation - `cancel()` Cancel animation - `finish()` Finish animation - `reverse()` Play animation in reverse - `updatePlaybackRate()` Update animation playback speed ## Animation Events X6 supports two styles of animation events. One is based on Web Animations API design, such as setting `onfinish` to listen for animation events: ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) const node = graph.addNode({ shape: 'rect', label: 'Click me to start', x: 100, y: 140, width: 150, height: 60, attrs: { body: { strokeWidth: 1, rx: 6, ry: 6, }, }, }) graph.on('node:click', ({ cell }) => { if (cell !== node) return const animation = node.animate( { 'position/x': [100, 300] }, { duration: 1000, direction: 'alternate', iterations: 1 }, ) // Listen to animation events via onfinish animation.onfinish = () => { console.log('Animation finished') } }) ``` Web Animations API style supports these events: - `onfinish` Animation finished - `oncancel` Animation canceled The other style is based on X6's own design, using the `on` API to listen for animation events: ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), height: 300, background: { color: '#F2F7FA', }, }) const node = graph.addNode({ shape: 'rect', label: 'Click me to start', x: 100, y: 140, width: 150, height: 60, attrs: { body: { strokeWidth: 1, rx: 6, ry: 6, }, }, }) graph.on('node:click', ({ cell }) => { if (cell !== node) return node.animate( { 'position/x': [100, 300] }, { duration: 1000, direction: 'alternate', iterations: 1 }, ) }) // Listen to animation events via on graph.on('node:animation:finish', () => { console.log('Animation finished') }) ``` X6's native event system supports these events: - `cell:animation:finish` Animation finished - `cell:animation:cancel` Animation canceled For detailed X6 event system, see [Events](/en/tutorial/basic/events). #### HTML Nodes - Canonical page: https://x6.antv.antgroup.com/en/tutorial/intermediate/html/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/intermediate/html.md - Description: HTML Nodes --- title: "HTML Nodes" description: "HTML Nodes" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/intermediate/html/" version: "3.x" --- > **This chapter mainly introduces knowledge related to HTML nodes. By reading, you can learn** - How to use HTML to render node content - How to update node content ## Rendering Nodes X6 comes with built-in `HTML` rendering capabilities, and it's very easy to use: ```ts import { Shape } from '@antv/x6' Shape.HTML.register({ shape: 'custom-html', width: 160, height: 80, html() { const div = document.createElement('div') div.className = 'custom-html' return div }, }) graph.addNode({ shape: 'custom-html', x: 60, y: 100, }) ``` In the example below, we add a hover animation effect to the `HTML` element, which would be quite complex to implement using `SVG`. ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Shape } from '@antv/x6' Shape.HTML.register({ shape: 'custom-html', width: 160, height: 80, effect: ['data'], html() { const div = document.createElement('div') div.className = 'custom-html' return div }, }) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.addNode({ shape: 'custom-html', x: 60, y: 100, }) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="html-basic-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Updating Nodes When registering the node, provide an `effect` field—the array of the node’s `props`. When any listed prop changes, the `html` method runs again and returns a new DOM to update the node content. ```ts Shape.HTML.register({ shape: 'custom-html', width: 160, height: 80, effect: ['data'], html(cell) { const { color } = cell.getData() const div = document.createElement('div') div.className = 'custom-html' div.style.background = color return div }, }) ``` ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Shape, Color } from '@antv/x6' Shape.HTML.register({ shape: 'custom-update-html', width: 160, height: 80, effect: ['data'], html(cell) { const { color } = cell.getData() const div = document.createElement('div') div.className = 'custom-html' div.style.background = color return div }, }) export default class Example extends React.Component { private container: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) const node = graph.addNode({ shape: 'custom-update-html', x: 60, y: 100, data: { color: '#333232', }, }) setInterval(() => { node.setData({ color: Color.randomHex(), }) }, 2000) graph.centerContent() } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="html-update-app"> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ### docs/demos/tutorial/intermediate/html/basic/index.less ```less .html-basic-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .custom-html { width: 100%; height: 100%; background: #333232; border-radius: 1em; box-shadow: 0 0.125em 0.3125em rgb(0 0 0 / 25%), 0 0.02125em 0.06125em rgb(0 0 0 / 25%); perspective: 600px; &::before { position: absolute; top: 50%; left: 0; width: 100%; height: 50%; background: #333232 linear-gradient(180deg, rgb(0 0 0 / 10%) 50%, rgb(0 0 0 / 40%)); border-radius: 0 0 1em 1em; transform: rotateX(180deg); transform-origin: center top; transition: 0.7s ease-in-out transform; content: ' '; } &:hover::before { transform: rotateX(0); } } } ``` ### docs/demos/tutorial/intermediate/html/update/index.less ```less .html-update-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .custom-html { width: 100%; height: 100%; background: #333232; border-radius: 1em; box-shadow: 0 0.125em 0.3125em rgb(0 0 0 / 25%), 0 0.02125em 0.06125em rgb(0 0 0 / 25%); perspective: 600px; &::before { position: absolute; top: 50%; left: 0; width: 100%; height: 50%; background: #333232 linear-gradient(180deg, rgb(0 0 0 / 10%) 50%, rgb(0 0 0 / 40%)); border-radius: 0 0 1em 1em; transform: rotateX(180deg); transform-origin: center top; transition: 0.7s ease-in-out transform; content: ' '; } &:hover::before { transform: rotateX(0); } } } ``` #### Dnd - Canonical page: https://x6.antv.antgroup.com/en/tutorial/plugins/dnd/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/plugins/dnd.md - Description: Dnd --- title: "Dnd" description: "Dnd" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/plugins/dnd/" version: "3.x" --- > **This chapter mainly introduces knowledge related to the Dnd plugin. By reading, you can learn about** - How to add nodes via drag-and-drop - How to customize the dragged and dropped nodes ## Usage We often need to add nodes to the canvas through drag-and-drop interactions, such as in process graph editing scenarios, where we drag and drop components from the process graph component library onto the canvas. You can enable drag-and-drop with the `Dnd` plugin. Example: ```ts import { Graph, Dnd } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) const dnd = new Dnd({ target: graph, }) ``` When starting to drag, we need to call the `dnd.start(node, e)` method. In React, we use it like this: ```tsx export default () => { const startDrag = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => { // The node is the node being dragged, which is also the node placed on the canvas by default, and can be customized with any properties const node = graph.createNode({ shape: 'rect', width: 100, height: 40, }) dnd.start(node, e.nativeEvent) } return ( <ul> <li onMouseDown={startDrag}></li> </ul> ) } ``` ## Demo ```ts import './index.less' import { createRoot } from 'react-dom/client' /* eslint-disable jsx-a11y/no-static-element-interactions */ import React from 'react' import { Graph, Dnd, Snapline } from '@antv/x6' export default class Example extends React.Component { private graph: Graph private container: HTMLDivElement private dndContainer: HTMLDivElement private dnd: Dnd componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, mousewheel: { enabled: true, modifiers: ['ctrl', 'meta'], }, }) graph.use( new Snapline({ enabled: true, sharp: true, }), ) const source = graph.addNode({ x: 130, y: 30, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode({ x: 180, y: 160, width: 100, height: 40, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.centerContent() this.dnd = new Dnd({ target: graph, scaled: false, dndContainer: this.dndContainer, }) this.graph = graph } startDrag = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => { const target = e.currentTarget const type = target.getAttribute('data-type') const node = type === 'rect' ? this.graph.createNode({ width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) : this.graph.createNode({ width: 60, height: 60, shape: 'circle', label: 'Circle', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.dnd.start(node, e.nativeEvent as any) } refContainer = (container: HTMLDivElement) => { this.container = container } dndContainerRef = (container: HTMLDivElement) => { this.dndContainer = container } render() { return ( <div className="dnd-app"> <div className="dnd-wrap" ref={this.dndContainerRef}> <div data-type="rect" className="dnd-rect" onMouseDown={this.startDrag} > Rect </div> <div data-type="circle" className="dnd-circle" onMouseDown={this.startDrag} > Circle </div> </div> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Configuration | Option | Type | Required | Default Value | Description | |-------------------|-------------------------------------------------------------------------------------|:----:|-----------------|-----------------------------------------------------------------------------------------------------| | target | Graph | ✓️ | | The target canvas. | | getDragNode | (sourceNode: Node, options: GetDragNodeOptions) => Node | | | Get the node being dragged when dragging starts, default to cloning the node passed to `dnd.start`. | | getDropNode | (draggingNode: Node, options: GetDropNodeOptions) => Node | | | Get the node to be placed on the target canvas when dragging ends, default to cloning the dragged node. | | validateNode | (droppingNode: Node, options: ValidateNodeOptions) => boolean \| Promise\<boolean\> | | | Validate whether the node can be placed on the target canvas when dragging ends. | | dndContainer | HTMLElement | | | If `dndContainer` is set, releasing the mouse on `dndContainer` will not place the node, commonly used in scenarios where the `dnd` container is above the canvas. | | draggingContainer | HTMLElement | | `document.body` | Customize the dragging canvas container. | ## Frequently Asked Questions 1. Why does the node ID change after dragging and dropping it onto the canvas? According to the dragging details above, we can see that the overall dragging process is: source node -> dragging node -> dropped node. By default, we clone the source node to get the dragging node, and clone the dragging node to get the dropped node. During the cloning process, the node ID will be reset. If you want to keep the original node ID, you can do the following: ```ts // This way, the ID of the node placed on the canvas will be the same as the ID of the node passed to `dnd.start`. const dnd = new Dnd({ getDragNode: (node) => node.clone({ keepId: true }), getDropNode: (node) => node.clone({ keepId: true }), }) ``` 2. How to customize the style of the dragged node? ```ts const dnd = new Dnd({ getDragNode(node) { // Return a new node as the dragged node return graph.createNode({ width: 100, height: 100, shape: 'rect', attrs: { body: { fill: '#ccc', }, }, }) }, }) ``` 3. How to customize the style of the node placed on the canvas? ```ts const dnd = new Dnd({ getDropNode(node) { const { width, height } = node.size() // Return a new node as the node placed on the canvas return node.clone().size(width * 3, height * 3) }, }) ``` 4. How to get the position of the node placed on the canvas? ```ts graph.on('node:added', ({ node }) => { const { x, y } = node.position() }) ``` 5. How to set the zIndex of the node placed on the canvas? ```ts graph.on('node:added', ({ node }) => { node.setZIndex(5) }) ``` ### docs/demos/tutorial/plugins/dnd/index.less ```less .dnd-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-content { flex: 1; height: 240px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .dnd-wrap { display: flex; flex-direction: column; align-items: center; width: 200px; padding: 16px; border: 1px solid #f0f0f0; user-select: none; .dnd-rect { width: 100px; height: 40px; margin: 16px; line-height: 40px; text-align: center; border: 1px solid #8f8f8f; border-radius: 6px; cursor: move; } .dnd-circle { width: 60px; height: 60px; margin: 16px; line-height: 60px; text-align: center; border: 1px solid #8f8f8f; border-radius: 100%; cursor: move; } } } ``` #### Mini Map - Canonical page: https://x6.antv.antgroup.com/en/tutorial/plugins/minimap/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/plugins/minimap.md - Description: Mini Map --- title: "Mini Map" description: "Mini Map" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/plugins/minimap/" version: "3.x" --- > **This chapter mainly introduces knowledge related to the Mini Map plugin. By reading, you can learn about** - How to use the Mini Map plugin ## Usage You can enable the mini-map with the `MiniMap` plugin. Example: ```ts import { Graph, MiniMap } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use( new MiniMap({ container: document.getElementById('minimap'), }), ) ``` ## Demo - Move the mini-map viewport to move the canvas. - Zoom the mini-map viewport to zoom the canvas. ```ts import './index.less' import { createRoot } from 'react-dom/client' import * as React from 'react' import { Graph, MiniMap, Scroller } from '@antv/x6' import { Radio } from 'antd' import { SimpleNodeView } from './simple-view.tsx' const options = [ { label: '简单视图', value: 'simple' }, { label: '详细视图', value: 'detailed' }, ] export default class Example extends React.Component { private container: HTMLDivElement private minimapContainer: HTMLDivElement private graph: Graph componentDidMount() { this.graph = new Graph({ container: this.container, width: 600, height: 320, panning: false, background: { color: '#F2F7FA', }, }) this.graph.use( new Scroller({ enabled: true, pageVisible: true, pageBreak: false, pannable: true, }), ) this.graph.use( new MiniMap({ container: this.minimapContainer, width: 200, height: 160, padding: 10, }), ) this.graph.addNode({ x: 200, y: 100, width: 100, height: 40, label: 'Rect', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const source = this.graph.addNode({ x: 32, y: 32, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = this.graph.addNode({ shape: 'circle', x: 160, y: 180, width: 60, height: 60, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', }, }, }) this.graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) } onMinimapViewChange = (val: string) => { this.graph.disposePlugins('minimap') if (val === 'simple') { this.graph.use( new MiniMap({ container: this.minimapContainer, width: 200, height: 160, padding: 10, graphOptions: { createCellView(cell) { // 可以返回三种类型数据 // 1. null: 不渲染 // 2. undefined: 使用 X6 默认渲染方式 // 3. CellView: 自定义渲染 if (cell.isEdge()) { return null } if (cell.isNode()) { return SimpleNodeView } }, }, }), ) } else { this.graph.use( new MiniMap({ container: this.minimapContainer, width: 200, height: 160, padding: 10, }), ) } } refContainer = (container: HTMLDivElement) => { this.container = container } refMiniMapContainer = (container: HTMLDivElement) => { this.minimapContainer = container } render() { return ( <div className="minimap-app"> <div className="app-btns"> <Radio.Group options={options} onChange={(e) => this.onMinimapViewChange(e.target.value)} defaultValue={'detailed'} optionType="button" /> </div> <div className="app-content" ref={this.refContainer} /> <div className="app-minimap" ref={this.refMiniMapContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Options | Property Name | Type | Default Value | Required | Description | |---------------|---------------|---------------|----------|----------------------------------| | container | HTMLElement | - | ✓️ | The container to mount the mini-map | | width | number | `300` | | The width of the mini-map | | height | number | `200` | | The height of the mini-map | | padding | number | `10` | | The padding margin of the mini-map container | | scalable | boolean | `true` | | Whether scaling is enabled | | minScale | number | `0.01` | | The minimum scale ratio | | maxScale | number | `16` | | The maximum scale ratio | | graphOptions | Graph.Options | `{}` | | Options for the internal mini-map Graph | | createGraph | (options: Graph.Options) => Graph | `options => new Graph(options)` | | Custom method to create the internal mini-map Graph | ### docs/demos/tutorial/plugins/minimap/index.less ```less .minimap-app { position: relative; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-btns { margin-bottom: 8px; } .app-minimap { position: absolute; bottom: 0; left: 620px; width: 200px; height: 150px; } .x6-widget-minimap-viewport { border: 2px solid #8f8f8f; } .x6-widget-minimap-viewport-zoom { border: 2px solid #8f8f8f; } } ``` ### docs/demos/tutorial/plugins/minimap/simple-view.tsx ```tsx import { NodeView } from '@antv/x6' export class SimpleNodeView extends NodeView { protected renderMarkup() { return this.renderJSONMarkup({ tagName: 'rect', selector: 'body', }) } update() { super.update({ body: { refWidth: '100%', refHeight: '100%', fill: '#8f8f8f', }, }) } } ``` #### Stencil - Canonical page: https://x6.antv.antgroup.com/en/tutorial/plugins/stencil/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/plugins/stencil.md - Description: Stencil --- title: "Stencil" description: "Stencil" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/plugins/stencil/" version: "3.x" --- > **This chapter mainly introduces knowledge related to the Stencil plugin. By reading, you can learn about** - How to enhance DnD with the Stencil plugin - How to configure grouping, collapse, and search ## Usage Stencil is a further encapsulation based on Dnd, providing a sidebar-like UI component that supports grouping, collapsing, searching, and more. You can enable the stencil sidebar with the `Stencil` plugin. Example: ```ts import { Graph, Stencil } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) const stencil = new Stencil({ target: graph, groups: [ { name: 'group1', }, { name: 'group2', }, ], }) const rect1 = graph.createNode({ shape: 'rect', width: 100, height: 40, }) const rect2 = rect1.clone() // A DOM container to hold the stencil, stencilContainer stencilContainer.appendChild(stencil.container) stencil.load([rect1, rect2], 'group1') ``` ## Demo ```ts import './index.less' import { createRoot } from 'react-dom/client' import React from 'react' import { Graph, Stencil, Snapline } from '@antv/x6' const commonAttrs = { body: { fill: '#fff', stroke: '#8f8f8f', strokeWidth: 1, }, } export default class Example extends React.Component { private container: HTMLDivElement private stencilContainer: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, background: { color: '#F2F7FA', }, }) graph.use( new Snapline({ enabled: true, sharp: true, }), ) const source = graph.addNode({ x: 130, y: 30, width: 100, height: 40, label: 'Hello', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) const target = graph.addNode({ x: 320, y: 240, width: 100, height: 40, label: 'World', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, }) graph.centerContent() const stencil = new Stencil({ title: 'Stencil', target: graph, search(cell, keyword) { return cell.shape.indexOf(keyword) !== -1 }, placeholder: 'Search by shape name', notFoundText: 'Not Found', collapsable: true, stencilGraphHeight: 0, groups: [ { name: 'group1', title: 'Group(Collapsable)', }, { name: 'group2', title: 'Group', collapsable: false, }, ], }) this.stencilContainer.appendChild(stencil.container) const n1 = graph.createNode({ shape: 'rect', x: 40, y: 40, width: 80, height: 40, label: 'rect', attrs: commonAttrs, }) const n2 = graph.createNode({ shape: 'circle', x: 180, y: 40, width: 40, height: 40, label: 'circle', attrs: commonAttrs, }) const n3 = graph.createNode({ shape: 'ellipse', x: 280, y: 40, width: 80, height: 40, label: 'ellipse', attrs: commonAttrs, }) const n4 = graph.createNode({ shape: 'path', x: 420, y: 40, width: 40, height: 40, // https://www.svgrepo.com/svg/13653/like path: 'M24.85,10.126c2.018-4.783,6.628-8.125,11.99-8.125c7.223,0,12.425,6.179,13.079,13.543c0,0,0.353,1.828-0.424,5.119c-1.058,4.482-3.545,8.464-6.898,11.503L24.85,48L7.402,32.165c-3.353-3.038-5.84-7.021-6.898-11.503c-0.777-3.291-0.424-5.119-0.424-5.119C0.734,8.179,5.936,2,13.159,2C18.522,2,22.832,5.343,24.85,10.126z', attrs: commonAttrs, label: 'path', }) stencil.load([n1, n2], 'group1') stencil.load([n3, n4], 'group2') } refContainer = (container: HTMLDivElement) => { this.container = container } refStencil = (container: HTMLDivElement) => { this.stencilContainer = container } render() { return ( <div className="stencil-app"> <div className="app-stencil" ref={this.refStencil} /> <div className="app-content" ref={this.refContainer} /> </div> ) } } const root = createRoot(document.getElementById('container')!) root.render(<Example />) window.addEventListener('pagehide', () => root.unmount(), { once: true }) ``` ## Configuration | Option | Type | Required | Default Value | Description | |---------------------|-------------------------------------------------------------|:--------:|----------------------|----------------------------------------------| | title | string | | `'Stencil'` | Title. | | groups | Group[] | ✓️ | - | Group information. | | search | Filter | | `false` | Search option. | | placeholder | string | | `'Search'` | Placeholder text for the search input. | | notFoundText | string | | `'No matches found'` | Text displayed when no search results are found. | | collapsable | boolean | | `false` | Whether to show a global collapse/expand button. | | layout | (this: Stencil, model: Model, group?: Group \| null) => any | | Grid layout | Layout method for nodes in the stencil canvas. | | layoutOptions | any | | - | Layout options. | | stencilGraphWidth | number | | `200` | Width of the stencil canvas. | | stencilGraphHeight | number | | `800` | Height of the stencil canvas. Set to 0 for auto-adjustment. | | stencilGraphPadding | number | | `10` | Padding for the stencil canvas. | | stencilGraphOptions | Graph.Options | | - | Options for the stencil canvas. | > **Tip** In addition to the configurations above, Stencil also inherits all configurations from Dnd. ### Grouping When initializing, a template canvas will be rendered in each group according to the `groups` provided. The type definition for groups is as follows: ```ts export interface Group { name: string // Group name title?: string // Group title, defaults to `name` collapsable?: boolean // Whether the group is collapsible, defaults to true collapsed?: boolean // Initial state whether it is collapsed nodeMovable?: boolean // Whether nodes in the stencil canvas can be dragged graphWidth?: number // Width of the stencil canvas graphHeight?: number // Height of the stencil canvas graphPadding?: number // Padding for the stencil canvas graphOptions?: Graph.Options // Options for the stencil canvas layout?: (this: Stencil, model: Model, group?: Group | null) => any layoutOptions?: any // Layout options } ``` As you can see, some configurations within the group overlap with the outer configurations, such as `graphWidth/stencilGraphWidth` and `graphHeight/stencilGraphHeight`, with the group configurations taking higher priority. ### Layout When adding nodes, use the group or global `layout` and `layoutOptions` to automatically layout the nodes. By default, the grid layout method is used to layout the template nodes, and the supported layout options are: | Option | Type | Default Value | Description | |---------------|-------------------------------|---------------|-------------------------------------------------------------------------------------------| | columns | number | `2` | Number of columns in the grid layout, defaults to `2`. The number of rows is calculated automatically based on the number of nodes. | | columnWidth | number \| 'auto' \| 'compact' | `'auto'` | Column width. `auto`: width of the widest node among all nodes, `compact`: width of the widest node in that column. | | rowHeight | number \| 'auto' \| 'compact' | `'auto'` | Row height. `auto`: height of the tallest node among all nodes, `compact`: height of the tallest node in that row. | | dx | number | `10` | Offset of the cell on the X-axis, defaults to `10`. | | dy | number | `10` | Offset of the cell on the Y-axis, defaults to `10`. | | marginX | number | `0` | Margin of the cell on the X-axis, defaults to `0`. | | marginY | number | `0` | Margin of the cell on the Y-axis, defaults to `0`. | | center | boolean | `true` | Whether the nodes are center-aligned with the grid, defaults to `true`. | | resizeToFit | boolean | `false` | Whether to automatically adjust the size of the nodes to fit the grid size, defaults to `false`. | You can also implement a custom layout according to the signature `(this: Stencil, model: Model, group?: Group | null) => any`. ### Search Stencil also provides search capabilities. ```ts // Only search for rect nodes const stencil = new Stencil({ search: (cell, keyword, groupName, stencil) => { if (keyword) { return cell.shape === 'rect' } return true }, }) // Search for rect nodes whose text contains the keyword const stencil = new Stencil({ search: (cell, keyword, groupName, stencil) => { if (keyword) { return cell.shape === 'rect' && cell.attr('text/text').includes(keyword) } return true }, }) ``` ## API ### stencil.load(...) ```ts load(nodes: (Node | Node.Metadata)[], groupName?: string): this ``` Load nodes. Parameters are as follows: | Name | Type | Required | Default Value | Description | |-----------|-----------------------------|:--------:|---------------|----------------------------| | nodes | `(Node \| Node.Metadata)[]` | ✓️ | - | Nodes to load. | | groupName | string | | - | Name of the group to load nodes into. | ### stencil.unload(...) ```ts unload(nodes: (Node | Node.Metadata)[], groupName?: string): this ``` Unload nodes. Parameters are as follows: | Name | Type | Required | Default Value | Description | |-----------|-----------------------------|:--------:|---------------|----------------------------| | nodes | `(Node \| Node.Metadata)[]` | ✓️ | - | Nodes to unload. | | groupName | string | | - | Name of the group to unload nodes from. | ### stencil.addGroup(...) ```ts addGroup(group: Stencil.Group | Stencil.Group[]) ``` Add a new group. ```ts stencil.addGroup({ name: 'group1', graphHeight: 100, }) ``` ### stencil.removeGroup(...) ```ts removeGroup(groupName: string | string[]) ``` Remove a group. ### stencil.resizeGroup(...) ```typescript resizeGroup(groupName: string, size: { width: number; height: number }) ``` Modify the size of a group. ### docs/demos/tutorial/plugins/stencil/index.less ```less .stencil-app { display: flex; border: 1px solid #f0f0f0; padding: 16px 8px; font-family: sans-serif; .app-stencil { position: relative; width: 200px; border: 1px solid #f0f0f0; } .app-content { flex: 1; height: 380px; margin-right: 8px; margin-left: 8px; box-shadow: 0 0 10px 1px #e9e9e9; } .x6-widget-stencil-group-title { // 避免 dumi 的样式影响 margin-top: 0 !important; } } ``` #### Export - Canonical page: https://x6.antv.antgroup.com/en/tutorial/plugins/export/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/plugins/export.md - Description: Export --- title: "Export" description: "Export" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/plugins/export/" version: "3.x" --- > **This chapter mainly introduces the knowledge of the export plugin. By reading, you can learn about** - How to export the canvas content in image format ## Usage You can export the canvas content as images via the `Export` plugin. Example: ```ts import { Graph, Export } from '@antv/x6' const graph = new Graph({ background: { color: '#F2F7FA', }, }) graph.use(new Export()) ``` ## API ### graph.exportSVG(...) ```ts exportSVG(fileName?: string, options?: ExportToSVGOptions): void ``` `fileName` is the name of the file, defaulting to `chart`. `ExportToSVGOptions` is described as follows: | Property Name | Type | Default Value | Required | Description | |--------------------|--------------------------------------------|---------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | preserveDimensions | `boolean \| Size` | - | | Controls the exported SVG size. When unset, width/height are `100%`; when `true`, they are auto-calculated to the actual content size; you can also pass `{ width, height }` to explicitly set the export size. | | viewBox | `RectangleLike` | - | | Sets the viewBox of the exported SVG. | | copyStyles | `boolean` | `true` | | Whether to copy styles from external stylesheets. When enabled, computed style differences of nodes are inlined into the exported SVG to keep visuals consistent with the page; this adds export time. If you prefer speed or styles are set via `attrs`, set to `false` and pair with `stylesheet` to inject necessary CSS. | | stylesheet | `string` | - | | Custom stylesheet. | | serializeImages | `boolean` | `true` | | Whether to convert the `xlink:href`/`href` of image elements to DataURI (forced for PNG/JPEG). | | beforeSerialize | `(this: Graph, svg: SVGSVGElement) => any` | - | | You can call `beforeSerialize` to modify the SVG string before exporting it. | ### graph.exportPNG(...) ```ts exportPNG(fileName?: string, options?: ExportToImageOptions): void ``` `fileName` is the name of the file, defaulting to `chart`. `ExportToImageOptions` inherits from the above `ExportToSVGOptions` and has the following additional configurations: | Property Name | Type | Default Value | Required | Description | |--------------------|-----------------------|---------------|----------|-------------------------------------------------------------------------------------------| | width | `number` | - | | Width of the exported image. | | height | `number` | - | | Height of the exported image. | | ratio | `number` | `1` | | Scale factor (e.g., device pixel ratio) used to compute export resolution. | | backgroundColor | `string` | `'white'` | | Background color of the exported image. | | padding | `NumberExt.SideOptions` | - | | Padding for the image. | | quality | `number` | - | | Image quality (0–1). If out of range, the default value `0.92` is used. | ### graph.exportJPEG(...) ```ts exportJPEG(fileName?: string, options?: ExportToImageOptions): void ``` ### graph.toSVG(...) ```ts toSVG(callback: ExportToSVGCallback, options?: ExportToSVGOptions): void ``` ### graph.toPNG(...) ```ts toPNG(callback: ExportToSVGCallback, options?: ExportToImageOptions): void ``` ### graph.toJPEG(...) ```ts toJPEG(callback: ExportToSVGCallback, options?: ExportToImageOptions): void ``` ### graph.toSVGAsync(...) ```ts toSVGAsync(options?: ExportToSVGOptions): Promise<string> ``` ### graph.toPNGAsync(...) ```ts toPNGAsync(options?: ExportToImageOptions): Promise<string> ``` ### graph.toJPEGAsync(...) ```ts toJPEGAsync(options?: ExportToImageOptions): Promise<string> ``` #### Port Editor - Canonical page: https://x6.antv.antgroup.com/en/tutorial/plugins/port-editor/ - Markdown source: https://x6.antv.antgroup.com/en/tutorial/plugins/port-editor.md - Description: Port Editor --- title: "Port Editor" description: "Port Editor" language: "en" canonical: "https://x6.antv.antgroup.com/en/tutorial/plugins/port-editor/" version: "3.x" --- :::info{title=This chapter introduces the port editor plugin, after reading you will know} - How to add and remove ports interactively on the canvas - How generated ports reuse the X6 port system (wiring and JSON export come for free) - How to configure the outline, the ids and the connection style ::: ## Usage A port in X6 is **declarative data**: position, style and group are written when the node is created, and at runtime only programmatic APIs such as `node.addPort` / `node.removePort` exist. The `PortEditor` plugin adds the missing **editing action**: it lets a user point at the canvas and say "put a port here". ```ts import { Graph, PortEditor } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container')!, }) const portEditor = new PortEditor({ group: 'pin', outlineSelector: '.x6-node-body', }) graph.use(portEditor) // Enter add-pin mode: a preview follows the node outline, a click creates the port there document.getElementById('add-pin')!.addEventListener('click', () => { portEditor.toggleAdding() }) ``` A pin lands on the **nearest point of the node outline**: every geometric shape of the node markup (`rect` / `circle` / `ellipse` / `path` / `polygon` / `polyline`) is sampled by arc length, and the pointer is projected onto the closest edge — so custom, non-rectangular symbols (hexagons, circles, transformer symbols) stay reachable, instead of snapping to the corners of a bounding box. The projection runs through the SVG CTM chain, so panning, zooming and node rotation need no special casing. The created pin is an ordinary port: you can drag a wire from it, it is included in `graph.toJSON()`, and `graph.fromJSON()` restores it unchanged. ## Options ```ts interface PortEditorOptions { // Port group used for pins; created on the node when absent group?: string // Prefix of generated pin ids: pin-1, pin-2 ... idPrefix?: string // 'local' keeps node-local pixels, 'percent' keeps the position relative to the node size positionUnit?: 'local' | 'percent' // Outline selector; defaults to every geometric shape of the markup, nearest one wins outlineSelector?: string // Max samples per outline sampleCount?: number // Create the port group when the node has none autoCreateGroup?: boolean // Group definition used by autoCreateGroup groupConfig?: Record<string, any> // Show a delete badge when hovering an existing pin deleteBadge?: boolean // Leave add-pin mode after a pin was placed stopAfterAdd?: boolean // Block node dragging while adding pins (default true) blockNodeMove?: boolean // Disable canvas panning while adding pins (default true) blockPanning?: boolean } ``` `blockNodeMove` and `blockPanning` default to `true`: pressing the element body to add a pin is also the gesture X6 uses to drag the node or pan the canvas. Both switches are scoped to add-pin mode and are **restored item by item** when the mode ends, leaving no trace in your graph options. ## API ```ts // mode portEditor.startAdding(): this portEditor.stopAdding(): this portEditor.toggleAdding(): this portEditor.isAdding(): boolean // pins portEditor.addPin(node: Node, local: Point): string | null portEditor.addPinAtClient(node: Node, clientX: number, clientY: number): string | null portEditor.addPinFromDrop(clientX: number, clientY: number, node?: Node): string | null portEditor.removePin(node: Node, portId: string): this portEditor.clearPins(node: Node): this portEditor.getPins(node: Node): Port[] // drop target (to be used with the Dnd plugin) portEditor.createDndDropValidator(options?: { isPinTemplate?: (node: Node) => boolean }): ( droppingNode: Node, ctx?: { targetGraph?: Graph }, ) => boolean // hit testing portEditor.findNodeAtClient(clientX: number, clientY: number): Node | null portEditor.dispose(): void ``` ## Working with other plugins - **Selection**: a selection box covers the node outline, which is exactly where pins live. Make the box ignore pointer events, otherwise a press on the outline neither creates a pin nor starts a wire while the node is selected: ```ts graph.use( new Selection({ showNodeSelectionBox: true, pointerEvents: 'none', }), ) ``` - **Dnd**: drop a "pin template" onto a node to create a pin, and prevent the template node itself from being dropped: ```ts graph.use( new Dnd({ target: graph, validateNode: portEditor.createDndDropValidator(), }), ) ``` By default a node is treated as a pin template when `node.getData().pinTemplate === true` or when its shape is `pin-dot`. - **History**: adding and removing a pin are wrapped in `model.startBatch/stopBatch`, so one user action equals one undo step. ## Examples ### OSCP 活动 - zh page: https://x6.antv.antgroup.com/zh/examples/animation/case/oscp/ - en page: https://x6.antv.antgroup.com/en/examples/animation/case/oscp/ - Source path: examples/animation/case/demo/oscp.ts ```ts import { Graph, Shape } from '@antv/x6' Graph.registerNode( 'custom-node', { width: 150, height: 60, attrs: { body: { stroke: '#000', strokeWidth: 2, fill: '#fff', refWidth: 1, refHeight: 1, rx: 10, }, image: { width: 16, height: 16, ref: 'body', }, icon: { ref: 'body', fill: '#fff', }, icon2: { ref: 'body', fill: '#fff', }, title: { text: 'Node', refX: 45, refY: 14, fill: '#fff', fontSize: 14, 'text-anchor': 'start', fontWeight: 'bold', }, text: { refX: 45, refY: 38, fontSize: 12, fill: '#fff', 'text-anchor': 'start', }, }, markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'image', selector: 'image', }, { tagName: 'text', selector: 'title', }, { tagName: 'text', selector: 'text', }, { tagName: 'path', selector: 'icon', }, { tagName: 'path', selector: 'icon2', }, ], }, true, ) const graph = new Graph({ container: document.getElementById('container'), grid: true, connecting: { snap: true, connector: { name: 'smooth' }, }, background: { color: '#fff', }, }) const mainNode = graph.addNode({ shape: 'custom-node', x: 300, y: 100, width: 300, height: 150, attrs: { body: { fill: '#FBEAD4', stroke: '#D7CEA9', strokeWidth: 2, rx: 20, }, image: { 'xlink:href': 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*A-lcQbVTpjwAAAAAAAAAAAAADmJ7AQ/original', width: 100, height: 100, refX: 0.5, refX2: -50, y: 0, }, title: { text: 'OSCP', fontSize: 20, refX: 0.5, refX2: -25, y: 90, fontWeight: 800, fill: 'rgba(0,0,0,0.85)', }, text: { text: '', }, }, }) const issueNode = graph.addNode({ shape: 'custom-node', x: 800, y: 140, attrs: { body: { fill: '#4CAF50', stroke: '', }, title: { text: '主线任务', }, text: { text: 'issue修复', }, icon: { d: 'M20 40 L20 30 L15 30 L20 25 L20 15 L30 15 L30 25 L35 30 L30 30 L30 40 ZM22 30 L28 30 M25 22 L25 28', refY: 0, refX: 0, }, }, animation: [ [ { 'attrs/icon/refY': -5 }, { duration: 800, iterations: Infinity, direction: 'alternate', }, ], ], }) const docNode = graph.addNode({ shape: 'custom-node', x: 650, y: 260, attrs: { body: { fill: '#4994EC', stroke: '', }, title: { text: '支线任务', }, text: { text: '文档捉"虫"', }, icon: { d: 'M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z', refX: 10, refY: 10, }, }, animation: [ [ { 'attrs/icon/refY': 15 }, { duration: 600, iterations: Infinity, direction: 'alternate', }, ], [ { 'attrs/icon/refX': 20 }, { duration: 700, iterations: Infinity, direction: 'alternate', }, ], ], }) const prNode = graph.addNode({ shape: 'custom-node', x: 800, y: 400, attrs: { body: { fill: '#FF9800', stroke: '', }, title: { text: '提交PR', }, text: { text: '一对一指导', }, icon: { d: 'M20 35 L25 30 L30 35 M25 30 L25 40', strokeWidth: 2, refY: -5, }, icon2: { d: 'M15 25 L35 25 L35 15 L15 15 Z', opacity: 0.6, }, }, animation: [ [ { 'attrs/icon/refY': 0 }, { duration: 600, iterations: Infinity, direction: 'alternate', }, ], ], }) const rewardNode = graph.addNode({ shape: 'custom-node', x: 350, y: 400, attrs: { body: { fill: '#9C27B0', stroke: '', filter: { name: 'highlight', args: { color: '#9C27B0', blur: 1, width: 8, opacity: 0.1, }, }, }, title: { text: '获得奖励', }, text: { text: '各种精美奖品', }, icon: { d: 'M20 35 L15 35 L15 25 L25 25 L25 35 L20 35 ZM18 25 L18 20 C18 18 19 16 20 16 C21 16 22 18 22 20 L22 25', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', }, }, animation: [ [ { 'attrs/body/opacity': [1, 0.85], }, { duration: 1000, iterations: Infinity, direction: 'alternate' }, ], ], }) const chatNode = graph.addNode({ shape: 'custom-node', x: 0, y: 240, attrs: { body: { fill: '#00BCD4', stroke: '', }, title: { text: '加入交流群', }, text: { text: '获取第一手资讯', }, icon: { refX: 10, refY: 10, d: 'M20 2H4C2.9 2 2 2.9 2 4V16C2 17.1 2.9 18 4 18H12L16 22V18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2Z', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', }, }, }) const edges = [ { source: mainNode, target: issueNode, }, { source: mainNode, target: docNode, attrs: { line: { stroke: '#C7D5F6', strokeWidth: 2, strokeDasharray: 5, strokeDashoffset: 0, }, }, }, { source: mainNode, target: chatNode, markup: [ { tagName: 'circle', selector: 'marker', attrs: { stroke: 'none', r: 5, }, }, ...Shape.Edge.getMarkup(), ], attrs: { line: { stroke: '#A3B1BF', strokeWidth: 2, strokeDasharray: 5, strokeDashoffset: 0, }, marker: { fill: '#C7D5F6', atConnectionRatio: 0, }, }, animation: [ [ { 'attrs/marker/atConnectionRatio': 1 }, { duration: 2000, iterations: Infinity, }, ], ], }, { source: issueNode, target: prNode, }, { source: docNode, target: prNode, attrs: { line: { stroke: '#C7D5F6', strokeWidth: 2, strokeDasharray: 5, strokeDashoffset: 0, }, }, }, { source: prNode, target: rewardNode, }, ] graph.addEdges( edges.map((edge) => { return { connector: { name: 'smooth' }, attrs: { line: { stroke: '#A3B1BF', strokeWidth: 2, strokeDasharray: 5, strokeDashoffset: 0, }, }, animation: [ [ { 'attrs/line/strokeDashoffset': -20 }, { duration: 600, iterations: Infinity, }, ], ], ...edge, } }), ) graph.centerContent() graph.zoomToFit() ``` ### 路线规划图 - zh page: https://x6.antv.antgroup.com/zh/examples/animation/case/route-planning/ - en page: https://x6.antv.antgroup.com/en/examples/animation/case/route-planning/ - Source path: examples/animation/case/demo/route-planning.ts ```ts import { Graph, Shape } from '@antv/x6' import insertCss from 'insert-css' Graph.registerConnector( 'loop-curve', (start, end) => { const sx = start.x const sy = start.y const ex = end.x const ey = end.y const mx = (sx + ex) / 2 const my = (sy + ey) / 2 const dir = sy < ey ? 1 : -1 const offset = 30 const path = [ `M ${sx} ${sy}`, `C ${sx + 0} ${sy - 0 * dir}, ${mx - 50} ${my + 15 * dir}, ${mx - 20} ${my}`, `C ${mx + 0} ${my - 6 * dir}, ${mx} ${my - offset * dir}, ${mx - 20} ${my - offset * dir + 10 * dir}`, `C ${mx - 25} ${my - offset * dir + 15 * dir}, ${mx - 30} ${my - 10 * dir}, ${mx} ${my}`, `C ${mx + 60} ${my + 20 * dir}, ${ex - 0} ${ey + 0 * dir}, ${ex} ${ey}`, ].join(' ') return path }, true, ) Graph.registerEdge( 'plane-edge', { markup: [ { tagName: 'svg', selector: 'plane', attrs: { viewBox: '0 0 1024 1024', width: 32, height: 32, y: -16, opacity: 0, }, children: [ { tagName: 'path', attrs: { d: 'M488.5 391.1l-90.5-69.9a9.6 9.6 0 0 1 5.9-17.1h94.3a9.6 9.6 0 0 1 5.9 2l110.2 85.1H488.5z', fill: '#fff', }, }, { tagName: 'path', attrs: { d: 'M488.7 294.4H374.6a19.2 19.2 0 0 0-15.2 7.5c-6.5 8.4-4.9 20.4 3.5 26.9l93 71.9h176.9l-114.4-85a19.2 19.2 0 0 0-11.7-4z m-26.2 87.1L374.6 313.6h114.1l87.8 67.9H462.4z', fill: '#30699A', }, }, { tagName: 'path', attrs: { d: 'M143.5 344.4l-26.9 10a28.8 28.8 0 0 0-18.8 27v78.5a57.6 57.6 0 0 0 22.6 45.7l150.3 115.2a57.6 57.6 0 0 0 35 11.9h557c31.8 0 57.6-25.8 57.6-57.6a57.6 57.6 0 0 0-21.6-45l-163.4-130.6l-.5-.4a57.6 57.6 0 0 0-35.4-12.2l-480.6 0-47-38a28.8 28.8 0 0 0-28.1-4.6z m6.7 18a9.6 9.6 0 0 1 9.1 1.3l.3.2l52.3 42.2h487.4a38.4 38.4 0 0 1 23.5 8l.4.4l163.4 130.6a38.4 38.4 0 0 1 14.4 30c0 21-16.8 38.1-37.8 38.4l-.6 0H305.7a38.4 38.4 0 0 1-22.9-7.6l-.5-.3L132.1 460.5a38.4 38.4 0 0 1-14.6-29.7v-79a9.6 9.6 0 0 1 6-8.9l.3-.1l26.9-10z', fill: '#30699A', }, }, { tagName: 'path', attrs: { d: 'M150.2 362.4a9.6 9.6 0 0 1 9.4 1.5l52.3 42.2h487.4a38.4 38.4 0 0 1 24 8.4l163.4 130.6a38.4 38.4 0 0 1 14.4 30c0 21.2-17.2 38.4-38.4 38.4H309L151.7 489.7a38.4 38.4 0 0 1-14.6-29.6v-.5v-87.2c0-2.1.7-4.1 1.9-5.8z', fill: '#3982D1', }, }, { tagName: 'path', attrs: { d: 'M560.6 584.6L426.5 702.1c-4.5 4-4.7 10.6-.4 14.7 2.1 2.1 5.1 3.2 8.2 3.2h68.4c2.9 0 5.7-1 7.9-2.9h10l141.4-132.6', fill: '#fff', }, }, { tagName: 'path', attrs: { d: 'M528 577.4L394.7 694.8c-8.7 7.7-9 20.8-.7 28.9 3.8 3.7 9 5.8 14.4 5.9h107.7c5.1 0 10-1.8 13.8-5l.4-.3l150.5-132.6l-12.7-14.4-150.5 132.6c-.3.3-.8.4-1.3.5h-107.7c-.6 0-1.2-.2-1.5-.5-.2-.2-.3-.3 0-.5l.1-.1l133.4-117.5l-12.7-14.4z', fill: '#30699A', }, }, { tagName: 'path', attrs: { d: 'M805.4 492.7v19.2H700.2v-19.2z', fill: '#fff', }, }, { tagName: 'path', attrs: { d: 'M431.5 497.5a8 8 0 1 1 0 16 8 8 0 0 1 0-16zm56.8 0a8 8 0 1 1 0 16 8 8 0 0 1 0-16zm57.4 0a8 8 0 1 1 0 16 8 8 0 0 1 0-16zm57.4 0a8 8 0 1 1 0 16 8 8 0 0 1 0-16z', fill: '#FFF', }, }, ], }, // @ts-expect-error ...Shape.Edge.getMarkup(), ], attrs: { line: { strokeDasharray: 4, stroke: '#B8A58A', }, plane: { atConnectionRatio: 0, }, }, }, true, ) Shape.HTML.register({ shape: 'destination-node', width: 160, height: 80, effect: ['data'], html(cell) { const { img, tag, tagPosition = 'top-left', des, star, } = cell.getData() ?? {} const div = document.createElement('div') div.className = 'routePlanning' div.innerHTML = ` <div class="destination-node"> <div class="node-image-wrapper"> <div class="node-image-shadow"></div> <img class="node-image" src='${img}' /> </div> <div class="node-label-wrapper"> <div class="node-label">${des}</div> <div class="node-label-shadow"></div> <div class="node-label-tag node-label-tag-${tagPosition}">${tag}</div> <svg width="30" height="30" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg" class="node-label-star" style="display: ${star ? 'block' : 'none'}" transform="rotate(0 0 0)"> <polygon points="15,5 18.54,13.62 27,14.46 21,21.8 22.38,30 15,25.2 7.62,30 9,21.8 3,14.46 11.46,13.62" fill="gold" stroke="orange" stroke-width="1" /> </svg> </div> </div> ` return div }, }) let currentPlaneIdx = 0 const graph = new Graph({ container: document.getElementById('container'), grid: { visible: true, type: 'doubleMesh', size: 25, args: { color: '#EEAD8A', }, }, background: { color: '#FAEFB3', }, }) const stops = [ { img: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*8HpYRKt7uzwAAAAAVmAAAAgAemJ7AQ/fmt.webp', y: 225, des: '法国 - 巴黎铁塔', }, { img: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ZFxTS428nZkAAAAAaLAAAAgAemJ7AQ/fmt.webp', y: 150, des: '美国 - 自由女神像', tagPosition: 'bottom-left', }, { img: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*tfejTKFkjUUAAAAAW0AAAAgAemJ7AQ/fmt.webp', y: 225, des: '罗马 - 古罗马斗兽场', tagPosition: 'top-right', }, { img: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*OM9JTbpmU-cAAAAAZbAAAAgAemJ7AQ/fmt.webp', y: 175, des: '英国 - 伦敦塔桥', star: true, }, ] stops.forEach((stop, index) => { graph.addNode({ id: `stop${index + 1}`, shape: 'destination-node', x: 160 + index * 310, y: stop.y, data: { ...stop, tag: `${index + 1} stop` }, }) if (index + 1 === stops.length) return graph.addEdge({ source: { cell: `stop${index + 1}` }, target: { cell: `stop${index + 2}` }, connector: { name: 'loop-curve' }, shape: 'plane-edge', animation: [ [ { 'attrs/line/strokeDashoffset': [0, -16] }, { duration: 1000, iterations: Infinity, }, ], ], }) }) const fly = () => { const edges = graph.getEdges() const edge = edges[currentPlaneIdx] edge.setAttrByPath('plane/opacity', 1) const flyAni = edge.getAnimations().find((item) => item.id === 'fly') if (flyAni) { flyAni.play() return } const ani = edge.animate( { 'attrs/plane/atConnectionRatio': 1 }, { duration: 2000, id: 'fly' }, ) ani.onfinish = () => { edge.setAttrByPath('plane/opacity', 0) currentPlaneIdx = currentPlaneIdx + 1 === edges.length ? 0 : currentPlaneIdx + 1 fly() } } fly() graph.centerContent() graph.zoomToFit() insertCss(` .destination-node { display: flex; flex-direction: column; align-items: center; font-family: "Hiragino Maru Gothic Pro", /* 圆体最可爱,优先日文/繁体环境 */ "PingFang SC", /* 苹方,简体中文首选,清新现代 */ "Hiragino Sans GB", /* 冬青黑体简体,柔和清晰 */ "Helvetica Neue", "Arial", sans-serif; } .node-image-wrapper { position: relative; width: 110px; height: 110px; background: #fff; border-radius: 50%; } .node-image { width: 100%; height: 100%; border-radius: 50%; } .node-image-shadow { position: absolute; top: 0; left: 4px; width: 100%; height: 100%; background: #EBB27D; z-index: -1; border-radius: 50%; } .node-label-wrapper { margin-top: 20px; background-color: #FEFCF2; min-height: 120px; border-radius: 4px; width: 150px; position: relative; color: #838181; box-shadow: 5px 5px 15px rgba(0, 0, 0, 0.2); transition: box-shadow 0.3s ease; } .node-label-wrapper:hover { box-shadow: 8px 8px 20px rgba(0, 0, 0, 0.3); } .node-label-shadow { position: absolute; top: 0; left: 3px; width: 100%; height: 100%; background: #F7D0AE; z-index: -1; border-radius: 4px; transform: rotate(-2deg); } .node-label { padding: 8px; z-index: 2; } .node-label-tag { background: #EEAD8A; color: #fff; border-radius: 4px; position: absolute; line-height: 22px; width: 60px; text-align: center; font-size: 12px; font-weight: 800; } @keyframes shake { from { transform: rotate(0deg); } to { transform: rotate(20deg); } } .node-label-star { position: absolute; right: 8px; bottom: 8px; animation: shake 0.5s ease infinite; animation-direction: alternate; } .node-label-tag-top-left { left: 0; top: 0; transform: translateX(-50%) translateY(-50%) rotate(-20deg); } .node-label-tag-bottom-left { left: 0; bottom: 0; transform: translateX(-50%) translateY(50%) rotate(20deg); } .node-label-tag-top-right { right: 0; top: 0; transform: translateX(50%) translateY(-50%) rotate(20deg); } .node-label-tag-bottom-right { right: 0; bottom: 0; transform: translateX(50%) translateY(50%) rotate(-20deg); } `) ``` ### 炫酷散射动画 - zh page: https://x6.antv.antgroup.com/zh/examples/animation/case/scattering/ - en page: https://x6.antv.antgroup.com/en/examples/animation/case/scattering/ - Source path: examples/animation/case/demo/scattering.ts ```ts import { Graph } from '@antv/x6' const circleRadius = 20 const rippleCounts = 5 const centerPoint = { x: 400, y: 400 } function getPoint( start: { x: number; y: number }, angleDeg: number, dist: number, ) { const rad = (angleDeg * Math.PI) / 180 return { x: start.x + dist * Math.cos(rad), y: start.y + dist * Math.sin(rad), } } const rippleMarkups = Array.from({ length: rippleCounts }).map((_, index) => ({ tagName: 'circle', selector: `ripple${index}`, groupSelector: 'rippleGroup', attrs: { cx: circleRadius, cy: circleRadius, }, })) const rippleAnimations = Array.from({ length: rippleCounts }).map( (_, index) => [ { [`attrs/ripple${index}/r`]: [0, 350], [`attrs/ripple${index}/strokeOpacity`]: [0.5, 0], }, { duration: 2000, fill: 'forwards', delay: index * 150, iterations: Infinity, easing: 'ease-out-sine', }, ], ) as AnimateParams[] Graph.registerNode( 'custom-circle', { inherit: 'circle', width: circleRadius * 2, height: circleRadius * 2, markup: [ ...rippleMarkups, { selector: 'center', tagName: 'circle', }, { selector: 'body', tagName: 'circle', }, ], attrs: { body: { fill: '#fff', stroke: 'none', magnet: true, }, center: { fill: '#DEE9EB', stroke: 'none', r: circleRadius, cx: circleRadius, cy: circleRadius, filter: { name: 'dropShadow', args: { dx: 0, dy: 0, blur: 10, color: '#fff', }, }, }, rippleGroup: { fill: 'transparent', strokeWidth: 4, stroke: '#0ff', }, }, animation: [ ...rippleAnimations, [ { 'attrs/center/r': [circleRadius, circleRadius * 1.2], }, { duration: 1000, fill: 'forwards', iterations: Infinity, direction: 'alternate', }, ], ], }, true, ) Graph.registerEdge( 'custom-edge', { inherit: 'edge', markup: [ { tagName: 'path', selector: 'line', }, { tagName: 'path', selector: 'fill', }, ], attrs: { line: { connection: true, strokeWidth: 8, strokeLinecap: 'round', opacity: 0.7, targetMarker: null, strokeDasharray: '10,20', stroke: { type: 'linearGradient', stops: [ { offset: '0%', color: '#0ff', opacity: 1 }, { offset: '50%', color: '#f0f', opacity: 0.8 }, { offset: '100%', color: '#ff0' }, ], }, }, }, animation: [ [ { 'attrs/line/opacity': [0.7, 1], }, { duration: 1000, fill: 'forwards', direction: 'alternate', iterations: Infinity, }, ], [ { 'attrs/line/strokeDashoffset': [30, 0], }, { duration: 500, iterations: Infinity, }, ], ], }, true, ) const graph = new Graph({ container: document.getElementById('container'), connecting: { connectionPoint: { name: 'anchor', args: { offset: 20, }, }, }, background: { color: '#000', }, }) graph.addNode({ id: 'main', shape: 'custom-circle', ...centerPoint, }) const edgeDistances = [300, 250, 280, 240, 230, 260, 210, 240, 270, 220] edgeDistances.forEach((d, index) => { const p = getPoint(centerPoint, (360 / 10) * index, d) graph.addNode({ id: `node${index}`, shape: 'circle', x: p.x, y: p.y, }) graph.addEdge({ source: 'main', target: `node${index}`, shape: 'custom-edge', zIndex: -1, }) }) graph.centerContent() ``` ### 基本使用 - zh page: https://x6.antv.antgroup.com/zh/examples/animation/usage/basic/ - en page: https://x6.antv.antgroup.com/en/examples/animation/usage/basic/ - Source path: examples/animation/usage/demo/basic.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addNode({ x: 40, y: 40, width: 100, height: 60, attrs: { body: { fill: '#9254de', stroke: 'none', rx: 10, }, }, animation: [ [ { 'position/x': 50 }, { duration: 1000, direction: 'alternate', iterations: Infinity }, ], ], }) graph.addNode({ x: 200, y: 40, width: 100, height: 60, attrs: { body: { stroke: 'none', rx: 10, }, }, animation: [ [ { 'attrs/body/fill': ['#9254de', '#06b6d4'] }, { duration: 3000, direction: 'alternate', iterations: Infinity }, ], ], }) graph.addNode({ x: 360, y: 40, width: 60, height: 60, attrs: { body: { fill: '#85C054', stroke: 'none', rx: 10, }, }, animation: [ [ { 'size/width': 120, }, { duration: 1000, direction: 'alternate', iterations: Infinity, }, ], ], }) graph.addNode({ x: 40, y: 160, width: 60, height: 60, attrs: { body: { fill: '#06b6d4', stroke: 'none', rx: 10, }, }, animation: [ [ { angle: 360, }, { duration: 1000, iterations: Infinity, }, ], ], }) graph.addNode({ x: 200, y: 160, width: 100, height: 60, attrs: { body: { fill: '#DB655C', stroke: 'none', rx: 10, }, }, animation: [ [ { 'attrs/body/opacity': [0.5, 1], }, { duration: 1000, direction: 'alternate', iterations: Infinity, }, ], ], }) graph.addNode({ x: 360, y: 160, width: 100, height: 60, label: '文本', attrs: { body: { fill: '#efdbff', stroke: 'none', rx: 10, }, }, animation: [ [ { 'attrs/text/fontSize': [10, 16], }, { duration: 1000, direction: 'alternate', iterations: Infinity, }, ], ], }) ``` ### 呼吸效果 - zh page: https://x6.antv.antgroup.com/zh/examples/animation/usage/breathing-circle/ - en page: https://x6.antv.antgroup.com/en/examples/animation/usage/breathing-circle/ - Source path: examples/animation/usage/demo/breathing-circle.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const palette = ['#3875f6', '#efb041', '#ec5b56', '#72c240'] const radius = 30 palette.forEach((color, index) => { graph.addNode({ shape: 'circle', width: radius * 2, height: radius * 2, x: index % 2 === 0 ? 0 : 300, y: index < 2 ? 0 : 260, markup: [ { tagName: 'circle', selector: 'wrapper', }, { tagName: 'circle', selector: 'body', }, ], attrs: { body: { fill: color, strokeWidth: 0, }, wrapper: { r: radius, cx: radius, cy: radius, fill: color, opacity: 0.4, strokeWidth: 0, }, }, animation: [ [ { 'attrs/wrapper/r': radius + 10, }, { duration: 1000, direction: 'alternate', iterations: Infinity, }, ], ], }) }) graph.centerContent() ``` ### 组合动画 - zh page: https://x6.antv.antgroup.com/zh/examples/animation/usage/composition/ - en page: https://x6.antv.antgroup.com/en/examples/animation/usage/composition/ - Source path: examples/animation/usage/demo/composition.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addNode({ shape: 'path', x: 50, y: 40, width: 80, height: 80, path: 'M24.85,10.126c2.018-4.783,6.628-8.125,11.99-8.125c7.223,0,12.425,6.179,13.079,13.543c0,0,0.353,1.828-0.424,5.119c-1.058,4.482-3.545,8.464-6.898,11.503L24.85,48L7.402,32.165c-3.353-3.038-5.84-7.021-6.898-11.503c-0.777-3.291-0.424-5.119-0.424-5.119C0.734,8.179,5.936,2,13.159,2C18.522,2,22.832,5.343,24.85,10.126z', attrs: { body: { fill: '#D75A4A', stroke: 'none', }, }, animation: [ [ { // 可以同时配置多种动画效果 'size/width': 85, 'size/height': 85, 'attrs/body/opacity': [1, 0.9], }, { duration: 900, iterations: Infinity }, ], ], }) graph.addNode({ shape: 'path', x: 210, y: 40, width: 80, height: 80, path: 'M26.285,2.486l5.407,10.956c0.376,0.762,1.103,1.29,1.944,1.412l12.091,1.757c2.118,0.308,2.963,2.91,1.431,4.403l-8.749,8.528c-0.608,0.593-0.886,1.448-0.742,2.285l2.065,12.042c0.362,2.109-1.852,3.717-3.746,2.722l-10.814-5.685c-0.752-0.395-1.651-0.395-2.403,0l-10.814,5.685c-1.894,0.996-4.108-0.613-3.746-2.722l2.065-12.042c0.144-0.837-0.134-1.692-0.742-2.285l-8.749-8.528c-1.532-1.494-0.687-4.096,1.431-4.403l12.091-1.757c0.841-0.122,1.568-0.65,1.944-1.412l5.407-10.956C22.602,0.567,25.338,0.567,26.285,2.486z', attrs: { body: { fill: '#ED8A19', stroke: 'none', }, }, animation: [ [ { angle: [0, 360] }, { duration: 3000, iterations: Infinity, }, ], // 可以配置多个动画效果,以及分别进行动画配置 [ { 'attrs/body/fill': '#D94F00', }, { duration: 2000, iterations: Infinity, direction: 'alternate', }, ], ], }) graph.addNode({ shape: 'path', x: 370, y: 40, width: 80, height: 80, path: 'M52.104,0.249c-0.216-0.189-0.501-0.275-0.789-0.241l-31,4.011c-0.499,0.065-0.872,0.489-0.872,0.992v6.017v4.212v26.035C17.706,39.285,14.997,38,11.944,38c-5.247,0-9.5,3.781-9.5,8.444s4.253,8.444,9.5,8.444s9.5-3.781,9.5-8.444c0-0.332-0.027-0.658-0.069-0.981c0.04-0.108,0.069-0.221,0.069-0.343V16.118l29-3.753v18.909C48.706,29.285,45.997,28,42.944,28c-5.247,0-9.5,3.781-9.5,8.444s4.253,8.444,9.5,8.444s9.5-3.781,9.5-8.444c0-0.092-0.012-0.181-0.015-0.272c0.002-0.027,0.015-0.05,0.015-0.077V11.227V7.016V1C52.444,0.712,52.32,0.438,52.104,0.249z', attrs: { body: { fill: '#eb2f96', stroke: 'none', }, }, animation: [ [ { // 可以同时配置多种关键帧 'position/x': [370, 390, 350], 'position/y': [40, 50, 60], angle: [0, 40, 0, 30], }, { duration: 2000, direction: 'alternate', iterations: Infinity }, ], ], }) ``` ### 控制动画播放 - zh page: https://x6.antv.antgroup.com/zh/examples/animation/usage/control/ - en page: https://x6.antv.antgroup.com/en/examples/animation/usage/control/ - Source path: examples/animation/usage/demo/control.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const cancelNode = graph.addNode({ x: 40, y: 40, width: 100, height: 60, label: '点击取消动画', attrs: { body: { fill: '#9254de', stroke: 'none', rx: 10, }, text: { fill: '#fff', }, }, animation: [ [ { 'position/x': 50 }, { duration: 1000, direction: 'alternate', iterations: Infinity }, ], ], }) const rateNode = graph.addNode({ x: 200, y: 40, width: 100, height: 60, label: '点击2倍速动画', attrs: { body: { fill: '#DB655C', stroke: 'none', rx: 10, }, }, animation: [ [ { 'attrs/body/opacity': [1, 0.7], }, { duration: 1000, direction: 'alternate', iterations: Infinity, }, ], ], }) const pauseNode = graph.addNode({ x: 360, y: 40, width: 100, height: 60, label: '点击暂停动画', attrs: { body: { fill: '#efdbff', stroke: 'none', rx: 10, }, }, animation: [ [ { 'attrs/text/fontSize': [10, 16], }, { duration: 1000, direction: 'alternate', iterations: Infinity, }, ], ], }) graph.on('node:click', ({ cell }) => { if (cell === cancelNode) { const [ani] = cell.getAnimations() ani.cancel() } if (cell === rateNode) { const [ani] = cell.getAnimations() ani.updatePlaybackRate(2) } if (cell === pauseNode) { const label = cell.getPropByPath('attrs/text/text') const [ani] = cell.getAnimations() if (label === '点击暂停动画') { ani.pause() cell.setPropByPath('attrs/text/text', '点击继续动画') } else { ani.play() cell.setPropByPath('attrs/text/text', '点击暂停动画') } } }) ``` ### CSS动画 - zh page: https://x6.antv.antgroup.com/zh/examples/animation/usage/css/ - en page: https://x6.antv.antgroup.com/en/examples/animation/usage/css/ - Source path: examples/animation/usage/demo/css.ts ```ts import { Graph } from '@antv/x6' import insertCss from 'insert-css' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addNode({ x: 40, y: 40, width: 100, height: 60, attrs: { body: { fill: '#efdbff', stroke: 'none', rx: 10, style: { // 也可以使用CSS动画,具体如何选择可参考文档中的详细说明 animation: 'trans 1s infinite linear', }, }, }, }) graph.addNode({ x: 200, y: 40, width: 100, height: 60, attrs: { body: { fill: '#85C054', stroke: 'none', rx: 10, // 也可以通过添加class来添加动画 class: 'css-animation-node', }, }, }) // 我们用 insert-css 协助demo演示 // 实际项目中只要将下面样式添加到样式文件中 insertCss(` @keyframes trans { to { transform: translateX(50px); } } .css-animation-node { animation: trans 1s infinite linear; } `) ``` ### 自定义属性动画 - zh page: https://x6.antv.antgroup.com/zh/examples/animation/usage/custom-attr/ - en page: https://x6.antv.antgroup.com/en/examples/animation/usage/custom-attr/ - Source path: examples/animation/usage/demo/custom-attr.ts ```ts import { Dom, Graph, Shape } from '@antv/x6' Shape.HTML.register({ shape: 'custom-html', width: 160, height: 80, effect: ['data'], html(cell) { // 自定义的html节点接收一个面积数据来控制div大小,可以对该自定义属性添加动画效果 const { ratio } = cell.getData() ?? {} const div = document.createElement('div') const area = 12000 const width = Math.sqrt(area / ratio) const height = width * ratio Dom.css(div, { width, height, background: '#72C240', borderRadius: 10, }) return div }, }) const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addNode({ shape: 'custom-html', x: 80, y: 80, data: { ratio: 1 }, animation: [ // 对自定义节点的面积数据添加动画效果 [{ 'data/ratio': 3 / 5 }, { duration: 1000, iterations: Infinity }], ], }) ``` ### 边动画 - zh page: https://x6.antv.antgroup.com/zh/examples/animation/usage/edge/ - en page: https://x6.antv.antgroup.com/en/examples/animation/usage/edge/ - Source path: examples/animation/usage/demo/edge.ts ```ts import { Graph, Shape } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addEdge({ source: { x: 60, y: 60 }, target: { x: 240, y: 60 }, attrs: { line: { strokeDasharray: 5, strokeDashoffset: 0, }, }, animation: [ [ { 'attrs/line/strokeDashoffset': -20 }, { duration: 1000, iterations: Infinity, }, ], ], }) graph.addEdge({ source: { x: 60, y: 120 }, target: { x: 240, y: 120 }, vertices: [{ x: 160, y: 140 }], connector: { name: 'smooth' }, markup: [ { tagName: 'circle', selector: 'marker', attrs: { stroke: 'none', r: 5, }, }, ...Shape.Edge.getMarkup(), ], attrs: { line: { strokeDasharray: 5, strokeDashoffset: 0, stroke: '#9DADCE', }, marker: { fill: '#C7D5F6', atConnectionRatio: 0, }, }, animation: [ [ { 'attrs/marker/atConnectionRatio': 1 }, { duration: 2000, iterations: Infinity, }, ], ], }) graph.addEdge({ source: { x: 60, y: 180 }, target: { x: 240, y: 180 }, vertices: [{ x: 160, y: 200 }], connector: { name: 'smooth' }, attrs: { line: { stroke: '#7d8fff', strokeWidth: 2, opacity: 0.6, style: { filter: 'drop-shadow(0 0 2px #7d8fff)', }, }, }, animation: [ [ { 'attrs/line/strokeWidth': 4 }, { duration: 2500, iterations: Infinity, direction: 'alternate', easing: 'ease-in-out-back', }, ], [ 'attrs/line/opacity', 1, { duration: 2500, iterations: Infinity, direction: 'alternate', easing: 'ease-in-out-quad', }, ], ], }) graph.addEdge({ source: { x: 60, y: 240 }, target: { x: 240, y: 240 }, vertices: [{ x: 160, y: 260 }], connector: { name: 'smooth' }, markup: [ { tagName: 'path', selector: 'line', }, ], attrs: { line: { connection: true, strokeWidth: 8, strokeLinecap: 'round', targetMarker: null, strokeDasharray: '10,20', fill: 'none', stroke: { type: 'linearGradient', stops: [ { offset: '0%', color: '#4874EE', opacity: 1 }, { offset: '50%', color: '#f0f', opacity: 0.8 }, { offset: '100%', color: '#DB655C' }, ], }, }, }, animation: [ [ { 'attrs/line/opacity': [0.7, 1], }, { duration: 1000, fill: 'forwards', direction: 'alternate', iterations: Infinity, }, ], [ { 'attrs/line/strokeDashoffset': [30, 0], }, { duration: 500, iterations: Infinity, }, ], ], }) ``` ### 涟漪效果 - zh page: https://x6.antv.antgroup.com/zh/examples/animation/usage/ripple-circle/ - en page: https://x6.antv.antgroup.com/en/examples/animation/usage/ripple-circle/ - Source path: examples/animation/usage/demo/ripple-circle.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const palette = ['#3875f6', '#efb041', '#ec5b56', '#72c240'] const r = 30 const length = 5 const opacity = 0.4 palette.forEach((color, index) => { graph.addNode({ shape: 'circle', width: r * 2, height: r * 2, x: index % 2 === 0 ? 0 : 300, y: index < 2 ? 0 : 260, markup: [ ...Array.from({ length }).map((_, markupIndex) => { return { tagName: 'circle', selector: `ripple${markupIndex}`, groupSelector: 'rippleGroup', } }), { tagName: 'circle', selector: 'body', }, ], attrs: { body: { fill: color, strokeWidth: 0, }, rippleGroup: { cx: r, cy: r, fill: color, strokeWidth: 0, }, }, animation: Array.from({ length }).map((_, aniIndex) => [ { [`attrs/ripple${aniIndex}/r`]: [r, r + length * 5], [`attrs/ripple${aniIndex}/opacity`]: [opacity, 0], }, { duration: 1000 * length, iterations: Infinity, delay: 1000 * aniIndex, }, ]), }) }) graph.centerContent() ``` ### 二次贝塞尔曲线 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/connector/curve/ - en page: https://x6.antv.antgroup.com/en/examples/edge/connector/curve/ - Source path: examples/edge/connector/demo/curve.ts ```ts import { Graph, Path, Transform } from '@antv/x6' const degreesToRadians = (degrees) => degrees * (Math.PI / 180) const transCanvasAngleToRos = (angle) => angle - 90 const calcPoint = (p) => { const distance = 10 const theta = degreesToRadians(transCanvasAngleToRos(p.angle)) const x = p.x + distance * Math.cos(theta) const y = p.y + distance * Math.sin(theta) return { x, y } } const calcIntersectionPoint = (points) => { const [p1, p2] = points const p11 = calcPoint(p1) const p22 = calcPoint(p2) const d1 = (p1.x - p11.x) * (p2.y - p22.y) const d2 = (p1.y - p11.y) * (p2.x - p22.x) const d = d1 - d2 if (d === 0) { return { x: p1.x, y: p1.y } } const u1 = p1.x * p11.y - p1.y * p11.x const u4 = p2.x * p22.y - p2.y * p22.x const u2x = p2.x - p22.x const u3x = p1.x - p11.x const u2y = p2.y - p22.y const u3y = p1.y - p11.y const px = (u1 * u2x - u3x * u4) / d const py = (u1 * u2y - u3y * u4) / d const p = { x: px, y: py } return p } const graph = new Graph({ container: document.getElementById('container'), grid: true, connecting: { anchor: 'nodeCenter', connectionPoint: 'anchor', }, }) graph.use( new Transform({ rotating: { enabled: true, }, }), ) Graph.registerConnector( 'curve', (sourcePoint, targetPoint) => { const path = new Path() path.appendSegment(Path.createSegment('M', sourcePoint)) // 基于方向获取控制点 const controlPoint = calcIntersectionPoint([ { ...sourcePoint, angle: graph.getNodesFromPoint(sourcePoint)[0].getAngle(), }, { ...targetPoint, angle: graph.getNodesFromPoint(targetPoint)[0].getAngle(), }, ]) // 基于控制点生成二次贝塞尔曲线 path.appendSegment(path.quadTo(controlPoint, targetPoint)) return path.serialize() }, true, ) graph.addNode({ id: 'source', x: 120, y: 40, angle: 0, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) graph.addNode({ id: 'target', x: 400, y: 260, angle: 0, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) graph.addEdge({ source: 'source', target: 'target', connector: { name: 'curve' }, attrs: { line: { stroke: '#722ed1', }, }, }) ``` ### 跳线 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/connector/jumpover/ - en page: https://x6.antv.antgroup.com/en/examples/edge/connector/jumpover/ - Source path: examples/edge/connector/demo/jumpover.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const rect = graph.createNode({ x: 140, y: 40, width: 70, height: 30, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const source = graph.addNode(rect.clone().position(30, 90)) const target = graph.addNode(rect.clone().position(30, 170)) const edge = graph.addEdge({ source, target, vertices: [ { x: 640, y: 160 }, { x: 640, y: 240 }, ], attrs: { line: { stroke: '#722ed1', }, }, }) for (let i = 0; i < 5; i += 1) { const source = graph.addNode(rect.clone().translate(i * 100, i * 10)) const target = graph.addNode(source.clone().translate(0, 200)) if (i % 2 === 0) { graph.addEdge({ source, target, connector: { name: 'jumpover', args: { type: 'gap', }, }, attrs: { line: { stroke: '#faad14', }, }, }) } else { graph.addEdge({ source, target, attrs: { line: { stroke: '#d9d9d9', }, }, }) } } edge.setConnector('jumpover') ``` ### 多条光滑曲线 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/connector/multiple-smooth-edges/ - en page: https://x6.antv.antgroup.com/en/examples/edge/connector/multiple-smooth-edges/ - Source path: examples/edge/connector/demo/multiple-smooth-edges.ts ```ts import { Graph, Line, Path, Curve } from '@antv/x6' Graph.registerConnector( 'multi-smooth', ( sourcePoint, targetPoint, routePoints, options: { raw?: boolean; index?: number; total?: number; gap?: number }, ) => { const { index = 0, total = 1, gap = 12 } = options const line = new Line(sourcePoint, targetPoint) const centerIndex = (total - 1) / 2 const dist = index - centerIndex const diff = Math.abs(dist) const factor = diff === 0 ? 1 : diff / dist const vertice = line .pointAtLength(line.length() / 2 + gap * factor * Math.ceil(diff)) .rotate(90, line.getCenter()) const points = [sourcePoint, vertice, targetPoint] const curves = Curve.throughPoints(points) const path = new Path(curves) return options.raw ? path : path.serialize() }, true, ) const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 120, y: 40, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const target = graph.addNode({ x: 400, y: 260, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const total = 15 for (let i = 0; i < total; i += 1) { graph.addEdge({ source, target, connector: { name: 'multi-smooth', args: { total, index: i, }, }, attrs: { line: { stroke: '#722ed1', strokeWidth: 1, targetMarker: null, }, }, }) } ``` ### 默认连线 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/connector/normal/ - en page: https://x6.antv.antgroup.com/en/examples/edge/connector/normal/ - Source path: examples/edge/connector/demo/normal.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addNode({ id: 'a', x: 120, y: 40, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) graph.addNode({ id: 'b', x: 400, y: 260, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) graph.addEdge({ source: 'a', target: 'b', vertices: [ { x: 200, y: 200 }, { x: 380, y: 120 }, ], // https://x6.antv.vision/zh/docs/api/registry/connector#normal connector: { name: 'normal' }, attrs: { line: { stroke: '#722ed1', }, }, }) ``` ### 圆角连线 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/connector/rounded/ - en page: https://x6.antv.antgroup.com/en/examples/edge/connector/rounded/ - Source path: examples/edge/connector/demo/rounded.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 120, y: 40, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const target = graph.addNode({ x: 400, y: 260, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) graph.addEdge({ source, target, vertices: [ { x: 200, y: 200 }, { x: 380, y: 120 }, ], // https://x6.antv.vision/zh/docs/api/registry/connector#rounded connector: { name: 'rounded', args: { radius: 20, }, }, attrs: { line: { stroke: '#722ed1', }, }, }) ``` ### 平滑连线(三次贝塞尔曲线) - zh page: https://x6.antv.antgroup.com/zh/examples/edge/connector/smooth/ - en page: https://x6.antv.antgroup.com/en/examples/edge/connector/smooth/ - Source path: examples/edge/connector/demo/smooth.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 120, y: 40, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const target = graph.addNode({ x: 400, y: 260, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) graph.addEdge({ source, target, vertices: [ { x: 200, y: 200 }, { x: 380, y: 120 }, ], // https://x6.antv.vision/zh/docs/api/registry/connector#smooth connector: { name: 'smooth' }, attrs: { line: { stroke: '#722ed1', }, }, }) ``` ### 随机摇摆线 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/connector/wobble/ - en page: https://x6.antv.antgroup.com/en/examples/edge/connector/wobble/ - Source path: examples/edge/connector/demo/wobble.ts ```ts import { Graph, Path, Point } from '@antv/x6' // 帮助文档:https://x6.antv.vision/zh/docs/api/registry/connector#registry Graph.registerConnector( 'wobble', (sourcePoint, targetPoint, vertices, args) => { const spread = args.spread || 20 const points = [...vertices, targetPoint].map((p) => Point.create(p)) let prev = Point.create(sourcePoint) const path = new Path() path.appendSegment(Path.createSegment('M', prev)) for (let i = 0, n = points.length; i < n; i += 1) { const next = points[i] const distance = prev.distance(next) let d = spread while (d < distance) { const current = prev.clone().move(next, -d) current.translate( Math.floor(7 * Math.random()) - 3, Math.floor(7 * Math.random()) - 3, ) path.appendSegment(Path.createSegment('L', current)) d += spread } path.appendSegment(Path.createSegment('L', next)) prev = next } return path }, true, ) const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 120, y: 40, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const target = graph.addNode({ x: 400, y: 260, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) graph.addEdge({ source, target, vertices: [ { x: 200, y: 200 }, { x: 380, y: 120 }, ], connector: { name: 'wobble', args: { spread: 10, }, }, attrs: { line: { stroke: '#722ed1', }, }, }) ``` ### 交错背景色 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/edge/cross-fill/ - en page: https://x6.antv.antgroup.com/en/examples/edge/edge/cross-fill/ - Source path: examples/edge/edge/demo/cross-fill.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addEdge({ source: { x: 320, y: 100 }, target: { x: 380, y: 260 }, vertices: [{ x: 320, y: 200 }], connector: { name: 'rounded' }, markup: [ { tagName: 'path', selector: 'wrap', groupSelector: 'lines', }, { tagName: 'path', selector: 'line1', groupSelector: 'lines', }, { tagName: 'path', selector: 'line2', groupSelector: 'lines', }, { tagName: 'path', selector: 'line3', groupSelector: 'lines', }, ], attrs: { lines: { connection: true, strokeDasharray: '10,20', strokeLinejoin: 'round', fill: 'none', }, line1: { stroke: '#fe854f', strokeWidth: 8, }, line2: { stroke: '#7c68fc', strokeDashoffset: 10, strokeWidth: 8, }, line3: { strokeWidth: 8, strokeDashoffset: 20, stroke: '#73d13d', targetMarker: { tagName: 'path', stroke: '#73d13d', strokeWidth: 2, d: 'M 0 -4 0 -10 -12 0 0 10 0 4', }, }, }, }) ``` ### 自定义点击事件 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/edge/custom-click-event/ - en page: https://x6.antv.antgroup.com/en/examples/edge/edge/custom-click-event/ - Source path: examples/edge/edge/demo/custom-click-event.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.on('edge:customevent', ({ name, e, edge }) => { if (name === 'click:circle' || name === 'click:rect') { e.stopPropagation() const t = edge.attr<number>('c1/atConnectionRatio') > 0.3 ? 0.3 : 0.9 edge.animate( { 'attrs/c1/atConnectionRatio': t, 'attrs/c2/atConnectionRatio': t, }, { delay: 100, duration: 1000, easing: 'ease-in-out-back', fill: 'forwards', }, ) } }) graph.addEdge({ source: { x: 320, y: 60 }, target: { x: 380, y: 300 }, vertices: [{ x: 320, y: 200 }], connector: { name: 'rounded' }, markup: [ { tagName: 'path', selector: 'p1', }, { tagName: 'path', selector: 'p2', }, { tagName: 'rect', selector: 'sign', }, { tagName: 'circle', selector: 'c1', }, { tagName: 'circle', selector: 'c2', }, { tagName: 'text', selector: 'signText', }, ], attrs: { p1: { connection: true, fill: 'none', stroke: '#237804', strokeWidth: 6, strokeLinejoin: 'round', }, p2: { connection: true, fill: 'none', stroke: '#73d13d', strokeWidth: 4, pointerEvents: 'none', strokeLinejoin: 'round', targetMarker: { tagName: 'path', fill: '#73d13d', stroke: '#237804', strokeWidth: 1, d: 'M 10 -3 10 -10 -2 0 10 10 10 3', }, }, sign: { x: -20, y: -10, width: 50, height: 20, stroke: '#237804', fill: '#73d13d', atConnectionLength: 30, strokeWidth: 1, event: 'click:rect', cursor: 'pointer', }, signText: { atConnectionLength: 34, textAnchor: 'middle', textVerticalAnchor: 'middle', text: 'Token', event: 'click:rect', cursor: 'pointer', }, c1: { r: 10, stroke: '#f5222d', fill: '#fe854f', atConnectionRatio: 0.68, strokeWidth: 1, event: 'click:circle', cursor: 'pointer', }, c2: { r: 5, stroke: '#f5222d', fill: 'white', atConnectionRatio: 0.68, strokeWidth: 1, pointerEvents: 'none', }, }, }) ``` ### 默认边 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/edge/edge/ - en page: https://x6.antv.antgroup.com/en/examples/edge/edge/edge/ - Source path: examples/edge/edge/demo/edge.ts ```ts import { Graph } from '@antv/x6' import insertCss from 'insert-css' const container = document.getElementById('container') const graph = new Graph({ container, grid: true, }) graph.addEdge({ source: { x: 240, y: 40 }, target: { x: 280, y: 180 }, vertices: [{ x: 240, y: 140 }], }) graph.addEdge({ source: { x: 340, y: 40 }, target: { x: 380, y: 180 }, vertices: [{ x: 340, y: 140 }], connector: { name: 'rounded', args: { radius: 10 }, }, attrs: { line: { targetMarker: 'classic', stroke: '#f5222d', }, }, }) graph.addEdge({ source: { x: 440, y: 40 }, target: { x: 480, y: 180 }, vertices: [{ x: 440, y: 140 }], connector: { name: 'smooth' }, attrs: { line: { stroke: '#faad14', targetMarker: 'classic', }, }, }) graph.addEdge({ source: { x: 540, y: 40 }, target: { x: 580, y: 180 }, vertices: [{ x: 540, y: 140 }], connector: { name: 'smooth' }, attrs: { line: { stroke: '#1890ff', strokeDasharray: 5, targetMarker: 'classic', style: { animation: 'ant-line 30s infinite linear', }, }, }, }) graph.addEdge({ source: { x: 240, y: 240 }, target: { x: 280, y: 380 }, vertices: [{ x: 240, y: 340 }], router: { name: 'er' }, }) graph.addEdge({ source: { x: 340, y: 240 }, target: { x: 380, y: 380 }, vertices: [{ x: 340, y: 340 }], router: { name: 'orth' }, connector: { name: 'rounded' }, attrs: { line: { targetMarker: 'classic', stroke: '#f5222d', }, }, }) graph.addEdge({ source: { x: 440, y: 240 }, target: { x: 480, y: 380 }, vertices: [{ x: 440, y: 340 }], router: { name: 'manhattan' }, connector: { name: 'rounded' }, attrs: { line: { stroke: '#faad14', targetMarker: 'classic', }, }, }) graph.addEdge({ source: { x: 540, y: 240 }, target: { x: 580, y: 380 }, vertices: [{ x: 540, y: 360 }], router: { name: 'metro' }, attrs: { line: { stroke: '#1890ff', strokeDasharray: 5, targetMarker: 'classic', }, }, }) // 我们用 insert-css 协助demo演示 // 实际项目中只要将下面样式添加到样式文件中 insertCss(` @keyframes ant-line { to { stroke-dashoffset: -1000 } } `) ``` ### 渐变背景色 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/edge/gradient/ - en page: https://x6.antv.antgroup.com/en/examples/edge/edge/gradient/ - Source path: examples/edge/edge/demo/gradient.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addEdge({ source: { x: 320, y: 100 }, target: { x: 380, y: 260 }, vertices: [{ x: 320, y: 200 }], markup: [ { tagName: 'path', selector: 'stroke', }, { tagName: 'path', selector: 'fill', }, ], connector: { name: 'rounded' }, attrs: { fill: { connection: true, strokeWidth: 12, strokeLinecap: 'round', fill: 'none', // 渐变使用教程:https://x6.antv.vision/zh/docs/api/registry/attr#fill stroke: { type: 'linearGradient', stops: [ { offset: '0%', color: '#ccc' }, { offset: '50%', color: '#73d13d' }, { offset: '100%', color: '#ccc' }, ], }, }, stroke: { fill: 'none', stroke: '#237804', connection: true, strokeWidth: 14, strokeLinecap: 'round', }, }, }) ``` ### 沿边长度方向上的图形 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/edge/shape-on-edge/ - en page: https://x6.antv.antgroup.com/en/examples/edge/edge/shape-on-edge/ - Source path: examples/edge/edge/demo/shape-on-edge.ts ```ts import { Graph } from '@antv/x6' Graph.registerEdge( 'arrow', { markup: [ { tagName: 'path', selector: 'wrap', attrs: { fill: 'none', cursor: 'pointer', stroke: 'transparent', }, }, { tagName: 'path', selector: 'line', attrs: { fill: 'none', 'pointer-events': 'none', }, }, { tagName: 'path', groupSelector: 'arrow', selector: 'arrow1', }, { tagName: 'path', groupSelector: 'arrow', selector: 'arrow2', }, { tagName: 'path', groupSelector: 'arrow', selector: 'arrow3', }, ], attrs: { wrap: { connection: true, strokeWidth: 10, strokeLinejoin: 'round', }, line: { connection: true, stroke: '#333333', strokeWidth: 2, strokeLinejoin: 'round', targetMarker: 'classic', }, arrow: { d: 'M 0 -10 10 0 0 10 z', fill: '#ffffff', stroke: '#333333', pointerEvents: 'none', }, arrow1: { // atConnectionRatio: 将边中的指定元素移动到指定比例 [0, 1] 位置处,并自动旋转元素, // 使其方向与所在位置边的斜率保持一致。教程 https://x6.antv.vision/zh/docs/api/registry/attr#atconnectionratiokeepgradient atConnectionRatio: 0.25, }, arrow2: { atConnectionRatio: 0.5, }, arrow3: { atConnectionRatio: 0.75, }, }, }, true, ) const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addEdge({ source: { x: 320, y: 100 }, target: { x: 380, y: 260 }, vertices: [{ x: 320, y: 200 }], shape: 'arrow', connector: { name: 'rounded' }, attrs: { line: { stroke: '#73d13d', strokeWidth: 3, }, arrow: { d: 'M 0 -6 8 0 0 6 z', fill: '#ED8A19', stroke: '#ED8A19', }, }, }) ``` ### 沿路径的 Label - zh page: https://x6.antv.antgroup.com/zh/examples/edge/label/label-along-path/ - en page: https://x6.antv.antgroup.com/en/examples/edge/label/label-along-path/ - Source path: examples/edge/label/demo/label-along-path.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addEdge({ source: { x: 310, y: 100 }, target: { x: 380, y: 260 }, vertices: [{ x: 320, y: 200 }], connector: { name: 'smooth' }, markup: [ { tagName: 'path', selector: 'line', attrs: { fill: 'none', pointerEvents: 'none', }, }, { tagName: 'text', selector: 'label', }, ], attrs: { line: { connection: true, stroke: '#52c41a', strokeWidth: 20, strokeLinejoin: 'round', targetMarker: '', }, label: { // textPath 教程:https://x6.antv.vision/zh/docs/api/registry/attr#textpath textPath: { selector: 'line', startOffset: '50%' }, textAnchor: 'middle', textVerticalAnchor: 'middle', text: 'Label Along Path', fill: '#237804', }, }, }) ``` ### 渲染多个 Label - zh page: https://x6.antv.antgroup.com/zh/examples/edge/label/multi-labels/ - en page: https://x6.antv.antgroup.com/en/examples/edge/label/multi-labels/ - Source path: examples/edge/label/demo/multi-labels.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addEdge({ source: [170, 160], target: [550, 160], attrs: { line: { stroke: '#ccc', }, }, labels: [ // label1 { markup: [ { tagName: 'rect', selector: 'labelBody', }, { tagName: 'text', selector: 'labelText', }, ], attrs: { labelText: { text: 'Label 1', fill: '#ffa940', textAnchor: 'middle', textVerticalAnchor: 'middle', }, labelBody: { ref: 'labelText', refX: -8, refY: -5, refWidth: '100%', refHeight: '100%', refWidth2: 16, refHeight2: 10, stroke: '#ffa940', fill: '#fff', strokeWidth: 2, rx: 5, ry: 5, }, }, position: { distance: 0.3, args: { keepGradient: true, ensureLegibility: true, }, }, }, // label 2 { markup: [ { tagName: 'ellipse', selector: 'labelBody', }, { tagName: 'text', selector: 'labelText', }, ], attrs: { labelText: { text: 'Label 2', fill: '#31d0c6', textAnchor: 'middle', textVerticalAnchor: 'middle', }, labelBody: { ref: 'labelText', refRx: '80%', refRy: '80%', stroke: '#31d0c6', fill: '#fff', strokeWidth: 2, }, }, position: { distance: 0.7, angle: 45, }, }, ], }) ``` ### 使用 HTML 渲染 Label - zh page: https://x6.antv.antgroup.com/zh/examples/edge/label/render-with-html/ - en page: https://x6.antv.antgroup.com/en/examples/edge/label/render-with-html/ - Source path: examples/edge/label/demo/render-with-html.ts ```ts import { Graph, Markup } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, onEdgeLabelRendered: (args) => { const { selectors } = args const content = selectors.foContent as HTMLDivElement if (content) { const btn = document.createElement('button') btn.appendChild(document.createTextNode('HTML Button')) btn.style.width = '100%' btn.style.height = '100%' btn.style.lineHeight = '1' btn.style.borderRadius = '4px' btn.style.textAlign = 'center' btn.style.color = '#000' btn.style.background = '#ffd591' btn.style.border = '2px solid #ffa940' btn.addEventListener('click', () => { alert('clicked') }) content.appendChild(btn) } }, }) graph.addEdge({ source: [170, 160], target: [550, 160], defaultLabel: { markup: Markup.getForeignObjectMarkup(), attrs: { fo: { width: 120, height: 30, x: 60, y: -15, }, }, }, label: { position: 0.25, }, attrs: { line: { stroke: '#ccc', }, }, }) ``` ### 使用 React 渲染 Label - zh page: https://x6.antv.antgroup.com/zh/examples/edge/label/render-with-react/ - en page: https://x6.antv.antgroup.com/en/examples/edge/label/render-with-react/ - Source path: examples/edge/label/demo/render-with-react.tsx ```tsx import { Graph, Markup } from '@antv/x6' import React from 'react' import { createRoot } from 'react-dom/client' class Label extends React.Component { onClick = () => { alert('clicked') } render() { return ( <button style={{ width: '100%', height: '100%', textAlign: 'center', color: '#000', background: '#ffd591', border: '2px solid #ffa940', borderRadius: 4, }} onClick={this.onClick} > React Button </button> ) } } const graph = new Graph({ container: document.getElementById('container'), grid: true, onEdgeLabelRendered: (args) => { const { selectors } = args const content = selectors.foContent as HTMLDivElement if (content) { createRoot(content).render(<Label />) } }, }) graph.addEdge({ source: [170, 160], target: [550, 160], defaultLabel: { markup: Markup.getForeignObjectMarkup(), attrs: { fo: { width: 120, height: 30, x: 60, y: -15, }, }, }, label: { position: 0.25, }, attrs: { line: { stroke: '#ccc', }, }, }) ``` ### 使用 SVG 渲染 Label - zh page: https://x6.antv.antgroup.com/zh/examples/edge/label/render-with-svg/ - en page: https://x6.antv.antgroup.com/en/examples/edge/label/render-with-svg/ - Source path: examples/edge/label/demo/render-with-svg.ts ```ts import { Graph } from '@antv/x6' Graph.registerEdge( 'custom-edge-label', { inherit: 'edge', defaultLabel: { markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'text', selector: 'label', }, ], attrs: { label: { fill: '#000', fontSize: 14, textAnchor: 'middle', textVerticalAnchor: 'middle', pointerEvents: 'none', }, body: { ref: 'label', fill: '#ffd591', stroke: '#ffa940', strokeWidth: 2, rx: 4, ry: 4, refWidth: '140%', refHeight: '140%', refX: '-20%', refY: '-20%', }, }, position: { distance: 200, options: { absoluteDistance: true, reverseDistance: true, }, }, }, }, true, ) const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addEdge({ source: [170, 160], target: [550, 160], shape: 'custom-edge-label', attrs: { line: { stroke: '#ccc', }, }, labels: [ { attrs: { line: { stroke: '#73d13d', }, text: { text: 'Custom Label', }, }, }, ], }) ``` ### 自定义复杂箭头 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/marker/custom-marker/ - en page: https://x6.antv.antgroup.com/en/examples/edge/marker/custom-marker/ - Source path: examples/edge/marker/demo/custom-marker.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) // https://x6.antv.vision/zh/docs/tutorial/intermediate/marker#path graph.addEdge({ source: { x: 240, y: 120 }, target: { x: 420, y: 80 }, vertices: [{ x: 320, y: 170 }], connector: { name: 'rounded' }, attrs: { line: { strokeWidth: 2, stroke: '#52c41a', sourceMarker: { name: 'path', d: 'M5.5,15.499,15.8,21.447,15.8,15.846,25.5,21.447,25.5,9.552,15.8,15.152,15.8,9.552z', }, targetMarker: { name: 'path', offsetX: 10, d: 'M4.834,4.834L4.833,4.833c-5.889,5.892-5.89,15.443,0.001,21.334s15.44,5.888,21.33-0.002c5.891-5.891,5.893-15.44,0.002-21.33C20.275-1.056,10.725-1.056,4.834,4.834zM25.459,5.542c0.833,0.836,1.523,1.757,2.104,2.726l-4.08,4.08c-0.418-1.062-1.053-2.06-1.912-2.918c-0.859-0.859-1.857-1.494-2.92-1.913l4.08-4.08C23.7,4.018,24.622,4.709,25.459,5.542zM10.139,20.862c-2.958-2.968-2.959-7.758-0.001-10.725c2.966-2.957,7.756-2.957,10.725,0c2.954,2.965,2.955,7.757-0.001,10.724C17.896,23.819,13.104,23.817,10.139,20.862zM5.542,25.459c-0.833-0.837-1.524-1.759-2.105-2.728l4.081-4.081c0.418,1.063,1.055,2.06,1.914,2.919c0.858,0.859,1.855,1.494,2.917,1.913l-4.081,4.081C7.299,26.982,6.379,26.292,5.542,25.459zM8.268,3.435l4.082,4.082C11.288,7.935,10.29,8.571,9.43,9.43c-0.858,0.859-1.494,1.855-1.912,2.918L3.436,8.267c0.58-0.969,1.271-1.89,2.105-2.727C6.377,4.707,7.299,4.016,8.268,3.435zM22.732,27.563l-4.082-4.082c1.062-0.418,2.061-1.053,2.919-1.912c0.859-0.859,1.495-1.857,1.913-2.92l4.082,4.082c-0.58,0.969-1.271,1.891-2.105,2.728C24.623,26.292,23.701,26.983,22.732,27.563z', }, }, }, }) ``` ### 使用 image 自定义箭头 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/marker/image-marker/ - en page: https://x6.antv.antgroup.com/en/examples/edge/marker/image-marker/ - Source path: examples/edge/marker/demo/image-marker.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const marker = { tagName: 'image', // https://www.iconfinder.com/icons/15539/arrow_left_previos_icon 'xlink:href': 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFMklEQVRYR+2WaWxUVRiGnzt3lntnptPOdGba6ZRS7ALUsFY2g7SyhKWA0ALKHrBgWBRqQioQKJZdVpHdH5jKokGBgopoBCpbIkvYJIoIspUudGEYmHY6c8eMLCHIojVATLjJ+XfP9z75znve7wg84094xvo8B/jfd8AsQZMq2AP4a+On/9KBZqmyvPpnj+enUhhTG/HgnloB6GFU/8YJM40Gg/mjA0dHK7DiaQHEN0CYkzmke8ao+SOYmTk/MGvrj12A7540gDEE+neJi80ZO/0tZ7v+rwOVZMSkuTddvNIKOPWkAV4a1jBu/3sLummi6xmpqizh/O8VZI3e8UeB60ZroPhJAOiA6juFW5vlrV99k9lDY5BAFDm2+wyjx205dNLnG2AymRJdLtevwJl7QGQguEyACqgCKgDPvbAPMqEzCkY6NdpOB2u8fYHLgKZnXJ19+Wc+bQFBLpk9679k8rsfX7e+3MZrMBrD9+Xnr/W4XMcjjVJilD08tq4zMiY03GTTaWVtQPELJUVlyuWzhZeOFhYVXERZDRy5/xY0jYIBHRvEDewz/NWoUJuRqZPW7T10pXRzS4u5w8ylmd3a9G8D1X7Qacib9AknTonkfr6OJYsXUT/iJimpzTHHRgABwAWuIryuMnzVHnw1ApWlHvZvP8GGlQWVWyuuDQG2CREwPNFoHNa1S6OWKV2StInJDkJC/Cg1XorOlXPxTCUxDevgiLdzo+IqNdfKELwlrFpwmMjkMYyYNY0ZM6bx5tAEHFFq3L8c4dql01Rdr0AjmZBCLaj1JrSGUDSSjE6vpiD/HJPHrT27z+NuFTyCVxJFMXvMoLZpaekRhJo8uCt9+Lw1CHjRaDQoiPj8IKol1JIOQ5jEqnlHCU3sxdD3c8jJzmJAiwtY9T4EKQSjIw5DZCxanRFkAyjgdRXjdZVjtEaD3kBWxyks3nui310PqGB8rzrWKeOyUy3J7euhlqz4FQMBRSEQ8COogj4S8N6oxO8+zcoPjqB/oTudR2WyYtFcsnpL2BwOVDrDrXjz3+piTc1NqsqKCfi9mOomIVscqIUq5o1dw8SNB7LuN2GjeEHMGdYzKSN9UALRCTEIchwIOqrdZfg9blQ6CcF3gSW5uxCdnWiakc6OTWvITtdhDNHj94sEVEGjiqAIBEQBtU6PWjbi93sRUOG7fp4lU75m5s6zfwO4Y8zBaZGm3IG9bHWTW8iExaWgtTVBo9WhUmtwnd7C0tk7uWpuT6u+vfn+szWBpOqTQp1oPXaHCZvDhMlqRh9mxRBuRyOF4PepUNChxsPpfRtZNPs31l9yjX3ULIirhzBncIq9z2v9HMQmp2KMCaauSNnxPJbN2Myuy9HlSZ1TLMcOHjp78PDhhWEQHymKjZ2y2CDSoolwOnRiRIQOq02L3WnBEWPD5ykl/4vz7D+s1Pzgrmz/uGGk0sDbfeuZc9/IkEzNU1rjbDsV9+UCFr+Ty/Sd5XMFWS4ON5uTCwsLB9++f1rACsSroGkYNI4U1UkOvTYxVBYssloreKq07C0vziuBkY8DuBNa7doapeVDu6pf7NivDbb6HViW/SFTtl+Z44WJt9PuJuB7SCTrATvQUA/NvODzQR5Q9E8BgnWj6wvi8mGdQnr0Tjfw7Q6FCZuvzPfChNrOgdq8B3QWmD0+1ZJlCtEwaVvxspsw9mkC/KUlw/jeDvvC3cVXNxQqysCnDhAUlGCkXaVqcUFRRj3i7B/L9m888KBiDqAM8D5W6SE//FeA2ure3fcc4HkH/gSNF7Qt0mUkowAAAABJRU5ErkJggg==', width: 32, height: 32, x: -16, y: -16, } graph.addEdge({ source: { x: 240, y: 120 }, target: { x: 420, y: 80 }, vertices: [{ x: 320, y: 170 }], connector: { name: 'rounded' }, attrs: { line: { stroke: '#653433', sourceMarker: { ...marker }, targetMarker: { ...marker }, }, }, }) ``` ### 内置箭头 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/marker/native-markers/ - en page: https://x6.antv.antgroup.com/en/examples/edge/marker/native-markers/ - Source path: examples/edge/marker/demo/native-markers.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) // 内置箭头:https://x6.antv.vision/zh/docs/tutorial/intermediate/marker#%E5%86%85%E7%BD%AE%E7%AE%AD%E5%A4%B4 const markers = [ ['block', { size: 6 }], ['classic', { size: 6 }], ['diamond', { size: 8 }], ['circle', { size: 6 }], ['circlePlus', { size: 6 }], ['ellipse', { rx: 6, ry: 4 }], ['cross', { size: 8, offset: 1 }], ['async', { size: 8, offset: 1 }], ] markers.forEach(([marker, args], i) => { graph.addEdge({ sourcePoint: [220, 30 + i * 40], targetPoint: [500, 30 + i * 40], label: marker, attrs: { line: { sourceMarker: { args, name: marker, }, targetMarker: { args, name: marker, }, strokeWidth: 1, }, }, }) }) ``` ### 使用 path 自定义箭头 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/marker/path-marker/ - en page: https://x6.antv.antgroup.com/en/examples/edge/marker/path-marker/ - Source path: examples/edge/marker/demo/path-marker.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addEdge({ source: { x: 240, y: 120 }, target: { x: 420, y: 80 }, vertices: [{ x: 320, y: 170 }], connector: { name: 'rounded' }, attrs: { line: { sourceMarker: { tagName: 'path', d: 'M 20 -10 0 0 20 10 Z', }, targetMarker: { tagName: 'path', d: 'M 20 -10 0 0 20 10 Z', strokeWidth: 2, fill: '#73d13d', stroke: '#237804', }, }, }, }) ``` ### 使用图形元素自定义箭头 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/marker/shape-marker/ - en page: https://x6.antv.antgroup.com/en/examples/edge/marker/shape-marker/ - Source path: examples/edge/marker/demo/shape-marker.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addEdge({ source: { x: 240, y: 120 }, target: { x: 420, y: 80 }, vertices: [{ x: 320, y: 170 }], connector: { name: 'rounded' }, attrs: { line: { sourceMarker: { tagName: 'circle', r: 12, cx: 12, fill: '#ffd591', stroke: '#ffa940', }, targetMarker: { tagName: 'ellipse', rx: 20, ry: 10, cx: 20, fill: '#73d13d', stroke: '#237804', }, }, }, }) ``` ### 自定义随机路由 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/router/custom-random-router/ - en page: https://x6.antv.antgroup.com/en/examples/edge/router/custom-random-router/ - Source path: examples/edge/router/demo/custom-random-router.ts ```ts import { Graph, Point } from '@antv/x6' Graph.registerRouter( 'random', (vertices, args, view) => { const BOUNCES = args.bounces || 20 const points = vertices.map((p) => Point.create(p)) for (let i = 0; i < BOUNCES; i += 1) { const sourceCorner = view.sourceBBox.getCenter() const targetCorner = view.targetBBox.getCenter() const randomPoint = Point.random( sourceCorner.x, targetCorner.x, sourceCorner.y, targetCorner.y, ) points.push(randomPoint) } return points }, true, ) const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 120, y: 40, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const target = graph.addNode({ x: 400, y: 260, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) graph.addEdge({ source, target, router: { name: 'random', args: { bounces: 3, }, }, attrs: { line: { stroke: '#722ed1', }, }, }) ``` ### 关系路由 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/router/er-center/ - en page: https://x6.antv.antgroup.com/en/examples/edge/router/er-center/ - Source path: examples/edge/router/demo/er-center.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 120, y: 40, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const target = graph.addNode({ x: 400, y: 260, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) graph.addEdge({ source, target, // https://x6.antv.antgroup.com/api/registry/router#er router: { name: 'er', args: { offset: 'center', }, }, attrs: { line: { stroke: '#722ed1', }, }, }) ``` ### 关系路由 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/router/er-normal/ - en page: https://x6.antv.antgroup.com/en/examples/edge/router/er-normal/ - Source path: examples/edge/router/demo/er-normal.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 120, y: 40, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const target = graph.addNode({ x: 400, y: 260, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) graph.addEdge({ source, target, // https://x6.antv.antgroup.com/api/registry/router#er router: { name: 'er', args: { offset: 40, }, }, attrs: { line: { stroke: '#722ed1', }, }, }) ``` ### 智能路由 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/router/manhattan/ - en page: https://x6.antv.antgroup.com/en/examples/edge/router/manhattan/ - Source path: examples/edge/router/demo/manhattan.ts ```ts import { Graph, Node, Edge, EdgeView } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 120, y: 40, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const target = graph.addNode({ x: 400, y: 260, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const obstacles: Node[] = [] const obstacle = graph.addNode({ x: 240, y: 120, width: 80, height: 30, attrs: { body: { fill: '#ffd591', stroke: '#ffa940', }, }, }) let edge: Edge const update = () => { const edgeView = graph.findViewByCell(edge) as EdgeView edgeView.update() } obstacles.push(obstacle) obstacles.push(graph.addNode(obstacle.clone().translate(100, 80))) obstacles.push(graph.addNode(obstacle.clone().translate(-100, 140))) obstacles.forEach((obstacle) => obstacle.on('change:position', update)) edge = graph.addEdge({ source, target, router: { name: 'manhattan', args: { startDirections: ['bottom'], endDirections: ['top'], }, }, attrs: { line: { stroke: '#722ed1', }, }, }) update() graph.translate(50, 0) ``` ### 智能地铁路由 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/router/metro/ - en page: https://x6.antv.antgroup.com/en/examples/edge/router/metro/ - Source path: examples/edge/router/demo/metro.ts ```ts import { Graph, Node, Edge, EdgeView } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 120, y: 40, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const target = graph.addNode({ x: 400, y: 260, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const obstacles: Node[] = [] const obstacle = graph.addNode({ x: 240, y: 120, width: 80, height: 30, attrs: { body: { fill: '#ffd591', stroke: '#ffa940', }, }, }) let edge: Edge const update = () => { const edgeView = graph.findViewByCell(edge) as EdgeView edgeView.update() } obstacles.push(obstacle) obstacles.push(graph.addNode(obstacle.clone().translate(100, 80))) obstacles.push(graph.addNode(obstacle.clone().translate(-100, 140))) obstacles.forEach((obstacle) => obstacle.on('change:position', update)) edge = graph.addEdge({ source, target, router: { name: 'metro', args: { startDirections: ['bottom'], endDirections: ['top'], }, }, attrs: { line: { stroke: '#722ed1', }, }, }) ``` ### 默认路由 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/router/normal/ - en page: https://x6.antv.antgroup.com/en/examples/edge/router/normal/ - Source path: examples/edge/router/demo/normal.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 120, y: 40, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const target = graph.addNode({ x: 400, y: 260, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) graph.addEdge({ source, target, vertices: [ { x: 200, y: 200 }, { x: 380, y: 120 }, ], // https://x6.antv.antgroup.com/api/registry/router#normal router: { name: 'normal', }, attrs: { line: { stroke: '#722ed1', }, }, }) ``` ### 单侧路由 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/router/one-side/ - en page: https://x6.antv.antgroup.com/en/examples/edge/router/one-side/ - Source path: examples/edge/router/demo/one-side.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 120, y: 40, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const target = graph.addNode({ x: 400, y: 260, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) graph.addEdge({ source, target, // https://x6.antv.antgroup.com/api/registry/router#oneside router: { name: 'oneSide', args: { side: 'right', padding: 50, }, }, attrs: { line: { stroke: '#722ed1', }, }, }) ``` ### 拐线 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/router/orth-normal/ - en page: https://x6.antv.antgroup.com/en/examples/edge/router/orth-normal/ - Source path: examples/edge/router/demo/orth-normal.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 120, y: 40, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const target = graph.addNode({ x: 400, y: 260, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) graph.addEdge({ source, target, // https://x6.antv.antgroup.com/api/registry/router#orth router: { name: 'orth', }, attrs: { line: { stroke: '#722ed1', }, }, }) ``` ### 正交线 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/router/orth-vertices/ - en page: https://x6.antv.antgroup.com/en/examples/edge/router/orth-vertices/ - Source path: examples/edge/router/demo/orth-vertices.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 120, y: 40, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) const target = graph.addNode({ x: 400, y: 260, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', }, }, }) graph.addEdge({ source, target, vertices: [ { x: 200, y: 200 }, { x: 380, y: 120 }, ], // https://x6.antv.antgroup.com/api/registry/router#orth router: { name: 'orth', }, attrs: { line: { stroke: '#722ed1', }, }, }) ``` ### 调整箭头 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/tool/arrowheads/ - en page: https://x6.antv.antgroup.com/en/examples/edge/tool/arrowheads/ - Source path: examples/edge/tool/demo/arrowheads.ts ```ts import { Graph, Shape } from '@antv/x6' Shape.Rect.config({ attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, ports: { groups: { in: { position: { name: 'top' }, }, out: { position: { name: 'bottom' }, }, }, }, portMarkup: [ { tagName: 'circle', selector: 'portBody', attrs: { r: 5, magnet: true, stroke: '#31d0c6', fill: '#fff', strokeWidth: 2, }, }, ], }) const magnetAvailabilityHighlighter = { name: 'stroke', args: { padding: 3, attrs: { strokeWidth: 3, stroke: '#52c41a', }, }, } const graph = new Graph({ container: document.getElementById('container'), grid: true, highlighting: { magnetAvailable: magnetAvailabilityHighlighter, }, connecting: { snap: true, allowBlank: false, allowLoop: false, allowNode: false, highlight: true, validateMagnet({ magnet }) { return magnet.getAttribute('port-group') !== 'in' }, validateConnection({ sourceMagnet, targetMagnet }) { // 只能从输出连接桩创建连接 if (!sourceMagnet || sourceMagnet.getAttribute('port-group') === 'in') { return false } // 只能连接到输入连接桩 if (!targetMagnet || targetMagnet.getAttribute('port-group') !== 'in') { return false } return true }, }, }) const source = graph.addNode({ x: 40, y: 40, width: 100, height: 40, ports: [ { id: 'in-1', group: 'in' }, { id: 'in-2', group: 'in' }, { id: 'out-1', group: 'out' }, { id: 'out-2', group: 'out' }, ], }) const target = graph.addNode({ x: 140, y: 240, width: 100, height: 40, ports: [ { id: 'in-1', group: 'in' }, { id: 'in-2', group: 'in' }, { id: 'out-1', group: 'out' }, { id: 'out-2', group: 'out' }, ], }) graph.addNode({ x: 320, y: 120, width: 100, height: 40, ports: [ { id: 'in-1', group: 'in' }, { id: 'in-2', group: 'in' }, { id: 'out-1', group: 'out' }, { id: 'out-2', group: 'out' }, ], }) graph.addEdge({ source: { cell: source.id, port: 'out-2' }, target: { cell: target.id, port: 'in-1' }, }) graph.on('edge:mouseenter', ({ cell }) => { cell.addTools([ { name: 'source-arrowhead', }, { name: 'target-arrowhead', args: { attrs: { fill: 'red', }, }, }, ]) }) graph.on('edge:mouseleave', ({ cell }) => { cell.removeTools() }) ``` ### 包围框 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/tool/boundary/ - en page: https://x6.antv.antgroup.com/en/examples/edge/tool/boundary/ - Source path: examples/edge/tool/demo/boundary.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#3c4260', strokeWidth: 2, targetMarker: 'classic', }, }, tools: { name: 'boundary', }, }) graph.addEdge({ source: { x: 40, y: 160 }, target: { x: 380, y: 160 }, vertices: [ { x: 40, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 160 }, ], attrs: { line: { stroke: '#3c4260', strokeWidth: 2, targetMarker: 'classic', }, }, connector: 'smooth', tools: { name: 'boundary', args: { padding: 5, attrs: { fill: '#7c68fc', stroke: '#333', strokeWidth: 0.5, fillOpacity: 0.2, }, }, }, }) ``` ### 自定义按钮 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/tool/button/ - en page: https://x6.antv.antgroup.com/en/examples/edge/tool/button/ - Source path: examples/edge/tool/demo/button.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#3c4260', strokeWidth: 2, targetMarker: 'classic', }, }, tools: [ { name: 'button', args: { markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 18, stroke: '#fe854f', strokeWidth: 2, fill: 'white', cursor: 'pointer', }, }, { tagName: 'text', textContent: 'Btn B', selector: 'icon', attrs: { fill: '#fe854f', fontSize: 10, textAnchor: 'middle', pointerEvents: 'none', y: '0.3em', }, }, ], distance: -40, onClick({ view }: any) { const edge = view.cell const source = edge.getSource() const target = edge.getTarget() edge.setSource(target) edge.setTarget(source) }, }, }, ], }) graph.addEdge({ source: { x: 40, y: 160 }, target: { x: 380, y: 160 }, vertices: [ { x: 40, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 160 }, ], attrs: { line: { stroke: '#3c4260', strokeWidth: 2, targetMarker: 'classic', }, }, connector: 'smooth', tools: { name: 'button', args: { markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 18, stroke: '#fe854f', strokeWidth: 2, fill: 'white', cursor: 'pointer', }, }, { tagName: 'text', textContent: 'Btn A', selector: 'icon', attrs: { fill: '#fe854f', fontSize: 10, textAnchor: 'middle', pointerEvents: 'none', y: '0.3em', }, }, ], distance: -100, offset: { x: 0, y: 0 }, onClick({ view }: any) { const edge = view.cell edge.attr({ line: { strokeDasharray: '5, 10', strokeDashoffset: (edge.attr('line/strokeDashoffset') | 0) + 20, }, }) }, }, }, }) ``` ### 删除按钮 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/tool/button-remove/ - en page: https://x6.antv.antgroup.com/en/examples/edge/tool/button-remove/ - Source path: examples/edge/tool/demo/button-remove.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#3c4260', strokeWidth: 2, targetMarker: 'classic', }, }, tools: { name: 'button-remove', args: { distance: -40 }, }, }) graph.addEdge({ source: { x: 40, y: 160 }, target: { x: 380, y: 160 }, vertices: [ { x: 40, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 160 }, ], attrs: { line: { stroke: '#3c4260', strokeWidth: 2, targetMarker: 'classic', }, }, connector: 'smooth', tools: { name: 'button-remove', args: { distance: -40 }, }, }) ``` ### 右键菜单 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/tool/context-menu/ - en page: https://x6.antv.antgroup.com/en/examples/edge/tool/context-menu/ - Source path: examples/edge/tool/demo/context-menu.tsx ```tsx import { Edge, Graph, Shape } from '@antv/x6' import { Dropdown } from 'antd' import type { MenuProps } from 'antd' import React, { useState, useEffect, useRef, useCallback } from 'react' import { createRoot } from 'react-dom/client' import type { Root } from 'react-dom/client' // 如果项目中使用的是 antd@4,使用方式参考:https://codesandbox.io/s/contextmenu-z8gpq3 // 2.x的使用可以参考2.x文档: https://x6-v2.antv.vision/examples/edge/tool/#context-menu Shape.Rect.config({ attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, ports: { groups: { in: { position: { name: 'top' } }, out: { position: { name: 'bottom' } }, }, }, portMarkup: [ { tagName: 'circle', selector: 'portBody', attrs: { r: 5, magnet: true, stroke: '#31d0c6', fill: '#fff', strokeWidth: 2, }, }, ], }) const magnetAvailabilityHighlighter = { name: 'stroke', args: { padding: 3, attrs: { strokeWidth: 3, stroke: '#52c41a', }, }, } // 工具函数:检查点击是否在 dropdown 内 const isClickInDropdown = (target: HTMLElement): boolean => { const checkElement = (el: HTMLElement | null): boolean => { if (!el || el === document.body) return false const className = el.className?.toString() || '' if ( className.includes('ant-dropdown') || className.includes('ant-dropdown-menu') ) { return true } return checkElement(el.parentElement) } return checkElement(target) } // 右键菜单组件 interface ContextMenuProps { x: number y: number cell: Edge onClose: () => void } const ContextMenu: React.FC<ContextMenuProps> = ({ x, y, cell, onClose }) => { const [visible, setVisible] = useState(true) const containerRef = useRef<HTMLDivElement>(null) // 关闭菜单 const handleClose = useCallback(() => { setVisible(false) // 异步调用 onClose,避免同步 unmount Promise.resolve().then(onClose) }, [onClose]) // 处理菜单项点击 const handleMenuClick: MenuProps['onClick'] = useCallback(({ key }) => { const actions: Record<string, () => void> = { 'color-blue': () => cell.setAttrs({ line: { stroke: '#1890ff' } }), 'color-green': () => cell.setAttrs({ line: { stroke: '#52c41a' } }), 'color-red': () => cell.setAttrs({ line: { stroke: '#ff4d4f' } }), 'style-solid': () => cell.setAttrs({ line: { strokeDasharray: '' } }), 'style-dashed': () => cell.setAttrs({ line: { strokeDasharray: '5 5' } }), 'delete': () => cell.remove(), } actions[key]?.() handleClose() }, [cell, handleClose]) // 监听外部点击 useEffect(() => { if (!visible) return const handleClickOutside = (e: MouseEvent) => { const target = e.target as HTMLElement const isInContainer = containerRef.current?.contains(target) const isInDropdown = isClickInDropdown(target) if (!isInContainer && !isInDropdown) { handleClose() } } // 延迟添加监听器,避免立即触发 const timer = setTimeout(() => { document.addEventListener('mousedown', handleClickOutside, true) }, 200) return () => { clearTimeout(timer) document.removeEventListener('mousedown', handleClickOutside, true) } }, [visible, handleClose]) // 菜单配置 const menuItems: MenuProps['items'] = [ { key: 'changeColor', label: '修改颜色', children: [ { key: 'color-blue', label: '蓝色' }, { key: 'color-green', label: '绿色' }, { key: 'color-red', label: '红色' }, ], }, { key: 'changeStyle', label: '修改样式', children: [ { key: 'style-solid', label: '实线' }, { key: 'style-dashed', label: '虚线' }, ], }, { type: 'divider' }, { key: 'delete', label: '删除', danger: true }, ] return ( <div ref={containerRef} style={{ position: 'fixed', left: x, top: y, zIndex: 9999, }} > <Dropdown open={visible} onOpenChange={(open) => !open && handleClose()} menu={{ items: menuItems, onClick: handleMenuClick }} trigger={[]} getPopupContainer={() => document.body} > <span style={{ display: 'inline-block', width: 0, height: 0 }} /> </Dropdown> </div> ) } // Graph 配置 const graph = new Graph({ container: document.getElementById('container')!, grid: true, highlighting: { magnetAvailable: magnetAvailabilityHighlighter, }, connecting: { snap: true, allowBlank: false, allowLoop: false, allowNode: false, highlight: true, validateMagnet({ magnet }) { return magnet.getAttribute('port-group') !== 'in' }, validateConnection({ sourceMagnet, targetMagnet }) { if (!sourceMagnet || sourceMagnet.getAttribute('port-group') === 'in') { return false } if (!targetMagnet || targetMagnet.getAttribute('port-group') !== 'in') { return false } return true }, }, }) // 添加节点和边 const source = graph.addNode({ x: 40, y: 40, width: 100, height: 40, ports: [{ id: 'out-2', group: 'out' }], }) const target = graph.addNode({ x: 140, y: 240, width: 100, height: 40, ports: [{ id: 'in-1', group: 'in' }], }) graph.addEdge({ source: { cell: source.id, port: 'out-2' }, target: { cell: target.id, port: 'in-1' }, }) // 菜单管理器 class MenuManager { private root: Root | null = null private container: HTMLDivElement | null = null private isCleaningUp = false cleanup = () => { if (this.isCleaningUp) return this.isCleaningUp = true requestAnimationFrame(() => { try { this.root?.unmount() this.container?.parentNode?.removeChild(this.container) } catch (error) { console.error('Error cleaning up menu:', error) } finally { this.root = null this.container = null this.isCleaningUp = false } }) } show = (x: number, y: number, cell: Edge) => { this.cleanup() requestAnimationFrame(() => { this.container = document.createElement('div') document.body.appendChild(this.container) this.root = createRoot(this.container) this.root.render( <ContextMenu x={x} y={y} cell={cell} onClose={this.cleanup} /> ) }) } } const menuManager = new MenuManager() // 监听边的右键菜单事件 graph.on('edge:contextmenu', ({ cell, e }: { cell: Edge; e: MouseEvent }) => { e.preventDefault() e.stopPropagation() menuManager.show(e.clientX, e.clientY, cell) }) ``` ### 调整线段 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/tool/segments/ - en page: https://x6.antv.antgroup.com/en/examples/edge/tool/segments/ - Source path: examples/edge/tool/demo/segments.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#3c4260', strokeWidth: 2, targetMarker: 'classic', }, }, tools: { name: 'segments', args: { snapRadius: 20, attrs: { fill: '#444', }, }, }, }) graph.addEdge({ source: { x: 40, y: 160 }, target: { x: 380, y: 160 }, vertices: [ { x: 40, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 160 }, ], attrs: { line: { stroke: '#3c4260', strokeWidth: 2, targetMarker: 'classic', }, }, connector: 'smooth', tools: { name: 'segments', args: { snapRadius: 20, attrs: { fill: '#444', }, }, }, }) ``` ### Tooltip - zh page: https://x6.antv.antgroup.com/zh/examples/edge/tool/tooltip/ - en page: https://x6.antv.antgroup.com/en/examples/edge/tool/tooltip/ - Source path: examples/edge/tool/demo/tooltip.tsx ```tsx import { Edge, Graph, Shape } from '@antv/x6' import React from 'react' import { createRoot } from 'react-dom/client' import type { Root } from 'react-dom/client' import { Tooltip } from 'antd' // 2.x的使用可以参考2.x文档: https://x6-v2.antv.vision/examples/edge/tool/#tooltip Shape.Rect.config({ attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, ports: { groups: { in: { position: { name: 'top' }, }, out: { position: { name: 'bottom' }, }, }, }, portMarkup: [ { tagName: 'circle', selector: 'portBody', attrs: { r: 5, magnet: true, stroke: '#31d0c6', fill: '#fff', strokeWidth: 2, }, }, ], }) const magnetAvailabilityHighlighter = { name: 'stroke', args: { padding: 3, attrs: { strokeWidth: 3, stroke: '#52c41a', }, }, } const graph = new Graph({ container: document.getElementById('container'), grid: true, highlighting: { magnetAvailable: magnetAvailabilityHighlighter, }, connecting: { snap: true, allowBlank: false, allowLoop: false, allowNode: false, highlight: true, validateMagnet({ magnet }) { return magnet.getAttribute('port-group') !== 'in' }, validateConnection({ sourceMagnet, targetMagnet }) { // 只能从输出连接桩创建连接 if (!sourceMagnet || sourceMagnet.getAttribute('port-group') === 'in') { return false } // 只能连接到输入连接桩 if (!targetMagnet || targetMagnet.getAttribute('port-group') !== 'in') { return false } return true }, }, }) const source = graph.addNode({ x: 40, y: 40, width: 100, height: 40, ports: [{ id: 'out-2', group: 'out' }], }) const target = graph.addNode({ x: 140, y: 240, width: 100, height: 40, ports: [{ id: 'in-1', group: 'in' }], }) graph.addEdge({ source: { cell: source.id, port: 'out-2' }, target: { cell: target.id, port: 'in-1' }, }) let tooltipRoot: Root | null = null let tooltipContainer: HTMLDivElement | null = null let currentEdgeId: string | null = null // 显示 Tooltip function showTooltip(cell: Edge, x: number, y: number) { // 如果是同一条边,只更新位置 if (currentEdgeId === cell.id && tooltipContainer) { tooltipContainer.style.left = `${x + 6}px` tooltipContainer.style.top = `${y - 6}px` return } // 隐藏之前的 tooltip hideTooltip() currentEdgeId = cell.id // 创建 tooltip 容器 tooltipContainer = document.createElement('div') tooltipContainer.style.cssText = ` position: fixed; left: ${x + 6}px; top: ${y - 6}px; z-index: 9999; pointer-events: none; ` document.body.appendChild(tooltipContainer) // 获取边的信息 const sourceNode = graph.getCellById(cell.getSourceCellId()) const targetNode = graph.getCellById(cell.getTargetCellId()) // Tooltip 内容 const tooltipContent = ( <div> <div> <strong>边信息</strong> </div> <div>ID: {cell.id}</div> <div>源节点: {sourceNode?.id || 'Unknown'}</div> <div>目标节点: {targetNode?.id || 'Unknown'}</div> </div> ) // 渲染 Tooltip tooltipRoot = createRoot(tooltipContainer) tooltipRoot.render( React.createElement( Tooltip, { open: true, title: tooltipContent, placement: 'right', }, React.createElement('span', { style: { display: 'inline-block', width: 0, height: 0 }, }), ), ) } // 隐藏 Tooltip function hideTooltip() { if (tooltipRoot) { tooltipRoot.unmount() tooltipRoot = null } if (tooltipContainer && tooltipContainer.parentNode) { tooltipContainer.parentNode.removeChild(tooltipContainer) tooltipContainer = null } currentEdgeId = null } // 监听边的 hover 事件 graph.on('edge:mouseenter', ({ cell, e }: { cell: Edge; e: MouseEvent }) => { showTooltip(cell, e.clientX, e.clientY) }) graph.on('edge:mouseleave', () => { hideTooltip() }) ``` ### 调整顶点 - zh page: https://x6.antv.antgroup.com/zh/examples/edge/tool/vertices/ - en page: https://x6.antv.antgroup.com/en/examples/edge/tool/vertices/ - Source path: examples/edge/tool/demo/vertices.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, onToolItemCreated({ name, cell, tool }) { if (name === 'vertices' && cell === edge2) { const options = (tool as any).options if (options && options.index % 2 === 1) { tool.setAttrs({ fill: 'red' }) } } }, }) graph.addEdge({ source: { x: 40, y: 40 }, target: { x: 380, y: 40 }, vertices: [ { x: 40, y: 80 }, { x: 200, y: 80 }, { x: 200, y: 40 }, ], attrs: { line: { stroke: '#3c4260', strokeWidth: 2, targetMarker: 'classic', }, }, tools: { name: 'vertices', args: { attrs: { fill: '#666' }, }, }, }) const edge2 = graph.addEdge({ source: { x: 40, y: 160 }, target: { x: 380, y: 160 }, vertices: [ { x: 40, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 160 }, ], connector: 'smooth', attrs: { line: { stroke: '#3c4260', strokeWidth: 2, targetMarker: 'classic', }, }, }) graph.on('edge:mouseenter', ({ cell }) => { if (cell === edge2) { cell.addTools('vertices', 'onhover') } }) graph.on('edge:mouseleave', ({ cell }) => { if (cell.hasTools('onhover')) { cell.removeTools() } }) ``` ### 环形布局 - zh page: https://x6.antv.antgroup.com/zh/examples/layout/general/circle/ - en page: https://x6.antv.antgroup.com/en/examples/layout/general/circle/ - Source path: examples/layout/general/demo/circle.ts ```ts import { Graph, Model } from '@antv/x6' import { CircularLayout } from '@antv/layout' const data: Model.FromJSONData = { nodes: [], } for (let i = 1; i <= 16; i++) { data.nodes!.push({ id: `${i}`, shape: 'circle', width: 30, height: 30, label: `${i}`, attrs: { body: { stroke: 'transparent', fill: '#5F95FF', }, label: { fill: '#fff', }, }, }) } const graph = new Graph({ container: document.getElementById('container')!, }) const circularLayout = new CircularLayout({ type: 'circular', center: [350, 250], }) const model = circularLayout.layout(data) graph.fromJSON(model) ``` ### 同心圆布局 - zh page: https://x6.antv.antgroup.com/zh/examples/layout/general/concentric-circles/ - en page: https://x6.antv.antgroup.com/en/examples/layout/general/concentric-circles/ - Source path: examples/layout/general/demo/concentric-circles.ts ```ts import { Graph, Model } from '@antv/x6' const data: Model.FromJSONData = { nodes: [], edges: [], } // 中心节点 const centerNode = { id: 'center', shape: 'circle', width: 50, height: 50, label: '核心', attrs: { body: { stroke: '#5F95FF', fill: '#5F95FF', strokeWidth: 2, }, label: { fill: '#fff', fontSize: 13, fontWeight: 'bold', }, }, } data.nodes!.push(centerNode) // 第一层节点(内圈) const innerNodes = ['A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8'] innerNodes.forEach((label, index) => { data.nodes!.push({ id: `inner-${index}`, shape: 'circle', width: 40, height: 40, label: label, attrs: { body: { stroke: '#5F95FF', fill: '#5F95FF', strokeWidth: 2, }, label: { fill: '#fff', fontSize: 12, }, }, }) // 连接到中心节点 data.edges!.push({ id: `edge-center-inner-${index}`, source: 'center', target: `inner-${index}`, attrs: { line: { stroke: '#d9d9d9', strokeWidth: 1, }, }, }) }) // 第二层节点(外圈) const outerNodes = [ 'B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8', 'B9', 'B10', 'B11', 'B12', 'B13', 'B14', 'B15', 'B16', ] outerNodes.forEach((label, index) => { data.nodes!.push({ id: `outer-${index}`, shape: 'circle', width: 30, height: 30, label: label, attrs: { body: { stroke: '#5F95FF', fill: '#5F95FF', strokeWidth: 2, }, label: { fill: '#fff', fontSize: 10, }, }, }) }) const graph = new Graph({ container: document.getElementById('container')!, }) // 同心圆布局位置 const center = [300, 300] // 设置各层节点的位置 const nodeMap = new Map(data.nodes!.map((node) => [node.id, node])) const DEG_TO_RAD = Math.PI / 180 const INNER_RADIUS = 110 const OUTER_RADIUS = 260 const positionNodesOnCircle = ( prefix: string, count: number, radius: number, ) => { for (let i = 0; i < count; i++) { const node = nodeMap.get(`${prefix}-${i}`) if (node) { const angle = ((i * 360) / count) * DEG_TO_RAD Object.assign(node, { x: center[0] + radius * Math.cos(angle), y: center[1] + radius * Math.sin(angle), }) } } } // 中心节点 const centerNodeData = nodeMap.get('center') if (centerNodeData) { Object.assign(centerNodeData, { x: center[0], y: center[1], }) } // 内层节点 positionNodesOnCircle('inner', innerNodes.length, INNER_RADIUS) // 外层节点 positionNodesOnCircle('outer', outerNodes.length, OUTER_RADIUS) // 渲染图形 graph.fromJSON(data) ``` ### Dagre - zh page: https://x6.antv.antgroup.com/zh/examples/layout/general/dagre/ - en page: https://x6.antv.antgroup.com/en/examples/layout/general/dagre/ - Source path: examples/layout/general/demo/dagre.ts ```ts import { Graph, Model } from '@antv/x6' import { DagreLayout } from '@antv/layout' const data: Model.FromJSONData = { nodes: [], edges: [], } const edges = [ ['1', '2'], ['2', '3'], ['2', '4'], ['4', '5'], ['4', '6'], ['4', '7'], ['4', '8'], ['5', '9'], ['6', '10'], ['7', '11'], ['8', '12'], ] for (let i = 1; i <= 12; i++) { data.nodes!.push({ id: `${i}`, shape: 'circle', width: 32, height: 32, label: i, attrs: { body: { fill: '#5F95FF', stroke: 'transparent', }, label: { fill: '#ffffff', }, }, }) } edges.forEach((edge: [string, string]) => { data.edges!.push({ source: edge[0], target: edge[1], attrs: { line: { stroke: '#A2B1C3', strokeWidth: 2, }, }, }) }) const graph = new Graph({ container: document.getElementById('container')!, }) const dagreLayout = new DagreLayout({ type: 'dagre', rankdir: 'LR', align: 'UR', ranksep: 35, nodesep: 15, }) const model = dagreLayout.layout(data) graph.fromJSON(model) ``` ### 椭圆布局 - zh page: https://x6.antv.antgroup.com/zh/examples/layout/general/ellipse/ - en page: https://x6.antv.antgroup.com/en/examples/layout/general/ellipse/ - Source path: examples/layout/general/demo/ellipse.ts ```ts import { Graph, Node, Point } from '@antv/x6' const nodes: Node.Metadata[] = [] for (let i = 1; i <= 20; i++) { nodes!.push({ id: `${i}`, shape: 'path', width: 26, height: 26, attrs: { body: { d: 'M0,-9.898961565145173L2.222455340918111,-3.0589473502942863L9.41447190108659,-3.058947350294287L3.596008280084239,1.1684139180159865L5.818463621002351,8.008428132866873L4.440892098500626e-16,3.7810668645565997L-5.8184636210023495,8.008428132866873L-3.5960082800842383,1.1684139180159867L-9.41447190108659,-3.058947350294285L-2.2224553409181116,-3.058947350294286Z', fill: '#EFF4FF', stroke: '#5F95FF', }, }, }) } function layout(nodes: Node.Metadata[]) { const cx = 320 const cy = 180 const rx = 200 const ry = 120 const ratio = rx / ry const center = new Point(cx, cy) const start = new Point(cx, cy - ry) const stepAngle = 360 / nodes.length nodes.forEach((node: Node.Metadata, index: number) => { const angle = stepAngle * index const p = start .clone() .rotate(-angle, center) .scale(ratio, 1, center) .round() node.x = p.x node.y = p.y }) return nodes } const graph = new Graph({ container: document.getElementById('container')!, }) graph.fromJSON(layout(nodes)) ``` ### 力导向布局 - zh page: https://x6.antv.antgroup.com/zh/examples/layout/general/force/ - en page: https://x6.antv.antgroup.com/en/examples/layout/general/force/ - Source path: examples/layout/general/demo/force.ts ```ts import { ForceLayout, type Edge as LayoutEdge, type OutNode as LayoutNode, } from '@antv/layout' import { FunctionExt, Graph, type Model, type Node } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container')!, }) const originData: { nodes: LayoutNode[] edges: LayoutEdge[] } = { nodes: [ { id: 'node0', size: 50, x: 0, y: 0 }, { id: 'node1', size: 30, x: 0, y: 0 }, { id: 'node2', size: 30, x: 0, y: 0 }, { id: 'node3', size: 30, x: 0, y: 0 }, { id: 'node4', size: 30, x: 0, y: 0 }, { id: 'node5', size: 30, x: 0, y: 0 }, { id: 'node6', size: 15, x: 0, y: 0 }, { id: 'node7', size: 15, x: 0, y: 0 }, { id: 'node8', size: 15, x: 0, y: 0 }, { id: 'node9', size: 15, x: 0, y: 0 }, { id: 'node10', size: 15, x: 0, y: 0 }, { id: 'node11', size: 15, x: 0, y: 0 }, { id: 'node12', size: 15, x: 0, y: 0 }, { id: 'node13', size: 15, x: 0, y: 0 }, { id: 'node14', size: 15, x: 0, y: 0 }, { id: 'node15', size: 15, x: 0, y: 0 }, { id: 'node16', size: 15, x: 0, y: 0 }, ], edges: [ { source: 'node0', target: 'node1' }, { source: 'node0', target: 'node2' }, { source: 'node0', target: 'node3' }, { source: 'node0', target: 'node4' }, { source: 'node0', target: 'node5' }, { source: 'node1', target: 'node6' }, { source: 'node1', target: 'node7' }, { source: 'node2', target: 'node8' }, { source: 'node2', target: 'node9' }, { source: 'node2', target: 'node10' }, { source: 'node2', target: 'node11' }, { source: 'node2', target: 'node12' }, { source: 'node2', target: 'node13' }, { source: 'node3', target: 'node14' }, { source: 'node3', target: 'node15' }, { source: 'node3', target: 'node16' }, ], } const getModelFromOriginData = (data: typeof originData) => { const model: Model.FromJSONData = { nodes: [], edges: [], } originData.nodes.forEach((item) => { model.nodes?.push({ id: item.id, shape: 'circle', width: item.size as number, height: item.size as number, x: item.x, y: item.y, attrs: { body: { fill: '#5F95FF', stroke: 'transparent', }, }, }) }) originData.edges.forEach((item) => { model.edges?.push({ source: item.source, target: item.target, attrs: { line: { stroke: '#A2B1C3', strokeWidth: 2, targetMarker: null, }, }, }) }) return model } // helper function to update node positions function update() { for (const n of originData.nodes) { const node: Node = graph.getCellById(n.id) as Node node.position(n.x, n.y) } } // Store the current layout promise to prevent concurrent executions let currentLayoutPromise: Promise<void> | null = null const forceLayout = new ForceLayout({ type: 'force', center: [369, 180], preventOverlap: true, // Adjust alpha parameters for faster convergence alpha: 0.5, // 初始"温度",默认 0.3,提高到 0.5 使初始运动更快 alphaDecay: 0.05, // 温度衰减率,默认 0.028,增加到 0.05 使其更快收敛 alphaMin: 0.01, // 停止阈值,默认 0.001,增加到 0.01 使其更早停止 linkDistance: (d) => { if (d.source.id === 'node0') { return 100 } return 30 }, nodeStrength: (d) => { if (d.isLeaf) { return -50 } return -10 }, edgeStrength: (d) => { if ( d.source.id === 'node1' || d.source.id === 'node2' || d.source.id === 'node3' ) { return 0.7 } return 0.1 }, tick: update, }) // Helper function to run layout and return a promise function runLayout(): Promise<void> { return new Promise((resolve) => { // Set onLayoutEnd callback to resolve the promise forceLayout.updateCfg({ onLayoutEnd: () => { currentLayoutPromise = null resolve() }, } as any) // Start the layout forceLayout.layout(originData) }) } // Helper to ensure layout runs, chaining if one is already running async function scheduleLayout(): Promise<void> { if (currentLayoutPromise) { // Wait for current layout to finish, then run a new one await currentLayoutPromise } // Start new layout currentLayoutPromise = runLayout() return currentLayoutPromise } // initial render const model = getModelFromOriginData(originData) graph.fromJSON(model) // run initial layout scheduleLayout() // Debounced layout function to prevent too frequent calls during dragging const debouncedScheduleLayout = FunctionExt.debounce( () => scheduleLayout(), 16, // Wait 16ms (~1 frame at 60fps) after last movement before running layout ) // rerun layout on node drag, keep the dragged node fixed graph.on('node:moving', (e) => { // Update the fixed position immediately for responsive dragging for (const n of originData.nodes) { if (n.id === e.cell.id) { n.fx = e.x n.fy = e.y } else { delete n.fx delete n.fy } } // Use debounced layout to avoid too frequent calls debouncedScheduleLayout() }) // When node drag ends, ensure layout runs one final time graph.on('node:moved', () => { // Cancel any pending debounced layout debouncedScheduleLayout.cancel?.() // Run final layout immediately when drag ends // This ensures the final state is correctly calculated scheduleLayout() }) ``` ### 网格布局 - zh page: https://x6.antv.antgroup.com/zh/examples/layout/general/grid/ - en page: https://x6.antv.antgroup.com/en/examples/layout/general/grid/ - Source path: examples/layout/general/demo/grid.ts ```ts import { Graph, Model } from '@antv/x6' import { GridLayout } from '@antv/layout' const data: Model.FromJSONData = { nodes: [], edges: [], } const keyPoints = [ 20, 12, 12, 4, 18, 12, 12, 6, 16, 17, 17, 10, 10, 3, 3, 2, 2, 9, 9, 10, ] for (let i = 1; i <= 21; i++) { data.nodes!.push({ id: `${i}`, shape: 'circle', width: 32, height: 32, attrs: { body: { fill: '#5F95FF', stroke: 'transparent', }, label: { fill: '#ffffff', }, }, label: i, }) } for (let i = 0; i < keyPoints.length; i += 2) { data.edges!.push({ source: `${keyPoints[i]}`, target: `${keyPoints[i + 1]}`, attrs: { line: { stroke: '#A2B1C3', strokeWidth: 2, targetMarker: null, }, }, }) } const graph = new Graph({ container: document.getElementById('container')!, }) const gridLayout = new GridLayout({ type: 'grid', width: 738, height: 360, sortBy: 'label', rows: 3, cols: 7, }) const model = gridLayout.layout(data) graph.fromJSON(model) ``` ### 脑图树 - zh page: https://x6.antv.antgroup.com/zh/examples/layout/tree/mindmap/ - en page: https://x6.antv.antgroup.com/en/examples/layout/tree/mindmap/ - Source path: examples/layout/tree/demo/mindmap.ts ```ts import exampleData from '../../../data/algorithm-category.json' import { Graph, Model } from '@antv/x6' import Hierarchy from '@antv/hierarchy' const graph = new Graph({ container: document.getElementById('container')!, connecting: { connector: 'smooth', }, }) Promise.resolve(exampleData) .then((data) => { const result = Hierarchy.mindmap(data, { direction: 'H', getHeight() { return 16 }, getWidth() { return 16 }, getHGap() { return 80 }, getVGap() { return 1 }, getSide: () => { return 'right' }, }) const model: Model.FromJSONData = { nodes: [], edges: [] } const traverse = (data: HierarchyResult) => { if (data) { model.nodes?.push({ id: `${data.id}`, x: data.x + 250, y: data.y + 250, shape: 'circle', width: 16, height: 16, attrs: { body: { fill: '#5F95FF', stroke: 'transparent', }, }, }) } if (data.children) { data.children.forEach((item: HierarchyResult) => { model.edges?.push({ source: `${data.id}`, target: `${item.id}`, attrs: { line: { stroke: '#A2B1C3', strokeWidth: 1, targetMarker: null, }, }, }) traverse(item) }) } } traverse(result) graph.fromJSON(model) }) interface HierarchyResult { id: number x: number y: number children: HierarchyResult[] } ``` ### examples/data/algorithm-category.json ```json { "id": "Modeling Methods", "children": [ { "id": "Classification", "children": [ { "id": "Logistic regression" }, { "id": "Linear discriminant analysis" }, { "id": "Rules" }, { "id": "Decision trees" }, { "id": "Naive Bayes" }, { "id": "K nearest neighbor" }, { "id": "Probabilistic neural network" }, { "id": "Support vector machine" } ] }, { "id": "Consensus", "children": [ { "id": "Models diversity", "children": [ { "id": "Different initializations" }, { "id": "Different parameter choices" }, { "id": "Different architectures" }, { "id": "Different modeling methods" }, { "id": "Different training sets" }, { "id": "Different feature sets" } ] }, { "id": "Methods", "children": [ { "id": "Classifier selection" }, { "id": "Classifier fusion" } ] }, { "id": "Common", "children": [ { "id": "Bagging" }, { "id": "Boosting" }, { "id": "AdaBoost" } ] } ] }, { "id": "Regression", "children": [ { "id": "Multiple linear regression" }, { "id": "Partial least squares" }, { "id": "Multi-layer feedforward neural network" }, { "id": "General regression neural network" }, { "id": "Support vector regression" } ] } ] } ``` ### 使用 SVG 自定义图标 - zh page: https://x6.antv.antgroup.com/zh/examples/node/custom-node/custom-icon-with-svg/ - en page: https://x6.antv.antgroup.com/en/examples/node/custom-node/custom-icon-with-svg/ - Source path: examples/node/custom-node/demo/custom-icon-with-svg.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addNode({ x: 320, y: 120, width: 96, height: 96, markup: [ { tagName: 'path', selector: 'eye1', groupSelector: 'eye', attrs: { d: 'M24.82,48.678c5.422,0,9.832-6.644,9.832-14.811c0-8.165-4.41-14.809-9.832-14.809s-9.833,6.644-9.833,14.809C14.987,42.034,19.399,48.678,24.82,48.678z', }, }, { tagName: 'path', selector: 'eye2', groupSelector: 'eye', attrs: { d: 'M71.606,48.678c5.422,0,9.833-6.644,9.833-14.811c0-8.165-4.411-14.809-9.833-14.809c-5.421,0-9.831,6.644-9.831,14.809C61.775,42.034,66.186,48.678,71.606,48.678z', }, }, { tagName: 'path', selector: 'lip', attrs: { d: 'M95.855,55.806c-0.6-0.605-1.516-0.77-2.285-0.4C81.232,61.29,65.125,64.53,48.214,64.53c-16.907,0-33.015-3.24-45.354-9.123c-0.77-0.367-1.688-0.205-2.284,0.4c-0.599,0.606-0.747,1.526-0.369,2.29c5.606,11.351,25.349,19.277,48.008,19.277c22.668,0,42.412-7.929,48.012-19.279C96.603,57.332,96.453,56.411,95.855,55.806z', }, }, ], attrs: { lip: { fill: '#E0A31A', }, eye: { fill: '#730000', }, }, }) ``` ### 使用 React 自定节点 - zh page: https://x6.antv.antgroup.com/zh/examples/node/custom-node/custom-with-react/ - en page: https://x6.antv.antgroup.com/en/examples/node/custom-node/custom-with-react/ - Source path: examples/node/custom-node/demo/custom-with-react.tsx ```tsx import React from 'react' import { Graph, Color } from '@antv/x6' import { register } from '@antv/x6-react-shape' const MyComponent = ({ node }) => { const color = node.prop('color') return ( <div style={{ color: '#fff', width: '100%', height: '100%', textAlign: 'center', lineHeight: '50px', borderRadius: 4, background: color, }} > {color} </div> ) } register({ shape: 'custom-node', width: 120, height: 50, effect: ['color'], component: MyComponent, }) const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 120, y: 50, shape: 'custom-node', }) const target = graph.addNode({ x: 320, y: 260, shape: 'custom-node', }) graph.addEdge({ source, target, }) const update = () => { source.prop('color', Color.randomHex()) target.prop('color', Color.randomHex()) setTimeout(update, 1000) } update() ``` ### 使用 SVG 自定义节点 - zh page: https://x6.antv.antgroup.com/zh/examples/node/custom-node/custom-with-svg/ - en page: https://x6.antv.antgroup.com/en/examples/node/custom-node/custom-with-svg/ - Source path: examples/node/custom-node/demo/custom-with-svg.ts ```ts import { Graph } from '@antv/x6' Graph.registerNode( 'custom-node', { width: 200, height: 60, attrs: { body: { stroke: '#5F95FF', strokeWidth: 1, fill: 'rgba(95,149,255,0.05)', refWidth: 1, refHeight: 1, }, image: { 'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 16, height: 16, x: 12, y: 12, }, title: { text: 'Node', refX: 40, refY: 14, fill: 'rgba(0,0,0,0.85)', fontSize: 12, 'text-anchor': 'start', }, text: { text: 'this is content text', refX: 40, refY: 38, fontSize: 12, fill: 'rgba(0,0,0,0.6)', 'text-anchor': 'start', }, }, markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'image', selector: 'image', }, { tagName: 'text', selector: 'title', }, { tagName: 'text', selector: 'text', }, ], }, true, ) const graph = new Graph({ container: document.getElementById('container')!, grid: true, }) graph.addNode({ x: 200, y: 160, shape: 'custom-node', }) ``` ### Cylinder - zh page: https://x6.antv.antgroup.com/zh/examples/node/custom-node/cylinder/ - en page: https://x6.antv.antgroup.com/en/examples/node/custom-node/cylinder/ - Source path: examples/node/custom-node/demo/cylinder.ts ```ts import { Graph, NumberExt } from '@antv/x6' Graph.registerNode('cylinder', { markup: [ { tagName: 'path', selector: 'body', }, { tagName: 'ellipse', selector: 'top', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { fill: '#ffffff', stroke: '#333333', strokeWidth: 2, lateral: 10, }, top: { fill: '#ffffff', stroke: '#333333', strokeWidth: 2, refCx: '50%', refRx: '50%', cy: 10, ry: 10, }, }, attrHooks: { lateral: { set(t: number | string, { refBBox }) { const isPercentage = NumberExt.isPercentage(t) if (isPercentage) { // eslint-disable-next-line t = parseFloat(t as string) / 100 } const x = refBBox.x const y = refBBox.y const w = refBBox.width const h = refBBox.height // curve control point variables const rx = w / 2 const ry = isPercentage ? h * (t as number) : (t as number) const kappa = 0.551784 const cx = kappa * rx const cy = kappa * ry // shape variables const xLeft = x const xCenter = x + w / 2 const xRight = x + w const ySideTop = y + ry const yCurveTop = ySideTop - ry const ySideBottom = y + h - ry const yCurveBottom = y + h // return calculated shape const data = [ 'M', xLeft, ySideTop, 'L', xLeft, ySideBottom, 'C', x, ySideBottom + cy, xCenter - cx, yCurveBottom, xCenter, yCurveBottom, 'C', xCenter + cx, yCurveBottom, xRight, ySideBottom + cy, xRight, ySideBottom, 'L', xRight, ySideTop, 'C', xRight, ySideTop - cy, xCenter + cx, yCurveTop, xCenter, yCurveTop, 'C', xCenter - cx, yCurveTop, xLeft, ySideTop - cy, xLeft, ySideTop, 'Z', ] return { d: data.join(' ') } }, }, }, }) const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addNode({ shape: 'cylinder', x: 320, y: 120, width: 80, height: 120, label: 'cylinder', attrs: { top: { fill: '#fe854f', fillOpacity: 0.5, }, body: { fill: '#ED8A19', fillOpacity: 0.8, }, }, }) ``` ### 自定义开关节点 - zh page: https://x6.antv.antgroup.com/zh/examples/node/custom-node/switch/ - en page: https://x6.antv.antgroup.com/en/examples/node/custom-node/switch/ - Source path: examples/node/custom-node/demo/switch.ts ```ts import { Graph } from '@antv/x6' import insertCss from 'insert-css' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const switchCenter = { x: 35, y: -2, } const switchOpen = `rotate(-30, ${switchCenter.x}, ${switchCenter.y})` const switchClose = `rotate(-12, ${switchCenter.x}, ${switchCenter.y})` graph.addNode({ x: 320, y: 120, markup: [ { tagName: 'g', selector: 'left-group', children: [ { tagName: 'rect', selector: 'left', groupSelector: 'line', attrs: { x: 0, y: 0, }, }, { tagName: 'circle', selector: 'lco', groupSelector: 'co', attrs: { cx: 30, }, }, { tagName: 'circle', selector: 'lci', groupSelector: 'ci', attrs: { cx: 30, }, }, ], }, { tagName: 'rect', selector: 'switch', groupSelector: 'line', }, { tagName: 'g', selector: 'right-group', children: [ { tagName: 'rect', selector: 'right', groupSelector: 'line', attrs: { x: 70, y: 0, }, }, { tagName: 'circle', selector: 'rco', groupSelector: 'co', attrs: { cx: 70, }, }, { tagName: 'circle', selector: 'rci', groupSelector: 'ci', attrs: { cx: 70, }, }, ], }, ], attrs: { line: { width: 30, height: 2, fill: '#000', stroke: '#000', }, co: { r: 8, fill: '#000', }, ci: { r: 4, fill: '#fff', }, switch: { ...switchCenter, width: 35, transform: switchOpen, }, }, }) graph.on('node:click', ({ node }) => { const attrPath = 'attrs/switch/transform' const current = node.prop(attrPath) node.animate( { 'attrs/switch/transform': current === switchOpen ? switchClose : switchOpen, }, { duration: 100, fill: 'forwards', }, ) }) // 我们用 insert-css 插入自定义样式,实际使用时请使用自己的样式定义方式 insertCss(` .x6-node { cursor: pointer; } `) ``` ### 折叠/展开父节点 - zh page: https://x6.antv.antgroup.com/zh/examples/node/group/collapsable/ - en page: https://x6.antv.antgroup.com/en/examples/node/group/collapsable/ - Source path: examples/node/group/demo/collapsable.ts ```ts import { Graph, Node } from '@antv/x6' class Group extends Node { private collapsed = false private expandSize: { width: number; height: number } protected postprocess() { this.toggleCollapse(false) } isCollapsed() { return this.collapsed } toggleCollapse(collapsed?: boolean) { const target = collapsed == null ? !this.collapsed : collapsed if (target) { this.attr('buttonSign', { d: 'M 1 5 9 5 M 5 1 5 9' }) this.expandSize = this.getSize() this.resize(100, 32) } else { this.attr('buttonSign', { d: 'M 2 5 8 5' }) if (this.expandSize) { this.resize(this.expandSize.width, this.expandSize.height) } } this.collapsed = target } } Group.config({ markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'text', selector: 'label', }, { tagName: 'g', selector: 'buttonGroup', children: [ { tagName: 'rect', selector: 'button', attrs: { 'pointer-events': 'visiblePainted', }, }, { tagName: 'path', selector: 'buttonSign', attrs: { fill: 'none', 'pointer-events': 'none', }, }, ], }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', strokeWidth: 1, fill: '#ffffff', stroke: 'none', }, buttonGroup: { refX: 8, refY: 8, }, button: { height: 14, width: 16, rx: 2, ry: 2, fill: '#f5f5f5', stroke: '#ccc', cursor: 'pointer', event: 'node:collapse', }, buttonSign: { refX: 3, refY: 2, stroke: '#808080', }, label: { fontSize: 12, fill: '#fff', refX: 32, refY: 10, }, }, }) const graph = new Graph({ container: document.getElementById('container'), grid: true, }) function createGroup( id: string, x: number, y: number, width: number, height: number, fill: string, stroke: string, ) { const group = new Group({ id, x, y, width, height, attrs: { body: { fill, stroke }, label: { text: id }, }, }) graph.addNode(group) return group } function createNode( id: string, x: number, y: number, width: number, height: number, fill: string, ) { return graph.addNode({ id, x, y, width, height, attrs: { body: { fill: fill || 'blue', stroke: 'none', }, label: { text: id, fill: '#fff', fontSize: 12, }, }, }) } function createEdge( id: string, source: string, target: string, vertices?: { x: number; y: number }[], ) { return graph.addEdge({ id, source, target, vertices, label: id, attrs: { label: { fontSize: 12, }, }, }) } const a = createGroup('a', 100, 40, 420, 240, '#fffbe6', '#ffe7ba') const aa = createGroup('aa', 150, 100, 160, 120, '#47C769', 'none') const aaa = createGroup('aaa', 180, 150, 100, 50, '#3199FF', 'none') const c = createNode('c', 400, 180, 60, 40, 'orange') a.addChild(aa) aa.addChild(aaa) a.addChild(c) createNode('d', 580, 140, 60, 40, '#000') createEdge('edge1', 'aa', 'c') createEdge('edge3', 'c', 'd') aa.addChild( createEdge('edge2', 'aa', 'aaa', [ { x: 60, y: 140 }, { x: 60, y: 220 }, ]), ) graph.on('node:collapse', ({ node }: { node: Group }) => { node.toggleCollapse() const collapsed = node.isCollapsed() const collapse = (parent: Group) => { const cells = parent.getChildren() if (cells) { cells.forEach((cell) => { if (collapsed) { cell.hide() } else { cell.show() } if (cell instanceof Group) { if (!cell.isCollapsed()) { collapse(cell) } } }) } } collapse(node) }) ``` ### 拖入/拖出父节点 - zh page: https://x6.antv.antgroup.com/zh/examples/node/group/embedding-by-dnd/ - en page: https://x6.antv.antgroup.com/en/examples/node/group/embedding-by-dnd/ - Source path: examples/node/group/demo/embedding-by-dnd.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, embedding: { enabled: true, findParent({ node }) { const bbox = node.getBBox() return this.getNodes().filter((node) => { const data = node.getData<any>() if (data && data.parent) { const targetBBox = node.getBBox() return bbox.isIntersectWithRect(targetBBox) } return false }) }, }, highlighting: { embedding: { name: 'stroke', args: { padding: -1, attrs: { stroke: '#73d13d', }, }, }, }, }) graph.addNode({ x: 40, y: 140, width: 100, height: 50, label: 'Child\n(unembed)', zIndex: 10, attrs: { body: { stroke: 'none', fill: '#3199FF', }, label: { fill: '#fff', fontSize: 12, }, }, }) graph.addNode({ x: 500, y: 100, width: 100, height: 50, label: 'Child\n(unembed)', zIndex: 10, attrs: { body: { stroke: 'none', fill: '#47C769', }, label: { fill: '#fff', fontSize: 12, }, }, }) graph.addNode({ x: 200, y: 80, width: 240, height: 160, zIndex: 1, label: 'Parent', attrs: { body: { fill: '#fffbe6', stroke: '#ffe7ba', }, label: { fontSize: 12, }, }, data: { parent: true, }, }) graph.on('node:change:parent', ({ node }) => { node.attr({ label: { text: 'Child\n(embed)', }, }) }) ``` ### 自动更新边的父节点 - zh page: https://x6.antv.antgroup.com/zh/examples/node/group/embedding-edges/ - en page: https://x6.antv.antgroup.com/en/examples/node/group/embedding-edges/ - Source path: examples/node/group/demo/embedding-edges.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 80, y: 100, width: 80, height: 40, label: 'Child\n(source)', zIndex: 10, attrs: { body: { stroke: 'none', fill: '#3199FF', }, label: { fill: '#fff', fontSize: 12, }, }, }) const target = graph.addNode({ x: 280, y: 80, width: 80, height: 40, label: 'Child\n(target)', zIndex: 10, attrs: { body: { stroke: 'none', fill: '#47C769', }, label: { fill: '#fff', fontSize: 12, }, }, }) const parent = graph.addNode({ x: 40, y: 40, width: 360, height: 160, zIndex: 1, label: 'Parent\n(try to move me)', attrs: { label: { refY: 140, fontSize: 12, }, body: { fill: '#fffbe6', stroke: '#ffe7ba', }, }, }) parent.addChild(source) parent.addChild(target) graph.addEdge({ source, target, vertices: [ { x: 120, y: 60 }, { x: 200, y: 100 }, ], }) ``` ### 嵌套节点 - zh page: https://x6.antv.antgroup.com/zh/examples/node/group/embedding-nodes/ - en page: https://x6.antv.antgroup.com/en/examples/node/group/embedding-nodes/ - Source path: examples/node/group/demo/embedding-nodes.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const parent = graph.addNode({ x: 40, y: 40, width: 240, height: 160, zIndex: 1, label: 'Parent\n(try to move me)', attrs: { label: { refY: 120, fontSize: 12, }, body: { fill: '#fffbe6', stroke: '#ffe7ba', }, }, }) const child1 = graph.addNode({ x: 110, y: 80, width: 100, height: 50, label: 'Child\n(inside)', zIndex: 10, attrs: { body: { stroke: 'none', fill: '#3199FF', }, label: { fill: '#fff', fontSize: 12, }, }, }) const child2 = graph.createNode({ x: 360, y: 80, width: 100, height: 50, label: 'Child\n(outside)', zIndex: 10, attrs: { body: { stroke: 'none', fill: '#47C769', }, label: { fill: '#fff', fontSize: 12, }, }, }) parent.addChild(child1) parent.addChild(child2) ``` ### 自动扩展/收缩父节点 - zh page: https://x6.antv.antgroup.com/zh/examples/node/group/expand-shrink/ - en page: https://x6.antv.antgroup.com/en/examples/node/group/expand-shrink/ - Source path: examples/node/group/demo/expand-shrink.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, embedding: { enabled: true, }, highlighting: { embedding: { name: 'stroke', args: { padding: -1, attrs: { stroke: '#73d13d', }, }, }, }, }) const source = graph.addNode({ x: 80, y: 100, width: 80, height: 40, label: 'Child', zIndex: 10, attrs: { body: { stroke: 'none', fill: '#3199FF', }, label: { fill: '#fff', fontSize: 12, }, }, }) const target = graph.addNode({ x: 280, y: 80, width: 80, height: 40, label: 'Child', zIndex: 10, attrs: { body: { stroke: 'none', fill: '#47C769', }, label: { fill: '#fff', fontSize: 12, }, }, }) const parent = graph.addNode({ x: 40, y: 40, width: 360, height: 160, zIndex: 1, label: 'Parent', attrs: { body: { fill: '#fffbe6', stroke: '#ffe7ba', }, label: { fontSize: 12, }, }, }) parent.addChild(source) parent.addChild(target) let ctrlPressed = false const embedPadding = 20 graph.on('node:embedding', ({ e }: { e: JQuery.MouseMoveEvent }) => { ctrlPressed = e.metaKey || e.ctrlKey }) graph.on('node:embedded', () => { ctrlPressed = false }) graph.on('node:change:size', ({ node, options }) => { if (options.skipParentHandler) { return } const children = node.getChildren() if (children && children.length) { node.prop('originSize', node.getSize()) } }) graph.on('node:change:position', ({ node, options }) => { if (options.skipParentHandler || ctrlPressed) { return } const children = node.getChildren() if (children && children.length) { node.prop('originPosition', node.getPosition()) } const parent = node.getParent() if (parent && parent.isNode()) { let originSize = parent.prop('originSize') if (originSize == null) { originSize = parent.getSize() parent.prop('originSize', originSize) } let originPosition = parent.prop('originPosition') if (originPosition == null) { originPosition = parent.getPosition() parent.prop('originPosition', originPosition) } let x = originPosition.x let y = originPosition.y let cornerX = originPosition.x + originSize.width let cornerY = originPosition.y + originSize.height let hasChange = false const children = parent.getChildren() if (children) { children.forEach((child) => { const bbox = child.getBBox().inflate(embedPadding) const corner = bbox.getCorner() if (bbox.x < x) { x = bbox.x hasChange = true } if (bbox.y < y) { y = bbox.y hasChange = true } if (corner.x > cornerX) { cornerX = corner.x hasChange = true } if (corner.y > cornerY) { cornerY = corner.y hasChange = true } }) } if (hasChange) { parent.prop( { position: { x, y }, size: { width: cornerX - x, height: cornerY - y }, }, { skipParentHandler: true }, ) } } }) ``` ### 限制子节点的移动范围 - zh page: https://x6.antv.antgroup.com/zh/examples/node/group/restrict-movement/ - en page: https://x6.antv.antgroup.com/en/examples/node/group/restrict-movement/ - Source path: examples/node/group/demo/restrict-movement.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, translating: { restrict(view) { const cell = view.cell if (cell.isNode()) { const parent = cell.getParent() if (parent) { return parent.getBBox() } } return null }, }, }) const child = graph.addNode({ x: 120, y: 80, width: 80, height: 40, label: 'Child', zIndex: 10, attrs: { body: { stroke: 'none', fill: '#3199FF', }, label: { fill: '#fff', fontSize: 12, }, }, }) const parent = graph.addNode({ x: 40, y: 40, width: 240, height: 160, zIndex: 1, label: 'Parent\n(try to move me)', attrs: { label: { refY: 120, fontSize: 12, }, body: { fill: '#fffbe6', stroke: '#ffe7ba', }, }, }) parent.addChild(child) ``` ### Circle - zh page: https://x6.antv.antgroup.com/zh/examples/node/native-node/circle/ - en page: https://x6.antv.antgroup.com/en/examples/node/native-node/circle/ - Source path: examples/node/native-node/demo/circle.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) // row 1 // ----- graph.addNode({ shape: 'circle', x: 40, y: 40, width: 60, height: 60, label: 'circle', }) graph.addNode({ shape: 'circle', x: 200, y: 40, width: 60, height: 60, label: 'circle', attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, }, }) graph.addNode({ shape: 'circle', x: 360, y: 40, width: 60, height: 60, label: 'circle', attrs: { body: { stroke: '#237804', fill: '#73d13d', }, }, }) graph.addNode({ shape: 'circle', x: 520, y: 40, width: 60, height: 60, label: 'circle', attrs: { body: { stroke: '#ffa940', fill: '#ffd591', }, }, }) // row 2 // ----- graph.addNode({ shape: 'circle', x: 40, y: 140, width: 60, height: 60, label: 'circle', attrs: { label: { refX: 16, refY: 16, textAnchor: 'start', textVerticalAnchor: 'top', }, }, }) graph.addNode({ shape: 'circle', x: 200, y: 140, width: 60, height: 60, label: 'circle', attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, label: { refX: 16, refY: 0.5, textAnchor: 'start', textVerticalAnchor: 'middle', }, }, }) graph.addNode({ shape: 'circle', x: 360, y: 140, width: 60, height: 60, label: 'circle', attrs: { body: { stroke: '#237804', fill: '#73d13d', }, label: { refX: 16, refY: '100%', refY2: -16, textAnchor: 'start', textVerticalAnchor: 'bottom', }, }, }) graph.addNode({ shape: 'circle', x: 520, y: 140, width: 60, height: 60, label: 'circle', attrs: { body: { stroke: '#ffa940', fill: '#ffd591', }, label: { refX: 0.5, refY: -4, textAnchor: 'middle', textVerticalAnchor: 'bottom', }, }, }) // row 3 // ----- graph.addNode({ shape: 'circle', x: 40, y: 240, width: 60, height: 60, label: 'circle', attrs: { label: { refX: 0.5, refY: 16, textAnchor: 'middle', textVerticalAnchor: 'top', }, }, }) graph.addNode({ shape: 'circle', x: 200, y: 240, width: 60, height: 60, label: 'circle', attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, label: { refX: 0.5, refY: 0.5, textAnchor: 'middle', textVerticalAnchor: 'middle', }, }, }) graph.addNode({ shape: 'circle', x: 360, y: 240, width: 60, height: 60, label: 'circle', attrs: { body: { stroke: '#237804', fill: '#73d13d', }, label: { refX: 0.5, refY: '100%', refY2: -16, textAnchor: 'middle', textVerticalAnchor: 'bottom', }, }, }) graph.addNode({ shape: 'circle', x: 520, y: 240, width: 60, height: 60, label: 'circle', attrs: { body: { stroke: '#ffa940', fill: '#ffd591', }, label: { refX: '100%', refX2: 4, refY: 0.5, textAnchor: 'start', textVerticalAnchor: 'middle', }, }, }) // row 4 // ----- graph.addNode({ shape: 'circle', x: 40, y: 340, width: 60, height: 60, label: 'circle', attrs: { label: { refX: '100%', refX2: -16, refY: 16, textAnchor: 'end', textVerticalAnchor: 'top', }, }, }) graph.addNode({ shape: 'circle', x: 200, y: 340, width: 60, height: 60, label: 'circle', attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, label: { refX: '100%', refX2: -16, refY: 0.5, textAnchor: 'end', textVerticalAnchor: 'middle', }, }, }) graph.addNode({ shape: 'circle', x: 360, y: 340, width: 60, height: 60, label: 'circle', attrs: { body: { stroke: '#237804', fill: '#73d13d', }, label: { refX: '100%', refX2: -16, refY: '100%', refY2: -16, textAnchor: 'end', textVerticalAnchor: 'bottom', }, }, }) graph.addNode({ shape: 'circle', x: 520, y: 340, width: 60, height: 60, label: 'circle', attrs: { body: { stroke: '#ffa940', fill: '#ffd591', }, label: { refX: 0.5, refY: '100%', refY2: 4, textAnchor: 'middle', textVerticalAnchor: 'top', }, }, }) ``` ### Ellipse - zh page: https://x6.antv.antgroup.com/zh/examples/node/native-node/ellipse/ - en page: https://x6.antv.antgroup.com/en/examples/node/native-node/ellipse/ - Source path: examples/node/native-node/demo/ellipse.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) // row 1 // ----- graph.addNode({ shape: 'ellipse', x: 40, y: 40, width: 120, height: 60, label: 'ellipse', }) graph.addNode({ shape: 'ellipse', x: 200, y: 40, width: 120, height: 60, label: 'ellipse', attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, }, }) graph.addNode({ shape: 'ellipse', x: 360, y: 40, width: 120, height: 60, label: 'ellipse', attrs: { body: { stroke: '#237804', fill: '#73d13d', }, }, }) graph.addNode({ shape: 'ellipse', x: 520, y: 40, width: 60, height: 60, label: 'ellipse', attrs: { body: { stroke: '#ffa940', fill: '#ffd591', }, }, }) // row 2 // ----- graph.addNode({ shape: 'ellipse', x: 40, y: 140, width: 120, height: 60, label: 'ellipse', attrs: { label: { refX: 16, refY: 16, textAnchor: 'start', textVerticalAnchor: 'top', }, }, }) graph.addNode({ shape: 'ellipse', x: 200, y: 140, width: 120, height: 60, label: 'ellipse', attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, label: { refX: 16, refY: 0.5, textAnchor: 'start', textVerticalAnchor: 'middle', }, }, }) graph.addNode({ shape: 'ellipse', x: 360, y: 140, width: 120, height: 60, label: 'ellipse', attrs: { body: { stroke: '#237804', fill: '#73d13d', }, label: { refX: 16, refY: '100%', refY2: -16, textAnchor: 'start', textVerticalAnchor: 'bottom', }, }, }) graph.addNode({ shape: 'ellipse', x: 520, y: 140, width: 60, height: 60, label: 'ellipse', attrs: { body: { stroke: '#ffa940', fill: '#ffd591', }, label: { refX: 0.5, refY: -4, textAnchor: 'middle', textVerticalAnchor: 'bottom', }, }, }) // row 3 // ----- graph.addNode({ shape: 'ellipse', x: 40, y: 240, width: 120, height: 60, label: 'ellipse', attrs: { label: { refX: 0.5, refY: 16, textAnchor: 'middle', textVerticalAnchor: 'top', }, }, }) graph.addNode({ shape: 'ellipse', x: 200, y: 240, width: 120, height: 60, label: 'ellipse', attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, label: { refX: 0.5, refY: 0.5, textAnchor: 'middle', textVerticalAnchor: 'middle', }, }, }) graph.addNode({ shape: 'ellipse', x: 360, y: 240, width: 120, height: 60, label: 'ellipse', attrs: { body: { stroke: '#237804', fill: '#73d13d', }, label: { refX: 0.5, refY: '100%', refY2: -16, textAnchor: 'middle', textVerticalAnchor: 'bottom', }, }, }) graph.addNode({ shape: 'ellipse', x: 520, y: 240, width: 60, height: 60, label: 'ellipse', attrs: { body: { stroke: '#ffa940', fill: '#ffd591', }, label: { refX: '100%', refX2: 4, refY: 0.5, textAnchor: 'start', textVerticalAnchor: 'middle', }, }, }) // row 4 // ----- graph.addNode({ shape: 'ellipse', x: 40, y: 340, width: 120, height: 60, label: 'ellipse', attrs: { label: { refX: '100%', refX2: -16, refY: 16, textAnchor: 'end', textVerticalAnchor: 'top', }, }, }) graph.addNode({ shape: 'ellipse', x: 200, y: 340, width: 120, height: 60, label: 'ellipse', attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, label: { refX: '100%', refX2: -16, refY: 0.5, textAnchor: 'end', textVerticalAnchor: 'middle', }, }, }) graph.addNode({ shape: 'ellipse', x: 360, y: 340, width: 120, height: 60, label: 'ellipse', attrs: { body: { stroke: '#237804', fill: '#73d13d', }, label: { refX: '100%', refX2: -16, refY: '100%', refY2: -16, textAnchor: 'end', textVerticalAnchor: 'bottom', }, }, }) graph.addNode({ shape: 'ellipse', x: 520, y: 340, width: 60, height: 60, label: 'ellipse', attrs: { body: { stroke: '#ffa940', fill: '#ffd591', }, label: { refX: 0.5, refY: '100%', refY2: 4, textAnchor: 'middle', textVerticalAnchor: 'top', }, }, }) ``` ### Image - zh page: https://x6.antv.antgroup.com/zh/examples/node/native-node/image/ - en page: https://x6.antv.antgroup.com/en/examples/node/native-node/image/ - Source path: examples/node/native-node/demo/image.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addNode({ shape: 'image', x: 320, y: 120, width: 94, height: 28, imageUrl: 'https://gw.alipayobjects.com/os/s/prod/antv/assets/image/logo-with-text-73b8a.svg', }) ``` ### Path - zh page: https://x6.antv.antgroup.com/zh/examples/node/native-node/path/ - en page: https://x6.antv.antgroup.com/en/examples/node/native-node/path/ - Source path: examples/node/native-node/demo/path.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addNode({ shape: 'path', x: 40, y: 40, width: 100, height: 80, label: 'path', // 使用 path 属性指定路径的 pathData,相当于指定路径的 refD 属性 // https://x6.antv.antgroup.com/api/registry/attr#refdresetoffset path: 'M 0 5 10 0 C 20 0 20 20 10 20 L 0 15 Z', attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, }, }) graph.addNode({ shape: 'path', x: 200, y: 40, width: 100, height: 80, label: 'path', attrs: { body: { fill: '#efdbff', stroke: '#9254de', // 指定 refD 属性,pathData 随图形大小自动缩放 // https://x6.antv.antgroup.com/api/registry/attr#refdresetoffset refD: 'M 0 5 10 0 C 20 0 20 20 10 20 L 0 15 Z', }, }, }) graph.addNode({ shape: 'path', x: 360, y: 70, width: 100, height: 80, label: 'path', attrs: { body: { fill: '#efdbff', stroke: '#9254de', // 指定 d 属性,pathData 不随图形的大小缩放 d: 'M 0 5 10 0 C 20 0 20 20 10 20 L 0 15 Z', }, }, }) graph.addNode({ shape: 'path', x: 50, y: 180, width: 80, height: 80, // https://www.svgrepo.com/svg/13653/like path: 'M24.85,10.126c2.018-4.783,6.628-8.125,11.99-8.125c7.223,0,12.425,6.179,13.079,13.543c0,0,0.353,1.828-0.424,5.119c-1.058,4.482-3.545,8.464-6.898,11.503L24.85,48L7.402,32.165c-3.353-3.038-5.84-7.021-6.898-11.503c-0.777-3.291-0.424-5.119-0.424-5.119C0.734,8.179,5.936,2,13.159,2C18.522,2,22.832,5.343,24.85,10.126z', attrs: { body: { fill: '#D75A4A', stroke: 'none', }, }, }) graph.addNode({ shape: 'path', x: 210, y: 180, width: 80, height: 80, // https://www.svgrepo.com/svg/13695/star path: 'M26.285,2.486l5.407,10.956c0.376,0.762,1.103,1.29,1.944,1.412l12.091,1.757c2.118,0.308,2.963,2.91,1.431,4.403l-8.749,8.528c-0.608,0.593-0.886,1.448-0.742,2.285l2.065,12.042c0.362,2.109-1.852,3.717-3.746,2.722l-10.814-5.685c-0.752-0.395-1.651-0.395-2.403,0l-10.814,5.685c-1.894,0.996-4.108-0.613-3.746-2.722l2.065-12.042c0.144-0.837-0.134-1.692-0.742-2.285l-8.749-8.528c-1.532-1.494-0.687-4.096,1.431-4.403l12.091-1.757c0.841-0.122,1.568-0.65,1.944-1.412l5.407-10.956C22.602,0.567,25.338,0.567,26.285,2.486z', attrs: { body: { fill: '#ED8A19', stroke: 'none', }, }, }) graph.addNode({ shape: 'path', x: 370, y: 180, width: 80, height: 80, // https://www.svgrepo.com/svg/13692/music-player path: 'M52.104,0.249c-0.216-0.189-0.501-0.275-0.789-0.241l-31,4.011c-0.499,0.065-0.872,0.489-0.872,0.992v6.017v4.212v26.035C17.706,39.285,14.997,38,11.944,38c-5.247,0-9.5,3.781-9.5,8.444s4.253,8.444,9.5,8.444s9.5-3.781,9.5-8.444c0-0.332-0.027-0.658-0.069-0.981c0.04-0.108,0.069-0.221,0.069-0.343V16.118l29-3.753v18.909C48.706,29.285,45.997,28,42.944,28c-5.247,0-9.5,3.781-9.5,8.444s4.253,8.444,9.5,8.444s9.5-3.781,9.5-8.444c0-0.092-0.012-0.181-0.015-0.272c0.002-0.027,0.015-0.05,0.015-0.077V11.227V7.016V1C52.444,0.712,52.32,0.438,52.104,0.249z', attrs: { body: { fill: '#eb2f96', stroke: 'none', }, }, }) ``` ### Polygon - zh page: https://x6.antv.antgroup.com/zh/examples/node/native-node/polygon/ - en page: https://x6.antv.antgroup.com/en/examples/node/native-node/polygon/ - Source path: examples/node/native-node/demo/polygon.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addNode({ shape: 'polygon', x: 40, y: 40, width: 80, height: 80, label: 'polygon', attrs: { body: { fill: '#efdbff', stroke: '#9254de', // 指定 refPoints 属性,多边形顶点随图形大小自动缩放 // https://x6.antv.antgroup.com/api/registry/attr#refpointsresetoffset refPoints: '0,10 10,0 20,10 10,20', }, }, }) graph.addNode({ shape: 'polygon', x: 180, y: 40, width: 80, height: 80, label: 'polygon', // 使用 points 属性指定多边形的顶点,相当于指定多边形的 refPoints 属性 // https://x6.antv.antgroup.com/api/registry/attr#refpointsresetoffset points: '0,10 10,0 20,10 10,20', attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, }, }) graph.addNode({ shape: 'polygon', x: 320, y: 40, width: 80, height: 80, label: 'polygon', // 使用 points 属性指定多边形的顶点数组 points: [ [0, 10], [10, 0], [20, 10], [10, 20], ], attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, }, }) graph.addNode({ shape: 'polygon', x: 460, y: 40, width: 80, height: 80, label: 'polygon', // 使用 points 属性指定多边形的顶点数组 points: [ { x: 0, y: 10 }, { x: 10, y: 0 }, { x: 20, y: 10 }, { x: 10, y: 20 }, ], attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, }, }) graph.addNode({ shape: 'polygon', x: 600, y: 70, width: 80, height: 80, label: 'polygon', attrs: { body: { fill: '#efdbff', stroke: '#9254de', // 直接指定 points 属性,多边形顶点不随随图形大小自动缩放 points: '0,10 10,0 20,10 10,20', }, }, }) graph.addNode({ shape: 'polygon', x: 40, y: 180, width: 80, height: 80, attrs: { body: { fill: '#73d13d', stroke: '#237804', refPoints: '0,100 50,25 50,75 100,0', }, }, }) graph.addNode({ shape: 'polygon', x: 180, y: 180, width: 80, height: 80, points: '100,10 40,198 190,78 10,78 160,198', attrs: { body: { fill: '#ffd591', stroke: '#ffa940', strokeWidth: 2, fillRule: 'evenodd', }, }, }) graph.addNode({ shape: 'polygon', x: 320, y: 180, width: 80, height: 80, points: '100,10 40,198 190,78 10,78 160,198', attrs: { body: { fill: '#ffd591', stroke: '#ffa940', strokeWidth: 2, fillRule: 'nonzero', }, }, }) graph.addNode({ shape: 'polygon', x: 460, y: 180, width: 80, height: 80, points: '26.934,1.318 35.256,18.182 53.867,20.887 40.4,34.013 43.579,52.549 26.934,43.798 10.288,52.549 13.467,34.013 0,20.887 18.611,18.182', attrs: { body: { fill: '#ED8A19', stroke: 'none', }, }, }) ``` ### Polyline - zh page: https://x6.antv.antgroup.com/zh/examples/node/native-node/polyline/ - en page: https://x6.antv.antgroup.com/en/examples/node/native-node/polyline/ - Source path: examples/node/native-node/demo/polyline.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addNode({ shape: 'polyline', x: 40, y: 40, width: 80, height: 80, label: 'polyline', attrs: { body: { fill: '#efdbff', stroke: '#9254de', // 指定 refPoints 属性,顶点随图形大小自动缩放 // https://x6.antv.antgroup.com/api/registry/attr#refpointsresetoffset refPoints: '0,0 0,10 10,10 10,0', }, }, }) graph.addNode({ shape: 'polyline', x: 180, y: 40, width: 80, height: 80, label: 'polyline', // 使用 points 属性指定顶点,相当于指定 refPoints 属性 // https://x6.antv.antgroup.com/api/registry/attr#refpointsresetoffset points: '0,0 0,10 10,10 10,0', attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, }, }) graph.addNode({ shape: 'polyline', x: 320, y: 40, width: 80, height: 80, label: 'polyline', // 使用 points 属性指定顶点数组 points: [ [0, 0], [0, 10], [10, 10], [10, 0], ], attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, }, }) graph.addNode({ shape: 'polyline', x: 40, y: 180, width: 80, height: 80, label: 'polyline', // 使用 points 属性指定顶点数组 points: [ { x: 0, y: 0 }, { x: 0, y: 10 }, { x: 10, y: 10 }, { x: 10, y: 0 }, ], attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, }, }) graph.addNode({ shape: 'polyline', x: 180, y: 210, width: 80, height: 80, label: 'polyline', attrs: { body: { fill: '#efdbff', stroke: '#9254de', // 直接指定 points 属性,多边形顶点不随随图形大小自动缩放 points: '0,10 10,0 20,10 10,20', }, }, }) graph.addNode({ shape: 'polyline', x: 320, y: 180, width: 80, height: 80, attrs: { body: { fill: 'none', stroke: '#ffa940', refPoints: '0,40 40,40 40,80 80,80 80,120 120,120 120,160', }, }, }) ``` ### Rectangle - zh page: https://x6.antv.antgroup.com/zh/examples/node/native-node/rect/ - en page: https://x6.antv.antgroup.com/en/examples/node/native-node/rect/ - Source path: examples/node/native-node/demo/rect.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) // row 1 // ----- graph.addNode({ x: 40, y: 40, width: 120, height: 60, label: 'rect', }) graph.addNode({ x: 200, y: 40, width: 120, height: 60, label: 'rect', attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, }, }) graph.addNode({ x: 360, y: 40, width: 120, height: 60, label: 'rect', attrs: { body: { stroke: '#237804', fill: '#73d13d', rx: 10, ry: 10, }, }, }) graph.addNode({ x: 520, y: 40, width: 60, height: 60, label: 'rect', attrs: { body: { stroke: '#ffa940', fill: '#ffd591', rx: 10, ry: 10, }, }, }) // row 2 // ----- graph.addNode({ x: 40, y: 140, width: 120, height: 60, label: 'rect', attrs: { label: { refX: 8, refY: 8, textAnchor: 'start', textVerticalAnchor: 'top', }, }, }) graph.addNode({ x: 200, y: 140, width: 120, height: 60, label: 'rect', attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, label: { refX: 8, refY: 0.5, textAnchor: 'start', textVerticalAnchor: 'middle', }, }, }) graph.addNode({ x: 360, y: 140, width: 120, height: 60, label: 'rect', attrs: { body: { stroke: '#237804', fill: '#73d13d', rx: 10, ry: 10, }, label: { refX: 8, refY: '100%', refY2: -8, textAnchor: 'start', textVerticalAnchor: 'bottom', }, }, }) graph.addNode({ x: 520, y: 140, width: 60, height: 60, label: 'rect', attrs: { body: { stroke: '#ffa940', fill: '#ffd591', rx: 10, ry: 10, }, label: { refX: 0.5, refY: -4, textAnchor: 'middle', textVerticalAnchor: 'bottom', }, }, }) // row 3 // ----- graph.addNode({ x: 40, y: 240, width: 120, height: 60, label: 'rect', attrs: { label: { refX: 0.5, refY: 8, textAnchor: 'middle', textVerticalAnchor: 'top', }, }, }) graph.addNode({ x: 200, y: 240, width: 120, height: 60, label: 'rect', attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, label: { refX: 0.5, refY: 0.5, textAnchor: 'middle', textVerticalAnchor: 'middle', }, }, }) graph.addNode({ x: 360, y: 240, width: 120, height: 60, label: 'rect', attrs: { body: { stroke: '#237804', fill: '#73d13d', rx: 10, ry: 10, }, label: { refX: 0.5, refY: '100%', refY2: -8, textAnchor: 'middle', textVerticalAnchor: 'bottom', }, }, }) graph.addNode({ x: 520, y: 240, width: 60, height: 60, label: 'rect', attrs: { body: { stroke: '#ffa940', fill: '#ffd591', rx: 10, ry: 10, }, label: { refX: '100%', refX2: 4, refY: 0.5, textAnchor: 'start', textVerticalAnchor: 'middle', }, }, }) // row 4 // ----- graph.addNode({ x: 40, y: 340, width: 120, height: 60, label: 'rect', attrs: { label: { refX: '100%', refX2: -8, refY: 8, textAnchor: 'end', textVerticalAnchor: 'top', }, }, }) graph.addNode({ x: 200, y: 340, width: 120, height: 60, label: 'rect', attrs: { body: { fill: '#efdbff', stroke: '#9254de', }, label: { refX: '100%', refX2: -8, refY: 0.5, textAnchor: 'end', textVerticalAnchor: 'middle', }, }, }) graph.addNode({ x: 360, y: 340, width: 120, height: 60, label: 'rect', attrs: { body: { stroke: '#237804', fill: '#73d13d', rx: 10, ry: 10, }, label: { refX: '100%', refX2: -8, refY: '100%', refY2: -8, textAnchor: 'end', textVerticalAnchor: 'bottom', }, }, }) graph.addNode({ x: 520, y: 340, width: 60, height: 60, label: 'rect', attrs: { body: { stroke: '#ffa940', fill: '#ffd591', rx: 10, ry: 10, }, label: { refX: 0.5, refY: '100%', refY2: 4, textAnchor: 'middle', textVerticalAnchor: 'top', }, }, }) ``` ### 文本 - zh page: https://x6.antv.antgroup.com/zh/examples/node/native-node/text-block/ - en page: https://x6.antv.antgroup.com/en/examples/node/native-node/text-block/ - Source path: examples/node/native-node/demo/text-block.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addNode({ x: 160, y: 120, width: 360, height: 120, shape: 'text-block', text: `There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable.`, attrs: { body: { fill: '#efdbff', stroke: '#9254de', rx: 4, ry: 4, }, }, }) ``` ### 连接到连接桩 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/connect-to-port/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/connect-to-port/ - Source path: examples/node/port/demo/connect-to-port.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) // 文档:https://x6.antv.antgroup.com/tutorial/basic/port const rect = graph.addNode({ x: 240, y: 60, width: 100, height: 180, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, ports: { groups: { group1: { attrs: { circle: { r: 6, magnet: true, stroke: '#31d0c6', fill: '#fff', strokeWidth: 2, }, }, }, }, items: [ { id: 'port1', group: 'group1' }, { id: 'port2', group: 'group1' }, { id: 'port3', group: 'group1' }, ], }, }) graph.addEdge({ source: { x: 40, y: 150 }, target: { cell: rect, port: 'port1', // 连接桩 ID }, attrs: { line: { stroke: '#d9d9d9' }, }, }) graph.addEdge({ source: { x: 40, y: 150 }, target: { cell: rect, port: 'port2', // 连接桩 ID }, attrs: { line: { stroke: '#d9d9d9' }, }, }) graph.addEdge({ source: { x: 40, y: 150 }, target: { cell: rect, port: 'port3', // 连接桩 ID }, attrs: { line: { stroke: '#d9d9d9' }, }, }) ``` ### 默认连接桩 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/default/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/default/ - Source path: examples/node/port/demo/default.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) // 文档:https://x6.antv.antgroup.com/tutorial/basic/port const rect = graph.addNode({ x: 240, y: 60, width: 100, height: 180, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, ports: [ // 默认样式 { id: 'port1' }, // 自定义连接桩样式 { id: 'port2', attrs: { circle: { magnet: true, r: 8, stroke: '#31d0c6', fill: '#fff', strokeWidth: 2, }, }, }, ], }) rect.addPort({ id: 'port3', attrs: { circle: { r: 6, magnet: true, stroke: '#31d0c6', fill: '#fff', strokeWidth: 2, }, }, }) ``` ### 添加或移除连接桩 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/dynamic/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/dynamic/ - Source path: examples/node/port/demo/dynamic.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) // 文档:https://x6.antv.antgroup.com/tutorial/basic/port const rect = graph.addNode({ x: 240, y: 60, width: 100, height: 180, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, ports: { groups: { group1: { attrs: { circle: { r: 6, magnet: true, stroke: '#31d0c6', fill: '#fff', strokeWidth: 2, }, }, }, }, items: [{ group: 'group1' }, { group: 'group1' }], }, }) const addPort = () => { rect.addPort({ group: 'group1' }) } const removePort = () => { const ports = rect.getPorts() if (ports.length) { rect.removePortAt(ports.length - 1) } } const count = 5 let current = 0 let adding = true const run = () => { if (adding && current === count) { adding = false } if (!adding && current === 0) { adding = true } if (adding && current < count) { addPort() current += 1 } if (!adding && current > 0) { removePort() current -= 1 } setTimeout(run, 1000) } run() ``` ### 使用 Group 定义连接桩样式 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/group/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/group/ - Source path: examples/node/port/demo/group.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) // 文档:https://x6.antv.antgroup.com/tutorial/basic/port const rect = graph.addNode({ x: 240, y: 60, width: 100, height: 180, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, ports: { groups: { group1: { attrs: { circle: { r: 6, magnet: true, stroke: '#31d0c6', fill: '#fff', strokeWidth: 2, }, }, }, }, items: [ { id: 'port1', group: 'group1' }, { id: 'port2', group: 'group1' }, ], }, }) rect.addPort({ id: 'port3', group: 'group1' }) ``` ### 连接桩标签 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/label/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/label/ - Source path: examples/node/port/demo/label.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) // 文档:https://x6.antv.antgroup.com/tutorial/basic/port const rect = graph.addNode({ x: 240, y: 60, width: 100, height: 180, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, ports: { groups: { group1: { attrs: { circle: { r: 6, magnet: true, stroke: '#31d0c6', fill: '#fff', strokeWidth: 2, }, }, }, }, items: [ { id: 'port1', group: 'group1', attrs: { text: { // 标签选择器 text: 'port1', // 标签文本 }, }, }, { id: 'port2', group: 'group1', attrs: { text: { // 标签选择器 text: 'port2', // 标签文本 }, }, }, ], }, }) rect.addPort({ id: 'port3', group: 'group1', attrs: { text: { // 标签选择器 text: 'port3', // 标签文本 }, }, }) ``` ### 位于节点内部或外部的标签 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/port-label-position-in-out/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/port-label-position-in-out/ - Source path: examples/node/port/demo/port-label-position-in-out.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const node = graph.addNode({ x: 100, y: 60, width: 400, height: 200, shape: 'ellipse', attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, label: { text: 'outside', fill: '#888', fontSize: 12, }, }, ports: { groups: { a: { position: { name: 'ellipseSpread', args: { compensateRotate: true, }, }, label: { position: { name: 'outside', }, }, attrs: { circle: { fill: '#ffffff', stroke: '#31d0c6', strokeWidth: 2, r: 10, magnet: true, }, text: { fill: '#6a6c8a', fontSize: 12, }, }, }, }, }, }) Array.from({ length: 10 }).forEach((_, index) => { node.addPort({ attrs: { text: { text: `P ${index}` } }, group: 'a' }) }) const sides = ['inside', 'outside', 'insideOriented', 'outsideOriented'] let index = 1 function toggle() { index += 1 if (index === sides.length) { index = 0 } const position = sides[index] node.prop('ports/groups/a/label/position', { name: position, }) node.attr('label/text', position) setTimeout(toggle, 1000) } toggle() ``` ### 位于椭圆外围的标签 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/port-label-position-radial/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/port-label-position-radial/ - Source path: examples/node/port/demo/port-label-position-radial.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const node = graph.addNode({ x: 100, y: 60, width: 400, height: 200, shape: 'ellipse', attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, label: { text: 'radial', fill: '#888', fontSize: 12, }, }, ports: { groups: { a: { position: { name: 'ellipseSpread', args: { compensateRotate: true, }, }, label: { position: { name: 'radial', }, }, attrs: { circle: { fill: '#ffffff', stroke: '#31d0c6', strokeWidth: 2, r: 10, magnet: true, }, text: { fill: '#6a6c8a', fontSize: 12, }, }, }, }, }, }) Array.from({ length: 10 }).forEach((_, index) => { node.addPort({ attrs: { text: { text: `P ${index}` } }, group: 'a' }) }) const prop = 'ports/groups/a/label/position' function toggle() { const position = node.prop(prop) === 'radial' ? 'radialOriented' : 'radial' node.prop(prop, position) node.attr('label/text', position) setTimeout(toggle, 1000) } toggle() ``` ### 位于连接桩一侧的标签 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/port-label-position-side/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/port-label-position-side/ - Source path: examples/node/port/demo/port-label-position-side.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const rect = graph.addNode({ x: 100, y: 60, width: 280, height: 120, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, label: { text: 'left', fill: '#888', fontSize: 12, }, }, ports: { groups: { a: { position: { name: 'top', }, label: { position: { name: 'left', }, }, attrs: { circle: { fill: '#fff', stroke: '#31d0c6', strokeWidth: 2, r: 8, }, text: { fill: '#666', fontSize: 12, }, }, }, }, }, }) Array.from({ length: 3 }).forEach((_, index) => { const label = index === 2 ? { position: { args: { x: 20, y: -20 } }, } : {} const stroke = index === 2 ? { stroke: 'red' } : {} const fill = index === 2 ? { fill: 'red' } : {} rect.addPort({ label, group: 'a', attrs: { circle: { magnet: true, ...stroke }, text: { text: `P${index}`, ...fill }, }, }) }) const sides = ['left', 'top', 'right', 'bottom'] let index = 0 function run() { index += 1 if (index === sides.length) { index = 0 } rect.prop('ports/groups/a/label/position/name', sides[index]) rect.attr('label/text', sides[index]) setTimeout(run, 1000) } run() ``` ### 绝对定位的连接桩 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/port-position-absolute/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/port-position-absolute/ - Source path: examples/node/port/demo/port-position-absolute.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addNode({ x: 100, y: 60, width: 280, height: 120, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, ports: { groups: { group1: { attrs: { circle: { r: 6, magnet: true, stroke: '#31d0c6', strokeWidth: 2, fill: '#fff', }, text: { fontSize: 12, fill: '#888', }, }, // 文档:https://x6.antv.antgroup.com/api/registry/port-layout#absolute position: { name: 'absolute', }, }, }, items: [ { id: 'port1', group: 'group1', // 通过 args 指定绝对位置 args: { x: 0, y: 60, }, attrs: { text: { text: '{ x: 0, y: 60 }' }, }, }, { id: 'port2', group: 'group1', // 通过 args 指定绝对位置和连接桩的旋转角度 args: { x: 0.6, y: 32, angle: 45, }, // 自定义连接桩渲染的 SVG markup: [ { tagName: 'path', selector: 'path', }, ], zIndex: 10, attrs: { path: { d: 'M -6 -8 L 0 8 L 6 -8 Z', magnet: true, fill: 'red', }, text: { text: '{ x: 0.6, y: 32, angle: 45 }', fill: 'red', }, }, }, { id: 'port3', group: 'group1', // 通过 args 指定绝对位置 args: { x: '100%', y: '100%', }, attrs: { text: { text: "{ x: '100%', y: '100%' }" }, }, label: { position: { name: 'right', }, }, }, ], }, }) ``` ### 自定义连接桩位置 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/port-position-custom/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/port-position-custom/ - Source path: examples/node/port/demo/port-position-custom.ts ```ts import { Graph } from '@antv/x6' Graph.registerPortLayout( 'sin', (portsPositionArgs, elemBBox) => { return portsPositionArgs.map((_, index) => { const step = -Math.PI / 8 const y = Math.sin(index * step) * 50 return { position: { x: index * 12, y: y + elemBBox.height, }, angle: 0, } }) }, true, ) const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const rect = graph.addNode({ x: 100, y: 60, width: 280, height: 120, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, ports: { groups: { sin: { attrs: { circle: { r: 6, magnet: true, stroke: '#31d0c6', strokeWidth: 2, fill: '#fff', }, }, position: 'sin', }, }, }, }) Array.from({ length: 24 }).forEach(() => { rect.addPort({ group: 'sin' }) }) ``` ### 沿椭圆分布的连接桩 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/port-position-ellipse/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/port-position-ellipse/ - Source path: examples/node/port/demo/port-position-ellipse.ts ```ts import { Graph, Point } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) function getPathData(deg: number) { const center = new Point(180, 100) const start = new Point(180, 0) const ratio = center.x / center.y const p = start .clone() .rotate(90 - deg, center) .scale(ratio, 1, center) return `M ${center.x} ${center.y} ${p.x} ${p.y}` } const node = graph.addNode({ x: 100, y: 60, width: 400, height: 200, markup: [ { tagName: 'ellipse', selector: 'body' }, { tagName: 'path', selector: 'line' }, ], attrs: { body: { refCx: '50%', refCy: '50%', refRx: '50%', refRy: '50%', fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, line: { d: getPathData(45), stroke: '#73d13d', strokeWidth: 1, strokeDasharray: '5 5', }, }, ports: { groups: { group1: { markup: [ { tagName: 'rect', selector: 'rect', }, { tagName: 'circle', selector: 'dot', }, ], attrs: { rect: { magnet: true, stroke: '#31d0c6', fill: 'rgba(255,255,255,0.8)', strokeWidth: 2, width: 16, height: 16, x: -8, y: -8, }, dot: { fill: '#fe854f', r: 2, }, text: { fontSize: 12, fill: '#6a6c8a', }, }, // https://x6.antv.antgroup.com/api/registry/port-label-layout#radial label: { position: 'radial', }, // https://x6.antv.antgroup.com/api/registry/port-layout#ellipse position: { name: 'ellipse', args: { start: 45, }, }, }, }, }, }) Array.from({ length: 10 }).forEach((_, index) => { node.addPort({ id: `${index}`, group: 'group1', attrs: { text: { text: index } }, }) }) node.portProp('0', { attrs: { rect: { stroke: 'red' }, dot: { fill: '#31d0c6' }, }, }) ``` ### 沿圆弧自动旋转的连接桩 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/port-position-ellipse-compensation/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/port-position-ellipse-compensation/ - Source path: examples/node/port/demo/port-position-ellipse-compensation.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const node = graph.addNode({ x: 100, y: 60, width: 400, height: 200, shape: 'ellipse', attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, label: { text: 'outside', fill: '#888', fontSize: 12, }, }, ports: { groups: { a: { markup: [ { tagName: 'rect', selector: 'rect', }, { tagName: 'circle', selector: 'dot', }, ], position: { name: 'ellipseSpread', args: { start: 0, dr: 0, compensateRotate: true }, }, label: { position: 'radial', }, attrs: { rect: { stroke: '#31d0c6', fill: '#ffffff', strokeWidth: 2, width: 20, height: 20, x: -10, y: -10, }, dot: { fill: '#fe854f', r: 2, }, text: { fill: '#6a6c8a', }, }, }, }, }, }) Array.from({ length: 24 }).forEach((_, index) => { node.addPort({ group: 'a', id: `${index}`, attrs: { text: { text: index } }, }) }) function toggle() { const path = 'ports/groups/a/position/args/compensateRotate' const current = node.prop<boolean>(path) node.prop('attrs/label/text', `compensateRotate: ${!current}`) node.prop(path, !current) setTimeout(toggle, 1000) } toggle() ``` ### 沿椭圆均匀分布的连接桩 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/port-position-ellipse-spread/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/port-position-ellipse-spread/ - Source path: examples/node/port/demo/port-position-ellipse-spread.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const node = graph.addNode({ x: 100, y: 60, width: 400, height: 200, shape: 'ellipse', attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, ports: { groups: { group1: { markup: [ { tagName: 'rect', selector: 'rect', }, { tagName: 'circle', selector: 'dot', }, ], attrs: { rect: { magnet: true, stroke: '#31d0c6', fill: 'rgba(255,255,255,0.8)', strokeWidth: 2, width: 16, height: 16, x: -8, y: -8, }, dot: { fill: '#fe854f', r: 2, }, text: { fontSize: 12, fill: '#6a6c8a', }, }, // https://x6.antv.antgroup.com/api/registry/port-label-layout#radial label: { position: 'radial', }, // https://x6.antv.antgroup.com/api/registry/port-layout#ellipsespread position: { name: 'ellipseSpread', args: { start: 45, }, }, }, }, }, }) Array.from({ length: 10 }).forEach((_, index) => { node.addPort({ id: `${index}`, group: 'group1', attrs: { text: { text: index } }, }) }) node.portProp('0', { attrs: { rect: { stroke: 'red' }, dot: { fill: '#31d0c6' }, }, }) ``` ### 沿任意直线均匀分布的连接桩 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/port-position-line/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/port-position-line/ - Source path: examples/node/port/demo/port-position-line.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) graph.addNode({ x: 100, y: 60, width: 280, height: 120, markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'path', selector: 'line', }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, line: { d: 'M 0 0 280 120', stroke: '#73d13d', strokeWidth: 1, strokeDasharray: '5 5', }, }, ports: { groups: { group1: { attrs: { circle: { r: 6, magnet: true, stroke: '#31d0c6', strokeWidth: 2, fill: '#fff', }, }, position: { name: 'line', args: { start: { x: 0, y: 0 }, end: { x: 280, y: 120 }, }, }, }, }, items: [ { id: 'port1', group: 'group1', }, { id: 'port2', group: 'group1', args: { angle: 45 }, markup: [ { tagName: 'path', selector: 'path', }, ], attrs: { path: { d: 'M -6 -8 L 0 8 L 6 -8 Z', magnet: true, fill: 'red' }, }, }, { id: 'port3', group: 'group1', }, ], }, }) ``` ### 沿矩形边框均匀分布的连接桩 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/port-position-side/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/port-position-side/ - Source path: examples/node/port/demo/port-position-side.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const rect = graph.addNode({ x: 100, y: 60, width: 280, height: 120, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, label: { text: 'left', fill: '#888', fontSize: 12, }, }, ports: { groups: { group1: { attrs: { circle: { r: 6, magnet: true, stroke: '#31d0c6', strokeWidth: 2, fill: '#fff', }, text: { fontSize: 12, fill: '#888', }, }, // 文档:https://x6.antv.antgroup.com/api/registry/port-layout#left-right-top-bottom position: { name: 'left', }, }, }, items: [ { id: 'port1', group: 'group1', }, { id: 'port2', group: 'group1', args: { angle: 45 }, markup: [ { tagName: 'path', selector: 'path', }, ], attrs: { path: { d: 'M -6 -8 L 0 8 L 6 -8 Z', magnet: true, fill: 'red', }, }, }, { id: 'port3', group: 'group1', }, ], }, }) const sides = ['left', 'top', 'right', 'bottom'] let index = 0 function run() { index += 1 if (index === sides.length) { index = 0 } rect.prop('ports/groups/group1/position', { name: sides[index], }) rect.attr('label/text', sides[index]) setTimeout(run, 1000) } run() ``` ### 使用 React 渲染连接桩 - zh page: https://x6.antv.antgroup.com/zh/examples/node/port/render-port-with-react/ - en page: https://x6.antv.antgroup.com/en/examples/node/port/render-port-with-react/ - Source path: examples/node/port/demo/render-port-with-react.tsx ```tsx import { Graph, Markup } from '@antv/x6' import { Tooltip } from 'antd' import insertCss from 'insert-css' import React from 'react' import { createRoot } from 'react-dom/client' const graph = new Graph({ container: document.getElementById('container'), grid: true, onPortRendered(args) { const selectors = args.contentSelectors const container = selectors && selectors.foContent if (container) { createRoot(container as HTMLElement).render( ( <Tooltip title="port"> <div className="my-port" /> </Tooltip> ) as any, ) } }, }) graph.addNode({ x: 100, y: 60, width: 280, height: 120, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, portMarkup: [Markup.getForeignObjectMarkup()], ports: { items: [ { group: 'in', id: 'in1' }, { group: 'in', id: 'in2' }, { group: 'out', id: 'out1' }, { group: 'out', id: 'out2' }, ], groups: { in: { position: { name: 'top' }, attrs: { fo: { width: 12, height: 12, x: -6, y: -6, magnet: 'true', }, }, zIndex: 1, }, out: { position: { name: 'bottom' }, attrs: { fo: { width: 12, height: 12, x: -6, y: -6, magnet: 'true', }, }, zIndex: 1, }, }, }, }) insertCss(` .my-port { width: 100%; height: 100%; border: 2px solid #31d0c6; border-radius: 100%; background: #fff; } `) ``` ### 包围框 - zh page: https://x6.antv.antgroup.com/zh/examples/node/tool/boundary/ - en page: https://x6.antv.antgroup.com/en/examples/node/tool/boundary/ - Source path: examples/node/tool/demo/boundary.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 180, y: 60, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, tools: [ { name: 'boundary', args: { padding: 5, attrs: { fill: '#7c68fc', stroke: '#9254de', strokeWidth: 1, fillOpacity: 0.2, }, }, }, ], }) const target = graph.addNode({ x: 320, y: 250, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#a0a0a0', strokeWidth: 1, targetMarker: { name: 'classic', size: 7, }, }, }, }) graph.on('node:mouseenter', ({ node }) => { if (node === target) { node.addTools({ name: 'boundary', args: { attrs: { fill: '#7c68fc', stroke: '#9254de', strokeWidth: 1, fillOpacity: 0.2, }, }, }) } }) graph.on('node:mouseleave', ({ node }) => { if (node === target) { node.removeTools() } }) ``` ### 自定义按钮 - zh page: https://x6.antv.antgroup.com/zh/examples/node/tool/button/ - en page: https://x6.antv.antgroup.com/en/examples/node/tool/button/ - Source path: examples/node/tool/demo/button.ts ```ts import { Graph, Cell, Color } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container'), grid: true, }) const source = graph.addNode({ x: 180, y: 60, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, tools: [ { name: 'button', args: { markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 14, stroke: '#fe854f', strokeWidth: 2, fill: 'white', cursor: 'pointer', }, }, { tagName: 'text', textContent: 'Btn', selector: 'icon', attrs: { fill: '#fe854f', fontSize: 10, textAnchor: 'middle', pointerEvents: 'none', y: '0.3em', }, }, ], x: '100%', y: '100%', offset: { x: -20, y: -20 }, onClick({ cell }: { cell: Cell }) { const fill = Color.randomHex() cell.attr({ body: { fill, }, label: { fill: Color.invert(fill, true), }, }) }, }, }, ], }) const target = graph.addNode({ x: 320, y: 250, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#a0a0a0', strokeWidth: 1, targetMarker: { name: 'classic', size: 7, }, }, }, }) graph.on('node:mouseenter', ({ node }) => { if (node === target) { node.addTools({ name: 'button', args: { markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 14, stroke: '#fe854f', strokeWidth: 2, fill: 'white', cursor: 'pointer', }, }, { tagName: 'text', textContent: 'Btn', selector: 'icon', attrs: { fill: '#fe854f', fontSize: 10, textAnchor: 'middle', pointerEvents: 'none', y: '0.3em', }, }, ], x: 0, y: 0, offset: { x: 20, y: 20 }, onClick({ cell }: { cell: Cell }) { cell.attr({ body: { stroke: Color.randomHex(), strokeDasharray: '5, 1', strokeDashoffset: (cell.attr<number>('line/strokeDashoffset') | 0) + 20, }, }) }, }, }) } }) graph.on('node:mouseleave', ({ cell }) => { if (cell === target) { cell.removeTools() } }) ``` ### 删除按钮 - zh page: https://x6.antv.antgroup.com/zh/examples/node/tool/button-remove/ - en page: https://x6.antv.antgroup.com/en/examples/node/tool/button-remove/ - Source path: examples/node/tool/demo/button-remove.ts ```ts import { Graph } from '@antv/x6' const graph = new Graph({ container: document.getElementById('container')!, grid: true, }) const source = graph.addNode({ x: 180, y: 60, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, tools: [ { name: 'button-remove', args: { x: '100%', y: 0, offset: { x: -10, y: 10 }, }, }, ], }) const target = graph.addNode({ x: 320, y: 250, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1, }, }, }) graph.addEdge({ source, target, attrs: { line: { stroke: '#a0a0a0', strokeWidth: 1, targetMarker: { name: 'classic', size: 7, }, }, }, }) graph.on('node:mouseenter', ({ node }) => { if (node === target) { node.addTools({ name: 'button-remove', args: { x: 0, y: 0, offset: { x: 10, y: 10 }, }, }) } }) graph.on('node:mouseleave', ({ node }) => { if (node === target) { node.removeTools() } }) ``` ### 文本编辑 - zh page: https://x6.antv.antgroup.com/zh/examples/node/tool/editable/ - en page: https://x6.antv.antgroup.com/en/examples/node/tool/editable/ - Source path: examples/node/tool/demo/editable.ts ```ts import { Graph } from '@antv/x6' const container = document.getElementById('container')! const graph = new Graph({ container, grid: true, }) const source = graph.addNode({ x: 180, y: 60, width: 100, height: 40, attrs: { body: { stroke: '#5F95FF', fill: '#EFF4FF', strokeWidth: 1, }, }, tools: [ { name: 'node-editor', args: { attrs: { backgroundColor: '#EFF4FF', }, }, }, ], }) const target = graph.addNode({ x: 320, y: 250, width: 100, height: 40, attrs: { body: { stroke: '#5F95FF', fill: '#EFF4FF', strokeWidth: 1, }, }, tools: [ { name: 'node-editor', args: { attrs: { backgroundColor: '#EFF4FF', }, }, }, ], }) graph.addEdge({ source, target, attrs: { line: { stroke: '#A2B1C3', strokeWidth: 2, }, }, tools: [ { name: 'edge-editor', args: { attrs: { backgroundColor: '#fff', }, }, }, ], }) ``` ### 连接桩 ToolTip - zh page: https://x6.antv.antgroup.com/zh/examples/node/tool/port-tooltip/ - en page: https://x6.antv.antgroup.com/en/examples/node/tool/port-tooltip/ - Source path: examples/node/tool/demo/port-tooltip.tsx ```tsx import { Graph } from '@antv/x6' import { Tooltip } from 'antd' import React from 'react' import { createRoot } from 'react-dom/client' const registerPortTooltip = (container: Element, text: string) => { container.addEventListener('mouseenter', (e: MouseEvent) => { const tooltip = document.querySelector('.x6-tooltip') as HTMLElement const content = tooltip?.querySelector('.ant-tooltip-inner') as HTMLElement if (content) { content.innerText = text tooltip.style.left = `${e.clientX - content.offsetWidth / 2}px` tooltip.style.top = `${e.clientY - 50}px` } }) container.addEventListener('mouseleave', () => { const tooltip = document.querySelector('.x6-tooltip') as HTMLElement tooltip.style.left = '-1000px' tooltip.style.top = '-1000px' }) } class Example extends React.Component { private container!: HTMLDivElement componentDidMount() { const graph = new Graph({ container: this.container, width: 800, height: 600, grid: true, onPortRendered({ contentContainer, port, node }) { const text = node.portProp(port.id, 'tip') as string registerPortTooltip(contentContainer, text) }, }) const source = graph.addNode({ x: 180, y: 60, width: 100, height: 40, attrs: { body: { stroke: '#5F95FF', fill: '#EFF4FF', strokeWidth: 1, }, }, ports: { groups: { bottom: { position: 'bottom', attrs: { circle: { stroke: '#5F95FF', strokeWidth: 1, r: 4, magnet: true, }, }, }, }, items: [ { id: 'port-1', group: 'bottom', tip: 'port-1-tip', }, ], }, }) const target = graph.addNode({ x: 320, y: 250, width: 100, height: 40, attrs: { body: { stroke: '#5F95FF', fill: '#EFF4FF', strokeWidth: 1, }, }, ports: { groups: { top: { position: 'top', attrs: { circle: { stroke: '#5F95FF', strokeWidth: 1, r: 4, }, }, }, }, items: [ { id: 'port-2', group: 'top', tip: 'port-2-tip', }, ], }, }) graph.addEdge({ source: { cell: source, port: 'port-1' }, target: { cell: target, port: 'port-2' }, attrs: { line: { stroke: '#A2B1C3', strokeWidth: 2, }, }, zIndex: -1, }) } refContainer = (container: HTMLDivElement) => { this.container = container } render() { return ( <div className="x6-graph-wrap"> <div ref={this.refContainer} className="x6-graph" /> <Tooltip title="content" overlayClassName="x6-tooltip" visible={true}> <span style={{ position: 'relative', left: -1000, top: -1000 }} /> </Tooltip> </div> ) } } const container = document.getElementById('container') const root = createRoot(container) root.render(<Example />) // ReactDOM.render(<Example />, document.getElementById('container')) ``` ### 智能体流程编排 - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/agentflow/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/agentflow/ - Source path: examples/showcase/practices/demo/agentFlow.ts ```ts import exampleData from '../../../data/agent-flow.json' import { type Edge, Graph, type Node, Snapline, Stencil } from '@antv/x6' import { register } from '@antv/x6-react-shape' import insertCss from 'insert-css' import React from 'react' // 为了协助代码演示 preWork() const CARD_WIDTH = 260 const CARD_HEIGHT = 96 const PORT_DOT_RADIUS = 3 const COLOR_PORT_GRAY = '#C2C8D5' const COLOR_PORT_BLUE = '#5F95FF' const graph = new Graph({ container: document.getElementById('graph-container') as HTMLElement, grid: true, mousewheel: { enabled: true, minScale: 0.5, maxScale: 3, }, connecting: { connector: { name: 'smooth' }, connectionPoint: 'anchor', allowBlank: false, snap: { radius: 20 }, allowEdge: false, allowLoop: false, highlight: true, createEdge(): Edge { return graph.createEdge({ shape: 'agent-edge' }) }, validateConnection({ targetMagnet, }: { targetMagnet?: Element | null }): boolean { return !!targetMagnet }, }, highlighting: { magnetAdsorbed: { name: 'stroke', args: { attrs: { fill: COLOR_PORT_BLUE, stroke: COLOR_PORT_BLUE, }, }, }, }, }) graph.use(new Snapline()) Graph.registerEdge( 'agent-edge', { inherit: 'edge', attrs: { line: { stroke: COLOR_PORT_BLUE, strokeWidth: 2, targetMarker: 'block' }, }, }, true, ) const stencil = new Stencil({ title: '智能体流程编排', target: graph, stencilGraphWidth: 240, stencilGraphHeight: 480, stencilGraphOptions: { panning: true }, collapsable: true, groups: [ { title: '业务逻辑', name: 'biz', graphHeight: 380, layoutOptions: { rowHeight: 88 }, }, { title: '知识库&数据', name: 'data', graphHeight: 300, layoutOptions: { rowHeight: 88 }, }, ], layoutOptions: { columns: 1, columnWidth: 230, rowHeight: 88, dx: 8 }, }) document .getElementById('stencil') ?.appendChild(stencil.container as HTMLElement) const basePortAttrs = { r: PORT_DOT_RADIUS, magnet: true, stroke: COLOR_PORT_GRAY, strokeWidth: 1, fill: COLOR_PORT_GRAY, style: { visibility: 'hidden' }, } const createPortGroup = (position: 'top' | 'right' | 'bottom' | 'left') => ({ position, attrs: { circle: { ...basePortAttrs } }, }) const ports = { groups: { top: createPortGroup('top'), right: createPortGroup('right'), bottom: createPortGroup('bottom'), left: createPortGroup('left'), }, items: [ { id: 'top', group: 'top' }, { id: 'right', group: 'right' }, { id: 'bottom', group: 'bottom' }, { id: 'left', group: 'left' }, ], } const isPortConnected = (node: Node, portId: string): boolean => { const edges = graph.getConnectedEdges(node) return edges.some( (e) => (e.getSourceCellId() === node.id && e.getSourcePortId() === portId) || (e.getTargetCellId() === node.id && e.getTargetPortId() === portId), ) } const setPortVisible = (node: Node, portId: string, visible: boolean) => { node.setPortProp( portId, 'attrs/circle/style/visibility', visible ? 'visible' : 'hidden', ) } const setPortColor = (node: Node, portId: string, color: string) => { node.setPortProp(portId, 'attrs/circle/fill', color) node.setPortProp(portId, 'attrs/circle/stroke', color) } const setPortDot = ( node: Node, portId: string, visible: boolean, color?: string, ) => { setPortVisible(node, portId, visible) if (color) setPortColor(node, portId, color) } const withNodePort = ( cellId?: string | null, portId?: string | null, fn?: (node: Node, portId: string) => void, ) => { if (!cellId || !portId || !fn) return const cell = graph.getCellById(cellId) if (cell && cell.isNode()) fn(cell as Node, portId) } const showNodePorts = (node: Node, show: boolean) => { const ps = node.getPorts() for (let i = 0; i < ps.length; i += 1) { const id = ps[i].id as string if (show) { setPortVisible(node, id, true) } else { const connected = isPortConnected(node, id) setPortVisible(node, id, connected) setPortColor(node, id, connected ? COLOR_PORT_BLUE : COLOR_PORT_GRAY) } } } graph.on('node:mouseenter', ({ node }: { node: Node }) => { showNodePorts(node, true) }) graph.on('node:mouseleave', ({ node }: { node: Node }) => { showNodePorts(node, false) }) graph.on('edge:mouseenter', ({ edge }: { edge: Edge }) => { edge.addTools({ name: 'button-remove', args: { distance: -40 } }) }) graph.on('edge:mouseleave', ({ edge }: { edge: Edge }) => { edge.removeTools() }) graph.on( 'edge:connected', ({ currentCell, currentPort, }: { currentCell: Node currentPort?: string }) => { if (!currentPort) return setPortDot(currentCell, currentPort, true, COLOR_PORT_BLUE) }, ) graph.on('edge:added', ({ edge }: { edge: Edge }) => { withNodePort(edge.getSourceCellId(), edge.getSourcePortId(), (node, port) => setPortDot(node, port, true, COLOR_PORT_BLUE), ) withNodePort(edge.getTargetCellId(), edge.getTargetPortId(), (node, port) => setPortDot(node, port, true, COLOR_PORT_BLUE), ) }) graph.on('edge:removed', ({ edge }: { edge: Edge }) => { withNodePort(edge.getSourceCellId(), edge.getSourcePortId(), (node, port) => { const stillConnected = isPortConnected(node, port) if (!stillConnected) setPortDot(node, port, false, COLOR_PORT_GRAY) }) withNodePort(edge.getTargetCellId(), edge.getTargetPortId(), (node, port) => { const stillConnected = isPortConnected(node, port) if (!stillConnected) setPortDot(node, port, false, COLOR_PORT_GRAY) }) }) type AgentCardConfig = { key: string iconText: string title: string desc: string theme?: 'blue' | 'green' | 'orange' | 'red' inputPlaceholder?: string } const AgentReactCard = ({ node }: { node: Node }) => { const raw = node.getData() as AgentCardConfig | null | undefined const data: AgentCardConfig = { key: raw?.key ?? 'unknown', iconText: raw?.iconText ?? '', title: raw?.title ?? '', desc: raw?.desc ?? '', theme: raw?.theme ?? 'blue', inputPlaceholder: raw?.inputPlaceholder, } const theme = data.theme ?? 'blue' return React.createElement('div', { className: `agent-card ${theme}` }, [ React.createElement('div', { className: 'header', key: 'header' }, [ React.createElement( 'div', { className: 'icon', key: 'icon' }, data.iconText, ), React.createElement( 'div', { className: 'title', key: 'title' }, data.title, ), React.createElement('div', { className: 'actions', key: 'actions' }, [ React.createElement( 'span', { className: 'op', key: 'delete', onClick: (e: React.MouseEvent) => { e.stopPropagation() const cfg = (node.getData() as AgentCardConfig | null) || null const k = cfg?.key if (k === 'start' || k === 'end') return node.remove() }, title: '删除节点', }, '✖️', ), ]), ]), data.inputPlaceholder ? React.createElement('div', { className: 'body', key: 'body' }, [ React.createElement( 'span', { className: 'section', key: 'sec' }, '节点内容', ), React.createElement('input', { type: 'text', placeholder: data.inputPlaceholder, }), ]) : React.createElement( 'div', { className: 'desc', key: 'desc' }, data.desc, ), ]) } register({ shape: 'agent-react-card', width: CARD_WIDTH, height: CARD_HEIGHT, effect: ['data'], component: AgentReactCard, }) const createAgentCard = (cfg: AgentCardConfig) => graph.createNode({ shape: 'agent-react-card', width: CARD_WIDTH, height: CARD_HEIGHT, data: cfg, ports, }) type FlowData = { title: string badge?: string } const FlowCard = ({ node }: { node: Node }) => { const data = node.getData() as FlowData const shape = node.shape const type = shape === 'agent-end-card' ? 'end' : 'start' return React.createElement('div', { className: `flow-card ${type}` }, [ React.createElement('div', { className: 'header', key: 'h' }, [ React.createElement( 'div', { className: 'icon', key: 'i' }, type === 'end' ? 'E' : 'S', ), React.createElement('div', { className: 'title', key: 't' }, data.title), data.badge ? React.createElement( 'div', { className: 'badge', key: 'b' }, data.badge, ) : null, ]), type === 'start' ? React.createElement('div', { className: 'body', key: 'body' }, [ React.createElement( 'span', { className: 'section', key: 's' }, 'Agent 开始节点', ), ]) : React.createElement('div', { className: 'footer', key: 'f' }, [ React.createElement( 'span', { className: 'section', key: 's2' }, 'Agent 结束节点', ), ]), ]) } const registerFlowShape = (shape: string) => { register({ shape, width: CARD_WIDTH, height: CARD_HEIGHT, effect: ['data'], component: FlowCard, }) } registerFlowShape('agent-start-card') registerFlowShape('agent-end-card') const createFlowCard = (kind: 'start' | 'end', data: FlowData) => graph.createNode({ shape: kind === 'start' ? 'agent-start-card' : 'agent-end-card', width: CARD_WIDTH, height: CARD_HEIGHT, data, ports, }) const createStartCard = () => createFlowCard('start', { title: '开始', badge: '触发器', }) const createEndCard = () => createFlowCard('end', { title: '结束', badge: '输出器', }) Graph.registerNode( 'agent-stencil-card', { inherit: 'rect', width: 220, height: 66, markup: [ { tagName: 'rect', selector: 'body' }, { tagName: 'rect', selector: 'iconRect' }, { tagName: 'text', selector: 'iconLabel' }, { tagName: 'text', selector: 'title' }, { tagName: 'text', selector: 'desc' }, ], attrs: { body: { stroke: '#5F95FF', strokeWidth: 1, fill: '#fff', rx: 8, ry: 8 }, iconRect: { width: 32, height: 32, rx: 8, ry: 8, refX: 12, refY: 20, fill: '#F0F5FF', }, iconLabel: { refX: 28, refY: 36, textAnchor: 'middle', textVerticalAnchor: 'middle', fontSize: 12, fontWeight: 600, fill: '#1D39C4', }, title: { refX: 60, refY: 26, textAnchor: 'start', textVerticalAnchor: 'middle', fontSize: 14, fontWeight: 600, fill: '#141414', textWrap: { width: 170, height: 20, ellipsis: '…' }, }, desc: { refX: 60, refY: 46, textAnchor: 'start', textVerticalAnchor: 'middle', fontSize: 12, fill: 'rgba(0,0,0,0.65)', textWrap: { width: 170, height: 32, ellipsis: '…' }, }, }, ports, }, true, ) const createAgentStencilCard = (cfg: AgentCardConfig) => { const themeMap = { blue: { rectFill: '#F0F5FF', textFill: '#1D39C4' }, green: { rectFill: '#E6FFFB', textFill: '#08979C' }, orange: { rectFill: '#FFF7E6', textFill: '#FA8C16' }, red: { rectFill: '#FFF1F0', textFill: '#CF1322' }, } as const const theme = cfg.theme ?? 'blue' const colors = themeMap[theme] return graph.createNode({ shape: 'agent-stencil-card', attrs: { iconRect: { fill: colors.rectFill }, iconLabel: { text: cfg.iconText, fill: colors.textFill }, title: { text: cfg.title }, desc: { text: cfg.desc }, }, data: { type: cfg.key }, ports, }) } let AGENT_CONFIGS: Record<string, AgentCardConfig> | null = null const getAgentConfig = (type: string): AgentCardConfig | null => { return AGENT_CONFIGS ? (AGENT_CONFIGS[type] ?? null) : null } graph.on('node:added', ({ node }: { node: Node }) => { const data = node.getData() as { type?: string } | undefined const type = data?.type if (type && node.shape !== 'agent-react-card') { const cfg = getAgentConfig(type) if (cfg) { const { x, y } = node.position() const newNode = createAgentCard(cfg).position(x, y) node.remove() graph.addNode(newNode) } } }) ;(async () => { AGENT_CONFIGS = await Promise.resolve(exampleData) stencil.load( [ createAgentStencilCard(getAgentConfig('llm')!), createAgentStencilCard(getAgentConfig('code')!), createAgentStencilCard(getAgentConfig('branch')!), createAgentStencilCard(getAgentConfig('loop')!), ], 'biz', ) stencil.load( [ createAgentStencilCard(getAgentConfig('kb')!), createAgentStencilCard(getAgentConfig('mcp')!), createAgentStencilCard(getAgentConfig('db')!), ], 'data', ) const start = graph.addNode(createStartCard().position(60, 60)) const llm = graph.addNode( createAgentCard(getAgentConfig('llm')!).position(220, 220), ) const end = graph.addNode(createEndCard().position(360, 380)) graph.addEdge({ shape: 'agent-edge', source: { cell: start.id, port: 'bottom' }, target: { cell: llm.id, port: 'top' }, }) graph.addEdge({ shape: 'agent-edge', source: { cell: llm.id, port: 'bottom' }, target: { cell: end.id, port: 'top' }, }) })().catch((err) => { console.error(err) }) function preWork() { const container = document.getElementById('container') as HTMLElement if (!container) { return } const stencilContainer = document.createElement('div') stencilContainer.id = 'stencil' const graphContainer = document.createElement('div') graphContainer.id = 'graph-container' container.appendChild(stencilContainer) container.appendChild(graphContainer) insertCss(` #container { display: flex; border: 1px solid #dfe3e8; height: 480px; } #stencil { width: 260px; height: 100%; position: relative; border-right: 1px solid #dfe3e8; background: #fff; } #graph-container { width: calc(100% - 260px); height: 100%; background: #fff; } .x6-widget-stencil { background-color: #fff; } .x6-widget-stencil-title { background-color: #fff; } .x6-widget-stencil-group-title { background-color: #fff !important; } /* agent card */ .agent-card { display: flex; flex-direction: column; border: 1px solid #5F95FF; border-radius: 8px; box-sizing: border-box; padding: 12px; width: 260px; height: 96px; background: #fff; gap: 8px; } .agent-card .header { display: flex; align-items: center; gap: 12px; } .agent-card .icon { width: 32px; height: 32px; border-radius: 8px; background: #F0F5FF; color: #1D39C4; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; font-family: Inter, PingFang SC, Arial; } .agent-card.green .icon { background: #E6FFFB; color: #08979C; } .agent-card.orange .icon { background: #FFF7E6; color: #FA8C16; } .agent-card.red .icon { background: #FFF1F0; color: #CF1322; } .agent-card .title { font-size: 16px; color: #141414; font-weight: 600; line-height: 20px; } .agent-card .desc { font-size: 13px; color: rgba(0,0,0,0.65); line-height: 18px; } .agent-card .body { display: flex; align-items: center; gap: 8px; } .agent-card .section { font-size: 12px; color: #8c8c8c; flex: 0 0 auto; white-space: nowrap; min-width: max-content; } .agent-card .body input { flex: 1; min-width: 0; height: 30px; border: 1px solid #5F95FF; border-radius: 6px; padding: 4px 8px; background: #FAFAFA; color: #141414; } .agent-card .actions { margin-left: auto; color: #8c8c8c; display: flex; gap: 8px; } .agent-card .actions .op { font-size: 14px; cursor: pointer; } /* start/end flow cards */ .flow-card { display: flex; flex-direction: column; border: 1px solid #5F95FF; border-radius: 12px; background: #fff; width: 260px; height: 96px; box-sizing: border-box; padding: 12px; gap: 8px; } .flow-card .header { display: flex; align-items: center; gap: 12px; } .flow-card .icon { width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 14px; } .flow-card.start .icon { background: #EEF2FF; color: #5F95FF; } .flow-card.end .icon { background: #FFF1F0; color: #FF7875; } .flow-card .title { font-size: 16px; color: #141414; font-weight: 600; } .flow-card .badge { margin-left: auto; font-size: 12px; color: #8c8c8c; background: #F5F5F5; border-radius: 8px; padding: 2px 8px; } .flow-card .body { display: flex; align-items: center; gap: 8px; } .flow-card .section { font-size: 12px; color: #8c8c8c; } .flow-card .chips { display: flex; align-items: center; gap: 6px; flex: 1; flex-wrap: wrap; } .flow-card .chip { font-size: 12px; color: #595959; background: #FAFAFA; border: 1px solid #5F95FF; border-radius: 6px; padding: 2px 8px; } .flow-card .actions { margin-left: auto; color: #8c8c8c; } .flow-card .footer { display: flex; align-items: center; gap: 8px; } `) } ``` ### examples/data/agent-flow.json ```json { "llm": { "key": "llm", "iconText": "LLM", "title": "文本大模型", "desc": "处理文本指令与上下文。", "inputPlaceholder": "输入", "theme": "blue" }, "code": { "key": "code", "iconText": "</>", "title": "代码", "desc": "运行脚本和逻辑。", "inputPlaceholder": "输入", "theme": "green" }, "branch": { "key": "branch", "iconText": "IF", "title": "分支", "desc": "根据条件执行不同业务逻辑。", "inputPlaceholder": "输入", "theme": "orange" }, "loop": { "key": "loop", "iconText": "FOR", "title": "循环", "desc": "迭代处理重复执行步骤。", "inputPlaceholder": "输入", "theme": "orange" }, "kb": { "key": "kb", "iconText": "KB", "title": "知识库", "desc": "检索信息,提供丰富上下文。", "inputPlaceholder": "输入", "theme": "blue" }, "mcp": { "key": "mcp", "iconText": "MCP", "title": "MCP 插件", "desc": "扩展外部能力。", "inputPlaceholder": "输入", "theme": "green" }, "db": { "key": "db", "iconText": "DB", "title": "数据库", "desc": "读写数据,支撑数据持久化。", "inputPlaceholder": "输入", "theme": "blue" } } ``` ### BPMN - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/bpmn/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/bpmn/ - Source path: examples/showcase/practices/demo/bpmn.ts ```ts import exampleData from '../../../data/bpmn.json' import { type Cell, type CellView, Graph, type Node } from '@antv/x6' function registerGateway(name: string, symbol: string) { Graph.registerNode( name, { inherit: 'polygon', attrs: { body: { refPoints: '0,10 10,0 20,10 10,20', strokeWidth: 2, stroke: '#5F95FF', fill: '#EFF4FF', }, label: { text: symbol, fontSize: 32, fontWeight: 'bold', fill: '#5F95FF', }, }, }, true, ) } Graph.registerNode( 'event', { inherit: 'circle', attrs: { body: { strokeWidth: 2, stroke: '#5F95FF', fill: '#FFF', }, }, }, true, ) Graph.registerNode( 'start-event', { inherit: 'circle', attrs: { body: { strokeWidth: 2, stroke: '#52C41A', fill: '#FFF', }, }, }, true, ) Graph.registerNode( 'end-event', { inherit: 'circle', attrs: { body: { strokeWidth: 4, stroke: '#FF4D4F', fill: '#FFF', }, }, }, true, ) Graph.registerNode( 'activity', { inherit: 'rect', markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'image', selector: 'img', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { rx: 6, ry: 6, stroke: '#5F95FF', fill: '#EFF4FF', strokeWidth: 1, }, img: { x: 6, y: 6, width: 16, height: 16, 'xlink:href': 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*pwLpRr7QPGwAAAAAAAAAAAAAARQnAQ', }, label: { fontSize: 12, fill: '#262626', }, }, }, true, ) Graph.registerNode( 'subprocess', { inherit: 'rect', markup: [ { tagName: 'rect', selector: 'body' }, { tagName: 'text', selector: 'label' }, { tagName: 'image', selector: 'badge' }, { tagName: 'text', selector: 'expand' }, { tagName: 'text', selector: 'details' }, ], attrs: { body: { rx: 6, ry: 6, stroke: '#5F95FF', fill: '#FAFBFF', strokeWidth: 1, }, label: { ref: 'body', refX: '50%', refY: '50%', textAnchor: 'middle', textVerticalAnchor: 'middle', fontSize: 12, fill: '#262626', }, badge: { ref: 'body', refX: 0, refY: 0, x: 6, y: 6, width: 16, height: 16, 'xlink:href': 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*pwLpRr7QPGwAAAAAAAAAAAAAARQnAQ', }, expand: { ref: 'body', refX: '100%', refY: '100%', x: -8, y: -8, textVerticalAnchor: 'bottom', textAnchor: 'end', fontSize: 16, fontWeight: 'bold', fill: '#5F95FF', cursor: 'pointer', text: '+', event: 'subproc:toggle', }, details: { ref: 'body', refX: '50%', refY: 44, y: 0, textAnchor: 'middle', textVerticalAnchor: 'top', fontSize: 12, fill: '#8C8C8C', display: 'none', textWrap: { width: -24, }, text: '', }, }, }, true, ) registerGateway('gateway-parallel', '⨁') registerGateway('gateway-exclusive', '×') Graph.registerNode( 'lane', { inherit: 'rect', markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'rect', selector: 'name-rect', }, { tagName: 'text', selector: 'name-text', }, ], attrs: { body: { fill: '#FFF', stroke: '#5F95FF', strokeWidth: 1, }, 'name-rect': { width: 240, height: 30, fill: '#5F95FF', stroke: '#fff', strokeWidth: 0, }, 'name-text': { ref: 'name-rect', refY: 0.5, refX: 0.5, textAnchor: 'middle', fontWeight: 'bold', fill: '#fff', fontSize: 12, }, }, }, true, ) Graph.registerEdge( 'bpmn-edge', { inherit: 'edge', attrs: { line: { stroke: '#A2B1C3', strokeWidth: 2, targetMarker: 'classic', }, }, }, true, ) const graph = new Graph({ container: document.getElementById('container') as HTMLElement, grid: true, connecting: { router: 'orth', }, mousewheel: true, translating: { restrict(this: Graph, cellView: CellView | null) { if (cellView?.cell?.isNode()) { const cell = cellView.cell as Node if (cell.shape === 'lane') { return this.transform.getGraphArea() } const parentId = cell.prop('parent') if (parentId) { const parentNode = this.getCellById(parentId) if (parentNode?.isNode()) { return parentNode.getBBox().moveAndExpand({ x: 0, y: 30, width: 0, height: -30, }) } } return this.transform.getGraphArea() } return this.transform.getGraphArea() }, }, }) graph.on('subproc:toggle', ({ node }: { node: Node }) => { if (!node || node.shape !== 'subprocess') return const data = node.getData() || {} const expanded = !!data.expanded const next = !expanded node.attr('expand/text', next ? '-' : '+') node.attr('details/display', next ? 'block' : 'none') const defaultDetails = '1、病例证明(如有)\n2、工作 backup' const rawDetails = data.details const detailsText = rawDetails ? Array.isArray(rawDetails) ? rawDetails.join('\n') : String(rawDetails) : defaultDetails if (next) { node.attr('details/text', detailsText) } const lines = String(detailsText).split(/\n/).length const size = node.getSize() const expandedHeight = Math.max(70, 40 + lines * 18 + 24) node.size(size.width, next ? expandedHeight : 70) if (next) { node.attr({ label: { textVerticalAnchor: 'top', refY: 18, refX: '50%', ref: 'body' }, details: { textVerticalAnchor: 'top', refY: 40, refX: '50%', ref: 'body', }, }) } else { node.attr('label', { textVerticalAnchor: 'middle', refY: '50%', refX: '50%', ref: 'body', }) } node.setData({ ...data, expanded: next }) }) Promise.resolve(exampleData) .then((data) => { const cells: Cell[] = [] const nodeMap: Record<string, Node> = {} data.forEach((item: any) => { if (item.shape !== 'bpmn-edge') { const node = graph.createNode(item) nodeMap[item.id] = node as Node cells.push(node) } }) data.forEach((item: any) => { if (item.shape === 'bpmn-edge') { const edge = graph.createEdge(item) if (item.label) { edge.setLabels([ { attrs: { label: { text: item.label, fill: '#8C8C8C', fontSize: 11, }, }, }, ]) } const sourceId = edge.getSourceCellId() const targetId = edge.getTargetCellId() const sParent = nodeMap[sourceId]?.prop('parent') const tParent = nodeMap[targetId]?.prop('parent') if (sParent && tParent && sParent !== tParent) { edge.attr('line/strokeDasharray', '5,5') } cells.push(edge) } }) graph.resetCells(cells) Object.values(nodeMap).forEach((node) => { const parentId = node.prop('parent') as string | undefined if (parentId) { const parentNode = nodeMap[parentId] if (parentNode) { parentNode.addChild(node) } } }) Object.values(nodeMap).forEach((node) => { if (node.shape === 'activity' && node.prop('parent') === 'lane-system') { node.attr( 'img/xlink:href', 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*mUotQqntWJQAAAAAF_AAAAgAemJ7AQ/original', ) } }) graph.zoomToFit({ padding: 10, maxScale: 1 }) }) ``` ### examples/data/bpmn.json ```json [ { "id": "lane-employee", "shape": "lane", "width": 240, "height": 600, "position": { "x": 60, "y": 60 }, "attrs": { "name-text": { "text": "员工" } } }, { "id": "lane-system", "shape": "lane", "width": 240, "height": 600, "position": { "x": 320, "y": 60 }, "attrs": { "name-text": { "text": "系统" } } }, { "id": "lane-leader", "shape": "lane", "width": 240, "height": 600, "position": { "x": 580, "y": 60 }, "attrs": { "name-text": { "text": "直属领导" } } }, { "id": "start", "shape": "start-event", "width": 40, "height": 40, "position": { "x": 160, "y": 100 }, "parent": "lane-employee" }, { "id": "apply", "shape": "activity", "width": 120, "height": 60, "position": { "x": 120, "y": 190 }, "label": "提交请假申请", "parent": "lane-employee" }, { "id": "check", "shape": "activity", "width": 120, "height": 60, "position": { "x": 380, "y": 110 }, "label": "自动校验", "parent": "lane-system", "attrs": { "img": { "xlink:href": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*mUotQqntWJQAAAAAF_AAAAgAemJ7AQ/original" } } }, { "id": "approve", "shape": "activity", "width": 120, "height": 60, "position": { "x": 640, "y": 110 }, "label": "领导审批", "parent": "lane-leader" }, { "id": "exgw", "shape": "gateway-exclusive", "width": 40, "height": 40, "position": { "x": 680, "y": 260 }, "parent": "lane-leader" }, { "id": "subproc", "shape": "subprocess", "width": 160, "height": 70, "position": { "x": 100, "y": 330 }, "label": "补充资料", "parent": "lane-employee" }, { "id": "archive", "shape": "activity", "width": 120, "height": 60, "position": { "x": 370, "y": 300 }, "label": "备案与系统更新", "parent": "lane-system", "attrs": { "img": { "xlink:href": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*mUotQqntWJQAAAAAF_AAAAgAemJ7AQ/original" } } }, { "id": "pgw", "shape": "gateway-parallel", "width": 40, "height": 40, "position": { "x": 410, "y": 450 }, "parent": "lane-system" }, { "id": "notify-hr", "shape": "activity", "width": 120, "height": 60, "position": { "x": 370, "y": 540 }, "label": "通知 HR", "parent": "lane-system", "attrs": { "img": { "xlink:href": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*mUotQqntWJQAAAAAF_AAAAgAemJ7AQ/original" } } }, { "id": "confirm", "shape": "activity", "width": 120, "height": 60, "position": { "x": 120, "y": 440 }, "label": "确认请假安排", "parent": "lane-employee" }, { "id": "pgw2", "shape": "gateway-parallel", "width": 40, "height": 40, "position": { "x": 160, "y": 550 }, "parent": "lane-employee" }, { "id": "end", "shape": "end-event", "width": 40, "height": 40, "position": { "x": 160, "y": 610 }, "parent": "lane-employee" }, { "id": "e1", "shape": "bpmn-edge", "source": "start", "target": "apply", "label": "开始" }, { "id": "e2", "shape": "bpmn-edge", "source": "apply", "target": "check" }, { "id": "e3", "shape": "bpmn-edge", "source": "check", "target": "approve" }, { "id": "e4", "shape": "bpmn-edge", "source": "approve", "target": "exgw" }, { "id": "e5", "shape": "bpmn-edge", "source": "exgw", "target": "subproc", "label": "资料不齐" }, { "id": "e6", "shape": "bpmn-edge", "source": "exgw", "target": "archive", "label": "审批通过" }, { "id": "e7", "shape": "bpmn-edge", "source": "subproc", "target": "archive", "label": "补充完成" }, { "id": "e8", "shape": "bpmn-edge", "source": "archive", "target": "pgw" }, { "id": "e9", "shape": "bpmn-edge", "source": "pgw", "target": "notify-hr" }, { "id": "e10", "shape": "bpmn-edge", "source": "pgw", "target": "confirm" }, { "id": "e11", "shape": "bpmn-edge", "source": "notify-hr", "target": "pgw2" }, { "id": "e12", "shape": "bpmn-edge", "source": "confirm", "target": "pgw2" }, { "id": "e13", "shape": "bpmn-edge", "source": "pgw2", "target": "end", "label": "结束" } ] ``` ### UML 类图 - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/class/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/class/ - Source path: examples/showcase/practices/demo/class.ts ```ts import exampleData from '../../../data/class.json' import { Graph, ObjectExt, Cell } from '@antv/x6' // #region 注册基础图形 Graph.registerNode( 'class', { inherit: 'rect', markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'rect', selector: 'name-rect', }, { tagName: 'rect', selector: 'attrs-rect', }, { tagName: 'rect', selector: 'methods-rect', }, { tagName: 'text', selector: 'name-text', }, { tagName: 'text', selector: 'attrs-text', }, { tagName: 'text', selector: 'methods-text', }, ], attrs: { rect: { width: 160, }, body: { stroke: '#fff', }, 'name-rect': { fill: '#5f95ff', stroke: '#fff', strokeWidth: 0.5, }, 'attrs-rect': { fill: '#eff4ff', stroke: '#fff', strokeWidth: 0.5, }, 'methods-rect': { fill: '#eff4ff', stroke: '#fff', strokeWidth: 0.5, }, 'name-text': { ref: 'name-rect', refY: 0.5, refX: 0.5, textAnchor: 'middle', fontWeight: 'bold', fill: '#fff', fontSize: 12, }, 'attrs-text': { ref: 'attrs-rect', refY: 0.5, refX: 5, textAnchor: 'left', fill: 'black', fontSize: 10, }, 'methods-text': { ref: 'methods-rect', refY: 0.5, refX: 5, textAnchor: 'left', fill: 'black', fontSize: 10, }, }, propHooks(meta) { const { name, attributes, methods, ...others } = meta if (!(name && attributes && methods)) { return meta } const rects = [ { type: 'name', text: name }, { type: 'attrs', text: attributes }, { type: 'methods', text: methods }, ] let offsetY = 0 rects.forEach((rect) => { const height = rect.text.length * 12 + 16 ObjectExt.setByPath( others, `attrs/${rect.type}-text/text`, rect.text.join('\n'), ) ObjectExt.setByPath(others, `attrs/${rect.type}-rect/height`, height) ObjectExt.setByPath( others, `attrs/${rect.type}-rect/transform`, 'translate(0,' + offsetY + ')', ) offsetY += height }) others.size = { width: 160, height: offsetY } return others }, }, true, ) // 继承 Graph.registerEdge( 'extends', { inherit: 'edge', attrs: { line: { strokeWidth: 1, targetMarker: { name: 'path', d: 'M 20 0 L 0 10 L 20 20 z', fill: 'white', offsetX: -10, }, }, }, }, true, ) // 实现 Graph.registerEdge( 'implement', { inherit: 'edge', attrs: { line: { strokeWidth: 1, strokeDasharray: '3,3', targetMarker: { name: 'path', d: 'M 20 0 L 0 10 L 20 20 z', fill: 'white', offsetX: -10, }, }, }, }, true, ) // 组合 Graph.registerEdge( 'composition', { inherit: 'edge', attrs: { line: { strokeWidth: 1, sourceMarker: { name: 'path', d: 'M 30 10 L 20 16 L 10 10 L 20 4 z', fill: 'black', offsetX: -10, }, targetMarker: { name: 'path', d: 'M 6 10 L 18 4 C 14.3333 6 10.6667 8 7 10 L 18 16 z', fill: 'black', offsetX: -5, }, }, }, }, true, ) // 聚合 Graph.registerEdge( 'aggregation', { inherit: 'edge', attrs: { line: { strokeWidth: 1, sourceMarker: { name: 'path', d: 'M 30 10 L 20 16 L 10 10 L 20 4 z', fill: 'white', offsetX: -10, }, targetMarker: { name: 'path', d: 'M 6 10 L 18 4 C 14.3333 6 10.6667 8 7 10 L 18 16 z', fill: 'black', offsetX: -5, }, }, }, }, true, ) // 关联 Graph.registerEdge( 'association', { inherit: 'edge', attrs: { line: { strokeWidth: 1, targetMarker: { name: 'path', d: 'M 6 10 L 18 4 C 14.3333 6 10.6667 8 7 10 L 18 16 z', fill: 'black', offsetX: -5, }, }, }, }, true, ) // #endregion const graph = new Graph({ container: document.getElementById('container')!, }) Promise.resolve(exampleData) .then((data) => { const cells: Cell[] = [] const edgeShapes = [ 'extends', 'composition', 'implement', 'aggregation', 'association', ] data.forEach((item: any) => { if (edgeShapes.includes(item.shape)) { cells.push(graph.createEdge(item)) } else { cells.push(graph.createNode(item)) } }) graph.resetCells(cells) graph.zoomToFit({ padding: 10, maxScale: 1 }) }) ``` ### examples/data/class.json ```json [ { "id": "1", "shape": "class", "name": ["<<Abstract>>", "动物"], "attributes": ["+有生命"], "methods": ["+新陈代谢()", "+繁殖()"], "position": { "x": 300, "y": 40 } }, { "id": "2", "shape": "class", "name": ["鸟"], "attributes": ["+羽毛"], "methods": ["+下蛋"], "position": { "x": 300, "y": 200 } }, { "id": "3", "shape": "extends", "source": "2", "target": "1" }, { "id": "4", "shape": "class", "name": ["翅膀"], "attributes": [], "methods": [], "position": { "x": 560, "y": 212 } }, { "id": "5", "shape": "composition", "source": "2", "target": "4", "label": "1:2" }, { "id": "6", "shape": "class", "name": ["大雁"], "attributes": [], "methods": ["+飞()"], "position": { "x": 210, "y": 340 } }, { "id": "7", "shape": "class", "name": ["企鹅"], "attributes": [], "methods": ["+下蛋()"], "position": { "x": 400, "y": 340 } }, { "id": "8", "shape": "extends", "source": "2", "target": "6" }, { "id": "9", "shape": "extends", "source": "2", "target": "7" }, { "id": "10", "shape": "class", "name": ["<<interface>>", "飞翔"], "attributes": [], "methods": ["+飞()"], "position": { "x": 320, "y": 500 } }, { "id": "11", "shape": "implement", "source": "6", "target": "10" }, { "id": "12", "shape": "class", "name": ["雁群"], "attributes": [], "methods": ["+V字飞行()", "+一字飞行()"], "position": { "x": 90, "y": 500 } }, { "id": "13", "shape": "aggregation", "source": "12", "target": "6" }, { "id": "14", "shape": "class", "name": ["气候"], "attributes": [], "methods": [], "position": { "x": 540, "y": 510 } }, { "id": "15", "shape": "association", "source": "7", "target": "14" } ] ``` ### 复杂时间轴 - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/complextimeline/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/complextimeline/ - Source path: examples/showcase/practices/demo/complexTimeline.ts ```ts import exampleData from '../../../data/complex-timeline.json' import { Graph, type Node, type Cell } from '@antv/x6' import { register } from '@antv/x6-react-shape' import insertCss from 'insert-css' import React from 'react' preWork() // 类型定义 interface EventCard { id: string title: string content: string } interface Milestone { id: string headline: string subtitle: string date: string color: string lightColor: string events: EventCard[] } interface PyramidPosition { x: number y: number row: number } interface MilestoneColumnData { headline: string date: string color: string lineHeight: number } // 常量配置 const CONFIG = { card: { width: 80, height: 104, gap: 16 }, layout: { columnSpacing: 40, cardsBottomOffset: 68, milestoneSpacing: 500 }, timeline: { y: 750, startX: 200, endX: 2200, height: 16 }, gradient: 'timeline-gradient', } // React 组件(使用 memo 优化) const MilestoneColumn = React.memo(({ node }: { node: Node }) => { const { headline, date, color, lineHeight } = node.getData() as MilestoneColumnData return React.createElement('div', { className: 'milestone-column' }, [ React.createElement('div', { className: 'milestone-top', key: 'top' }, [ React.createElement('div', { className: 'milestone-big-dot', key: 'dot', style: { backgroundColor: color }, }), React.createElement( 'div', { className: 'milestone-headline', key: 'headline', style: { color }, }, headline, ), ]), React.createElement('div', { className: 'milestone-spine', key: 'spine', style: { backgroundColor: color, height: `${lineHeight}px` }, }), React.createElement( 'div', { className: 'milestone-date', key: 'date' }, date, ), ]) }) const SubtitleNode = React.memo(({ node }: { node: Node }) => { const { subtitle } = node.getData() as { subtitle: string } return React.createElement('div', { className: 'subtitle-text' }, subtitle) }) const EventCardNode = React.memo(({ node }: { node: Node }) => { const { title, bgColor } = node.getData() as { title: string bgColor: string } return React.createElement( 'div', { className: 'event-card-vertical', style: { backgroundColor: bgColor } }, React.createElement('div', { className: 'event-card-title' }, title), ) }) // 注册节点 register({ shape: 'milestone-column-node', width: 250, height: 800, effect: ['data'], component: MilestoneColumn, }) register({ shape: 'subtitle-node', width: 150, height: 20, effect: ['data'], component: SubtitleNode, }) register({ shape: 'event-card-node', width: CONFIG.card.width, height: CONFIG.card.height, effect: ['data'], component: EventCardNode, }) // 创建画布 const graph = new Graph({ container: document.getElementById('graph-container')!, background: { color: '#FFFFFF' }, grid: { size: 24, visible: true, args: { color: '#E0E0E0', thickness: 2 } }, panning: { enabled: true, eventTypes: ['leftMouseDown', 'mouseWheel'] }, mousewheel: { enabled: true, modifiers: 'ctrl', factor: 1.1, maxScale: 3, minScale: 0.1, }, interacting: { nodeMovable: (view) => view.cell.shape === 'event-card-node', edgeMovable: false, }, }) // 定义渐变色 graph.defineGradient({ type: 'linearGradient', id: CONFIG.gradient, attrs: { x1: '0%', y1: '0%', x2: '100%', y2: '0%' }, stops: [ { offset: '0%', color: '#8A2BE2' }, { offset: '33%', color: '#4169E1' }, { offset: '66%', color: '#FFD700' }, { offset: '100%', color: '#2E8B57' }, ], }) // 工具函数:计算金字塔布局 function calculatePyramidLayout( totalCards: number, startX: number, timelineY: number, ): { positions: PyramidPosition[]; topY: number } { let totalRows = 0 let tempCount = 0 while (tempCount < totalCards) { totalRows++ tempCount += totalRows } const rows: Array<{ y: number; count: number }> = [] let currentY = timelineY - CONFIG.layout.cardsBottomOffset let cardIndex = 0 for (let row = totalRows; row >= 1; row--) { const count = Math.min(row, totalCards - cardIndex) if (count <= 0) break const y = currentY - CONFIG.card.height rows.push({ y, count }) cardIndex += count currentY = y - CONFIG.card.gap } rows.reverse() const positions: PyramidPosition[] = [] rows.forEach((row, rowIndex) => { for (let col = 0; col < row.count; col++) { positions.push({ x: startX + col * (CONFIG.card.width + CONFIG.card.gap), y: row.y, row: rowIndex + 1, }) } }) return { positions, topY: rows.length > 0 ? rows[0].y : timelineY - CONFIG.layout.cardsBottomOffset, } } // 工具函数:数据转换 function transformTimelineData(data: Milestone[]): Cell[] { const cells: Cell[] = [] const { timeline, layout } = CONFIG // 时间轴主体 cells.push( graph.createNode({ shape: 'rect', x: timeline.startX, y: timeline.y - timeline.height / 2, width: timeline.endX - timeline.startX, height: timeline.height, attrs: { body: { fill: `url(#${CONFIG.gradient})`, stroke: 'none', rx: 8, ry: 8, }, }, zIndex: 5, }), ) // 时间轴端点 ;[timeline.startX, timeline.endX].forEach((x) => { cells.push( graph.createNode({ shape: 'circle', x: x - 10, y: timeline.y - 10, width: 20, height: 20, attrs: { body: { fill: '#333333', stroke: 'none' } }, zIndex: 10, }), ) }) // 遍历里程碑 data.forEach((milestone, index) => { const baseX = timeline.startX + 100 + index * layout.milestoneSpacing const leftX = baseX const rightX = baseX + layout.columnSpacing const { positions, topY } = calculatePyramidLayout( milestone.events.length, rightX, timeline.y, ) const subtitleY = topY - 35 const headlineY = subtitleY - 45 const spineHeight = timeline.y - (headlineY + 34) const columnHeight = timeline.y - headlineY + 40 // 里程碑列 cells.push( graph.createNode({ id: `${milestone.id}-column`, shape: 'milestone-column-node', x: leftX - 125, y: headlineY, width: 250, height: columnHeight, data: { headline: milestone.headline, date: milestone.date, color: milestone.color, lineHeight: spineHeight, }, zIndex: 3, }), ) // 时间轴圆点 cells.push( graph.createNode({ id: `${milestone.id}-timeline-dot`, shape: 'circle', x: leftX - 9, y: timeline.y - 9, width: 18, height: 18, attrs: { body: { fill: milestone.color, stroke: '#FFFFFF', strokeWidth: 2 }, }, zIndex: 15, }), ) // 副标题 cells.push( graph.createNode({ shape: 'subtitle-node', x: rightX, y: subtitleY, data: { subtitle: milestone.subtitle }, zIndex: 7, }), ) // 事件卡片 milestone.events.forEach((event, eventIndex) => { const pos = positions[eventIndex] cells.push( graph.createNode({ id: event.id, shape: 'event-card-node', x: pos.x, y: pos.y, data: { title: event.title, bgColor: milestone.lightColor }, zIndex: 8, }), ) }) }) return cells } // 交互效果 graph.on('node:mouseenter', ({ node }) => { if (node.shape === 'event-card-node') node.setZIndex(100) }) graph.on('node:mouseleave', ({ node }) => { if (node.shape === 'event-card-node') node.setZIndex(8) }) // 使用 render:done 钩子 let isFirstRender = true graph.on('render:done', () => { if (!isFirstRender) return isFirstRender = false graph.zoomToFit({ padding: 20, maxScale: 1 }) showGraph() }) function showGraph() { document.getElementById('graph-container')?.classList.add('graph-visible') } // 数据加载与渲染 Promise.resolve(exampleData) .then((data) => { const cells = transformTimelineData(data) graph.resetCells(cells) }) .catch(() => { console.error('数据加载失败') }) // 预处理 function preWork() { const container = document.getElementById('container') as HTMLElement if (!container) return const graphContainer = document.createElement('div') graphContainer.id = 'graph-container' container.appendChild(graphContainer) insertCss(` * { margin: 0; padding: 0; box-sizing: border-box; } html, body { width: 100%; height: 100%; overflow: hidden; } #container { width: 100vw; height: 100vh; overflow: hidden; background: #FFFFFF; } #graph-container { width: 100%; height: 100%; background: #FFFFFF; opacity: 0; transition: opacity 0.3s ease-in-out; position: absolute; top: 0; left: 0; } #graph-container.graph-visible { opacity: 1; } .milestone-column { display: flex; flex-direction: column; align-items: center; font-family: Inter, Helvetica, Arial, sans-serif; width: 100%; height: 100%; } .milestone-top { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; } .milestone-big-dot { width: 24px; height: 24px; border-radius: 50%; flex-shrink: 0; } .milestone-headline { font-size: 18px; font-weight: 700; line-height: 1.2; white-space: nowrap; } .milestone-spine { width: 2px; flex-shrink: 0; } .milestone-date { font-size: 12px; color: #888888; text-align: center; margin-top: 30px; } .subtitle-text { font-size: 13px; font-weight: 400; color: #888888; font-family: Inter, Helvetica, Arial, sans-serif; } .event-card-vertical { width: 100%; height: 100%; border-radius: 4px; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.06); display: flex; align-items: center; justify-content: center; padding: 14px 8px; font-family: Inter, Helvetica, Arial, sans-serif; transition: transform 0.2s ease, box-shadow 0.2s ease; cursor: move; } .event-card-vertical:hover { box-shadow: 0 6px 12px rgba(0, 0, 0, 0.12); } .event-card-title { font-size: 11px; color: #333333; text-align: center; line-height: 1.4; word-break: break-word; pointer-events: none; } `) } ``` ### examples/data/complex-timeline.json ```json [ { "id": "milestone-1", "headline": "Phase 1", "subtitle": "Initial Setup", "date": "01/01/25", "color": "#8A2BE2", "lightColor": "rgba(138, 43, 226, 0.1)", "events": [ { "id": "e1-1", "title": "Kickoff", "content": "" }, { "id": "e1-2", "title": "Planning", "content": "" }, { "id": "e1-3", "title": "Team", "content": "" }, { "id": "e1-4", "title": "Resources", "content": "" }, { "id": "e1-5", "title": "Budget", "content": "" }, { "id": "e1-6", "title": "Setup", "content": "" } ] }, { "id": "milestone-2", "headline": "Phase 2", "subtitle": "Design", "date": "02/15/25", "color": "#4169E1", "lightColor": "rgba(65, 105, 225, 0.1)", "events": [ { "id": "e2-1", "title": "Research", "content": "" }, { "id": "e2-2", "title": "Wireframes", "content": "" }, { "id": "e2-3", "title": "Mockups", "content": "" }, { "id": "e2-4", "title": "Prototype", "content": "" } ] }, { "id": "milestone-3", "headline": "Phase 3", "subtitle": "Development", "date": "04/01/25", "color": "#FFD700", "lightColor": "rgba(255, 215, 0, 0.1)", "events": [ { "id": "e3-1", "title": "Frontend", "content": "" }, { "id": "e3-2", "title": "Backend", "content": "" }, { "id": "e3-3", "title": "Database", "content": "" }, { "id": "e3-4", "title": "API", "content": "" }, { "id": "e3-5", "title": "Testing", "content": "" }, { "id": "e3-6", "title": "Debug", "content": "" }, { "id": "e3-7", "title": "Optimize", "content": "" }, { "id": "e3-8", "title": "Review", "content": "" }, { "id": "e3-9", "title": "Deploy", "content": "" }, { "id": "e3-10", "title": "Monitor", "content": "" } ] }, { "id": "milestone-4", "headline": "Phase 4", "subtitle": "Launch", "date": "06/01/25", "color": "#2E8B57", "lightColor": "rgba(46, 139, 87, 0.1)", "events": [ { "id": "e4-1", "title": "QA", "content": "" }, { "id": "e4-2", "title": "Release", "content": "" }, { "id": "e4-3", "title": "Support", "content": "" } ] } ] ``` ### 人工智能建模 DAG 图 - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/dag/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/dag/ - Source path: examples/showcase/practices/demo/dag.tsx ```tsx import exampleData from '../../../data/dag.json' import React from 'react' import { Graph, Path, Cell, Selection } from '@antv/x6' import insertCss from 'insert-css' import { register } from '@antv/x6-react-shape' interface NodeStatus { id: string status: 'default' | 'success' | 'failed' | 'running' label?: string } const image = { logo: 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*evDjT5vjkX0AAAAAAAAAAAAAARQnAQ', success: 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*6l60T6h8TTQAAAAAAAAAAAAAARQnAQ', failed: 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*SEISQ6My-HoAAAAAAAAAAAAAARQnAQ', running: 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*t8fURKfgSOgAAAAAAAAAAAAAARQnAQ', } const AlgoNode = (props: { node: Cell }) => { const { node } = props const data = node?.getData() as NodeStatus const { label, status = 'default' } = data return ( <div className={`node ${status}`}> <img src={image.logo} alt="logo" /> <span className="label">{label}</span> <span className="status"> {status === 'success' && <img src={image.success} alt="success" />} {status === 'failed' && <img src={image.failed} alt="failed" />} {status === 'running' && <img src={image.running} alt="running" />} </span> </div> ) } register({ shape: 'dag-node', width: 180, height: 36, component: AlgoNode, ports: { groups: { top: { position: 'top', attrs: { circle: { r: 4, magnet: true, stroke: '#C2C8D5', strokeWidth: 1, fill: '#fff', }, }, }, bottom: { position: 'bottom', attrs: { circle: { r: 4, magnet: true, stroke: '#C2C8D5', strokeWidth: 1, fill: '#fff', }, }, }, }, }, }) Graph.registerEdge( 'dag-edge', { inherit: 'edge', attrs: { line: { stroke: '#C2C8D5', strokeWidth: 1, targetMarker: null, }, }, }, true, ) Graph.registerConnector( 'algo-connector', (s, e) => { const offset = 4 const deltaY = Math.abs(e.y - s.y) const control = Math.floor((deltaY / 3) * 2) const v1 = { x: s.x, y: s.y + offset + control } const v2 = { x: e.x, y: e.y - offset - control } return Path.parse( ` M ${s.x} ${s.y} L ${s.x} ${s.y + offset} C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x} ${e.y - offset} L ${e.x} ${e.y} `, ).serialize() }, true, ) const nodeStatusList: NodeStatus[][] = [ [ { id: '1', status: 'running', }, { id: '2', status: 'default', }, { id: '3', status: 'default', }, { id: '4', status: 'default', }, ], [ { id: '1', status: 'success', }, { id: '2', status: 'running', }, { id: '3', status: 'default', }, { id: '4', status: 'default', }, ], [ { id: '1', status: 'success', }, { id: '2', status: 'success', }, { id: '3', status: 'running', }, { id: '4', status: 'running', }, ], [ { id: '1', status: 'success', }, { id: '2', status: 'success', }, { id: '3', status: 'success', }, { id: '4', status: 'failed', }, ], ] const graph: Graph = new Graph({ container: document.getElementById('container')!, panning: { enabled: true, eventTypes: ['leftMouseDown', 'mouseWheel'], }, mousewheel: { enabled: true, modifiers: 'ctrl', factor: 1.1, maxScale: 1.5, minScale: 0.5, }, highlighting: { magnetAdsorbed: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#31d0c6', strokeWidth: 4, }, }, }, }, connecting: { snap: true, allowBlank: false, allowLoop: false, highlight: true, connector: 'algo-connector', connectionPoint: 'anchor', anchor: 'center', validateMagnet({ magnet }) { return magnet.getAttribute('port-group') !== 'top' }, createEdge() { return graph.createEdge({ shape: 'dag-edge', attrs: { line: { strokeDasharray: '5 5', }, }, zIndex: -1, }) }, }, }) graph.use( new Selection({ multiple: true, rubberEdge: true, rubberNode: true, modifiers: 'shift', rubberband: true, }), ) graph.on('edge:connected', ({ edge }) => { edge.attr({ line: { strokeDasharray: '', }, }) }) graph.on('node:change:data', ({ node }) => { const edges = graph.getIncomingEdges(node) const { status } = node.getData() as NodeStatus edges?.forEach((edge) => { if (status === 'running') { edge.attr('line/strokeDasharray', 5) edge.attr('line/style/animation', 'running-line 30s infinite linear') } else { edge.attr('line/strokeDasharray', '') edge.attr('line/style/animation', '') } }) }) // 初始化节点/边 const init = (data: any[]) => { const cells: Cell[] = [] data.forEach((item) => { if (item.shape === 'dag-node') { cells.push(graph.createNode(item)) } else { cells.push(graph.createEdge(item)) } }) graph.resetCells(cells) } // 显示节点状态 const showNodeStatus = async (statusList: NodeStatus[][]) => { const status = statusList.shift() status?.forEach((item) => { const { id, status } = item const node = graph.getCellById(id) const data = node.getData() as NodeStatus node.setData({ ...data, status, }) }) setTimeout(() => { showNodeStatus(statusList) }, 3000) } Promise.resolve(exampleData) .then((data) => { init(data) showNodeStatus(nodeStatusList) graph.centerContent() }) insertCss(` .node { display: flex; align-items: center; width: 100%; height: 100%; background-color: #fff; border: 1px solid #c2c8d5; border-left: 4px solid #5F95FF; border-radius: 4px; box-shadow: 0 2px 5px 1px rgba(0, 0, 0, 0.06); } .node img { width: 20px; height: 20px; flex-shrink: 0; margin-left: 8px; } .node .label { display: inline-block; flex-shrink: 0; width: 104px; margin-left: 8px; color: #666; font-size: 12px; } .node .status { flex-shrink: 0; } .node.success { border-left: 4px solid #52c41a; } .node.failed { border-left: 4px solid #ff4d4f; } .node.running .status img { animation: spin 1s linear infinite; } .x6-node-selected .node { border-color: #1890ff; border-radius: 2px; box-shadow: 0 0 0 4px #d4e8fe; } .x6-node-selected .node.success { border-color: #52c41a; border-radius: 2px; box-shadow: 0 0 0 4px #ccecc0; } .x6-node-selected .node.failed { border-color: #ff4d4f; border-radius: 2px; box-shadow: 0 0 0 4px #fedcdc; } .x6-edge:hover path:nth-child(2){ stroke: #1890ff; stroke-width: 1px; } .x6-edge-selected path:nth-child(2){ stroke: #1890ff; stroke-width: 1.5px !important; } @keyframes running-line { to { stroke-dashoffset: -1000; } } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } `) ``` ### examples/data/dag.json ```json [ { "id": "1", "shape": "dag-node", "x": 290, "y": 110, "data": { "label": "读数据", "status": "success" }, "ports": [ { "id": "1-1", "group": "bottom" } ] }, { "id": "2", "shape": "dag-node", "x": 290, "y": 225, "data": { "label": "逻辑回归", "status": "success" }, "ports": [ { "id": "2-1", "group": "top" }, { "id": "2-2", "group": "bottom" }, { "id": "2-3", "group": "bottom" } ] }, { "id": "3", "shape": "dag-node", "x": 170, "y": 350, "data": { "label": "模型预测", "status": "success" }, "ports": [ { "id": "3-1", "group": "top" }, { "id": "3-2", "group": "bottom" } ] }, { "id": "4", "shape": "dag-node", "x": 450, "y": 350, "data": { "label": "读取参数", "status": "success" }, "ports": [ { "id": "4-1", "group": "top" }, { "id": "4-2", "group": "bottom" } ] }, { "id": "5", "shape": "dag-edge", "source": { "cell": "1", "port": "1-1" }, "target": { "cell": "2", "port": "2-1" }, "zIndex": 0 }, { "id": "6", "shape": "dag-edge", "source": { "cell": "2", "port": "2-2" }, "target": { "cell": "3", "port": "3-1" }, "zIndex": 0 }, { "id": "7", "shape": "dag-edge", "source": { "cell": "2", "port": "2-3" }, "target": { "cell": "4", "port": "4-1" }, "zIndex": 0 } ] ``` ### 数据加工 DAG 图 - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/dataprocessingdag/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/dataprocessingdag/ - Source path: examples/showcase/practices/demo/dataProcessingDag.tsx ```tsx import exampleData from '../../../data/data-processing-dag.json' import React from 'react' import { Graph, Node, Path, Edge, StringExt, Selection, IS_SAFARI, } from '@antv/x6' import classnames from 'classnames' import insertCss from 'insert-css' import { register } from '@antv/x6-react-shape' import { Tooltip, Dropdown } from 'antd' // 节点类型 enum NodeType { INPUT = 'INPUT', // 数据输入 FILTER = 'FILTER', // 数据过滤 JOIN = 'JOIN', // 数据连接 UNION = 'UNION', // 数据合并 AGG = 'AGG', // 数据聚合 OUTPUT = 'OUTPUT', // 数据输出 } // 元素校验状态 enum CellStatus { DEFAULT = 'default', SUCCESS = 'success', ERROR = 'error', } interface ProcessingNodeData { name: string type: NodeType status?: CellStatus statusMsg?: string } // 节点位置信息 interface Position { x: number y: number } // 加工类型列表 const PROCESSING_TYPE_LIST: { type: NodeType; name: string }[] = [ { type: NodeType.FILTER, name: '数据筛选', }, { type: NodeType.JOIN, name: '数据连接', }, { type: NodeType.UNION, name: '数据合并', }, { type: NodeType.AGG, name: '数据聚合', }, { type: NodeType.OUTPUT, name: '数据输出', }, ] // 不同节点类型的icon const NODE_TYPE_LOGO: Record<NodeType, string> = { INPUT: 'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*RXnuTpQ22xkAAAAAAAAAAAAADtOHAQ/original', // 数据输入 FILTER: 'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*ZJ6qToit8P4AAAAAAAAAAAAADtOHAQ/original', // 数据筛选 JOIN: 'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*EHqyQoDeBvIAAAAAAAAAAAAADtOHAQ/original', // 数据连接 UNION: 'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*k4eyRaXv8gsAAAAAAAAAAAAADtOHAQ/original', // 数据合并 AGG: 'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*TKG8R6nfYiAAAAAAAAAAAAAADtOHAQ/original', // 数据聚合 OUTPUT: 'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*zUgORbGg1HIAAAAAAAAAAAAADtOHAQ/original', // 数据输出 } /** * 根据起点初始下游节点的位置信息 * @param node 起始节点 * @param graph * @returns */ const getDownstreamNodePosition = ( node: Node, graph: Graph, dx = 250, dy = 100, ) => { // 找出画布中以该起始节点为起点的相关边的终点id集合 const downstreamNodeIdList: string[] = [] graph.getEdges().forEach((edge) => { const originEdge = edge.toJSON()?.data if (originEdge.source === node.id) { downstreamNodeIdList.push(originEdge.target) } }) // 获取起点的位置信息 const position = node.getPosition() let minX = Infinity let maxY = -Infinity graph.getNodes().forEach((graphNode) => { if (downstreamNodeIdList.indexOf(graphNode.id) > -1) { const nodePosition = graphNode.getPosition() // 找到所有节点中最左侧的节点的x坐标 if (nodePosition.x < minX) { minX = nodePosition.x } // 找到所有节点中最x下方的节点的y坐标 if (nodePosition.y > maxY) { maxY = nodePosition.y } } }) return { x: minX !== Infinity ? minX : position.x + dx, y: maxY !== -Infinity ? maxY + dy : position.y, } } // 根据节点的类型获取ports const getPortsByType = ( type: NodeType, nodeId: string, ): { id: string; group: 'in' | 'out' }[] => { let ports: { id: string; group: 'in' | 'out' }[] = [] switch (type) { case NodeType.INPUT: ports = [ { id: `${nodeId}-out`, group: 'out', }, ] break case NodeType.OUTPUT: ports = [ { id: `${nodeId}-in`, group: 'in', }, ] break default: ports = [ { id: `${nodeId}-in`, group: 'in', }, { id: `${nodeId}-out`, group: 'out', }, ] break } return ports } /** * 创建节点并添加到画布 * @param type 节点类型 * @param graph * @param position 节点位置 * @returns */ export const createNode = ( type: NodeType, graph: Graph, position?: Position, ): Node => { const sameTypeNodes = graph .getNodes() .filter((item) => item.getData()?.type === type) const typeName = PROCESSING_TYPE_LIST?.find( (item) => item.type === type, )?.name const id = StringExt.uuid() const node = { id, shape: 'data-processing-dag-node', x: position?.x, y: position?.y, ports: getPortsByType(type, id), data: { name: `${typeName}_${sameTypeNodes.length + 1}`, type, }, } return graph.addNode(node) } /** * 创建边并添加到画布 * @param source * @param target * @param graph */ const createEdge = (source: string, target: string, graph: Graph) => { const edge = { id: StringExt.uuid(), shape: 'data-processing-curve', source: { cell: source, port: `${source}-out`, }, target: { cell: target, port: `${target}-in`, }, zIndex: -1, data: { source, target, }, } if (graph) { graph.addEdge(edge) } } class DataProcessingDagNode extends React.Component<{ node: Node }> { state = { plusActionSelected: false, } // 创建下游的节点和边 createDownstream = (type: NodeType) => { const { node } = this.props const { graph } = node.model || {} if (graph) { // 获取下游节点的初始位置信息 const position = getDownstreamNodePosition(node, graph) // 创建下游节点 const newNode = createNode(type, graph, position) const source = node.id const target = newNode.id // 创建该节点出发到下游节点的边 createEdge(source, target, graph) } } // 点击添加下游+号 clickPlusDragMenu = (type: NodeType) => { this.createDownstream(type) this.setState({ plusActionSelected: false, }) } // 获取+号下拉菜单 getPlusDagMenu = () => { return ( <ul> {PROCESSING_TYPE_LIST.map((item) => { const content = ( // eslint-disable-next-line <a onClick={() => this.clickPlusDragMenu(item.type)}> <i className="node-mini-logo" style={{ backgroundImage: `url(${NODE_TYPE_LOGO[item.type]})` }} /> <span>{item.name}</span> </a> ) return ( <li className="each-sub-menu" key={item.type}> {content} </li> ) })} </ul> ) } // 添加下游菜单的打开状态变化 onPlusDropdownOpenChange = (value: boolean) => { this.setState({ plusActionSelected: value, }) } // 鼠标进入矩形主区域的时候显示连接桩 onMainMouseEnter = () => { const { node } = this.props // 获取该节点下的所有连接桩 const ports = node.getPorts() || [] ports.forEach((port: { id: string }) => { node.setPortProp(port.id, 'attrs/circle', { fill: '#fff', stroke: '#85A5FF', }) }) } // 鼠标离开矩形主区域的时候隐藏连接桩 onMainMouseLeave = () => { const { node } = this.props // 获取该节点下的所有连接桩 const ports = node.getPorts() || [] ports.forEach((port: { id: string }) => { node.setPortProp(port.id, 'attrs/circle', { fill: 'transparent', stroke: 'transparent', }) }) } render() { const { plusActionSelected } = this.state const { node } = this.props const data = node?.getData() as ProcessingNodeData const { name, type, status, statusMsg } = data return ( <div className="data-processing-dag-node"> <div className="main-area" onMouseEnter={this.onMainMouseEnter} onMouseLeave={this.onMainMouseLeave} > <div className="main-info"> {/* 节点类型icon */} <i className="node-logo" style={{ backgroundImage: `url(${NODE_TYPE_LOGO[type]})` }} /> <Tooltip title={name} mouseEnterDelay={0.8}> <div className="ellipsis-row node-name">{name}</div> </Tooltip> </div> {/* 节点状态信息 */} <div className="status-action"> {status === CellStatus.ERROR && ( <Tooltip title={statusMsg}> <i className="status-icon status-icon-error" /> </Tooltip> )} {status === CellStatus.SUCCESS && ( <i className="status-icon status-icon-success" /> )} {/* 节点操作菜单 */} <div className="more-action-container"> <i className="more-action" /> </div> </div> </div> {/* 添加下游节点 */} {type !== NodeType.OUTPUT && ( <div className="plus-dag"> <Dropdown dropdownRender={this.getPlusDagMenu} overlayClassName="processing-node-menu" trigger={['click']} placement="bottom" open={plusActionSelected} onOpenChange={this.onPlusDropdownOpenChange} > <i className={classnames('plus-action', { 'plus-action-selected': plusActionSelected, })} /> </Dropdown> </div> )} </div> ) } } register({ shape: 'data-processing-dag-node', width: 212, height: 48, component: DataProcessingDagNode, // port默认不可见 ports: { groups: { in: { position: 'left', attrs: { circle: { r: 4, magnet: true, stroke: 'transparent', strokeWidth: 1, fill: 'transparent', }, }, }, out: { position: { name: 'right', args: { dx: -32, }, }, attrs: { circle: { r: 4, magnet: true, stroke: 'transparent', strokeWidth: 1, fill: 'transparent', }, }, }, }, }, }) // 注册连线 Graph.registerConnector( 'curveConnector', (sourcePoint, targetPoint) => { const hgap = Math.abs(targetPoint.x - sourcePoint.x) const path = new Path() path.appendSegment( Path.createSegment('M', sourcePoint.x - 4, sourcePoint.y), ) path.appendSegment( Path.createSegment('L', sourcePoint.x + 12, sourcePoint.y), ) // 水平三阶贝塞尔曲线 path.appendSegment( Path.createSegment( 'C', sourcePoint.x < targetPoint.x ? sourcePoint.x + hgap / 2 : sourcePoint.x - hgap / 2, sourcePoint.y, sourcePoint.x < targetPoint.x ? targetPoint.x - hgap / 2 : targetPoint.x + hgap / 2, targetPoint.y, targetPoint.x - 6, targetPoint.y, ), ) path.appendSegment( Path.createSegment('L', targetPoint.x + 2, targetPoint.y), ) return path.serialize() }, true, ) 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', }, }, ], connector: { name: 'curveConnector' }, attrs: { wrap: { connection: true, strokeWidth: 10, strokeLinejoin: 'round', }, line: { connection: true, stroke: '#A2B1C3', strokeWidth: 1, targetMarker: { name: 'classic', size: 6, }, }, }, }) Graph.registerEdge('data-processing-curve', Edge, true) const graph: Graph = new Graph({ container: document.getElementById('container')!, panning: { enabled: true, eventTypes: ['leftMouseDown', 'mouseWheel'], }, mousewheel: { enabled: true, modifiers: 'ctrl', factor: 1.1, maxScale: 1.5, minScale: 0.5, }, highlighting: { magnetAdsorbed: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#31d0c6', strokeWidth: 4, }, }, }, }, connecting: { snap: true, allowBlank: false, allowLoop: false, highlight: true, sourceAnchor: { name: 'left', args: { dx: IS_SAFARI ? 4 : 8, }, }, targetAnchor: { name: 'right', args: { dx: IS_SAFARI ? 4 : -8, }, }, createEdge() { return graph.createEdge({ shape: 'data-processing-curve', attrs: { line: { strokeDasharray: '5 5', }, }, zIndex: -1, }) }, // 连接桩校验 validateConnection({ sourceMagnet, targetMagnet }) { // 只能从输出链接桩创建连接 if (!sourceMagnet || sourceMagnet.getAttribute('port-group') === 'in') { return false } // 只能连接到输入链接桩 if (!targetMagnet || targetMagnet.getAttribute('port-group') !== 'in') { return false } return true }, }, }) graph.use( new Selection({ multiple: true, rubberEdge: true, rubberNode: true, modifiers: 'shift', rubberband: true, }), ) // 节点状态列表 const nodeStatusList = [ { id: 'node-0', status: 'success', }, { id: 'node-1', status: 'success', }, { id: 'node-2', status: 'success', }, { id: 'node-3', status: 'success', }, { id: 'node-4', status: 'error', statusMsg: '错误信息示例', }, ] // 边状态列表 const edgeStatusList = [ { id: 'edge-0', status: 'success', }, { id: 'edge-1', status: 'success', }, { id: 'edge-2', status: 'success', }, { id: 'edge-3', status: 'success', }, ] // 显示节点状态 const showNodeStatus = () => { nodeStatusList.forEach((item) => { const { id, status, statusMsg } = item const node = graph.getCellById(id) const data = (node.getData() || {}) as ProcessingNodeData node.setData({ ...data, status, statusMsg, }) }) } // 开启边的运行动画 const excuteAnimate = () => { graph.getEdges().forEach((edge) => { edge.attr({ line: { stroke: '#3471F9', }, }) edge.attr('line/strokeDasharray', 5) edge.attr('line/style/animation', 'running-line 30s infinite linear') }) } // 关闭边的动画 const stopAnimate = () => { graph.getEdges().forEach((edge) => { edge.attr('line/strokeDasharray', 0) edge.attr('line/style/animation', '') }) edgeStatusList.forEach((item) => { const { id, status } = item const edge = graph.getCellById(id) if (status === 'success') { edge.attr('line/stroke', '#52c41a') } if (status === 'error') { edge.attr('line/stroke', '#ff4d4f') } }) // 默认选中一个节点 graph.select('node-2') } Promise.resolve(exampleData) .then((data) => { graph.fromJSON(data) const zoomOptions = { padding: { left: 10, right: 10, }, } graph.zoomToFit(zoomOptions) setTimeout(() => { excuteAnimate() }, 2000) setTimeout(() => { showNodeStatus() stopAnimate() }, 3000) }) insertCss(` .data-processing-dag-node { display: flex; flex-direction: row; align-items: center; } .main-area { display: flex; flex-direction: row; justify-content: space-between; padding: 12px; width: 180px; height: 48px; color: rgba(0, 0, 0, 65%); font-size: 12px; font-family: PingFangSC; line-height: 24px; background-color: #fff; box-shadow: 0 -1px 4px 0 rgba(209, 209, 209, 50%), 1px 1px 4px 0 rgba(217, 217, 217, 50%); border-radius: 2px; border: 1px solid transparent; } .main-area:hover { border: 1px solid rgba(0, 0, 0, 10%); box-shadow: 0 -2px 4px 0 rgba(209, 209, 209, 50%), 2px 2px 4px 0 rgba(217, 217, 217, 50%); } .node-logo { display: inline-block; width: 24px; height: 24px; background-repeat: no-repeat; background-position: center; background-size: 100%; } .node-name { overflow: hidden; display: inline-block; width: 70px; margin-left: 6px; color: rgba(0, 0, 0, 65%); font-size: 12px; font-family: PingFangSC; white-space: nowrap; text-overflow: ellipsis; vertical-align: top; } .status-action { display: flex; flex-direction: row; align-items: center; } .status-icon { display: inline-block; width: 24px; height: 24px; } .status-icon-error { background: url('https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*SEISQ6My-HoAAAAAAAAAAAAAARQnAQ') no-repeat center center / 100% 100%; } .status-icon-success { background: url('https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*6l60T6h8TTQAAAAAAAAAAAAAARQnAQ') no-repeat center center / 100% 100%; } .more-action-container { margin-left: 12px; width: 15px; height: 15px; text-align: center; cursor: pointer; } .more-action { display: inline-block; width: 3px; height: 15px; background: url('https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*tFw7SIy-ttQAAAAAAAAAAAAADtOHAQ/original') no-repeat center center / 100% 100%; } .plus-dag { visibility: hidden; position: relative; margin-left: 12px; height: 48px; } .plus-action { position: absolute; top: calc(50% - 8px); left: 0; width: 16px; height: 16px; background: url('https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*ScX2R4ODfokAAAAAAAAAAAAADtOHAQ/original') no-repeat center center / 100% 100%; cursor: pointer; } .plus-action:hover { background-image: url('https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*tRaoS5XhsuQAAAAAAAAAAAAADtOHAQ/original'); } .plus-action:active, .plus-action-selected { background-image: url('https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*k9cnSaSmlw4AAAAAAAAAAAAADtOHAQ/original'); } .x6-node-selected .main-area { border-color: #3471f9; } .x6-node-selected .plus-dag { visibility: visible; } .processing-node-menu { padding: 2px 0; width: 105px; background-color: #fff; box-shadow: 0 9px 28px 8px rgba(0, 0, 0, 5%), 0 6px 16px 0 rgba(0, 0, 0, 8%), 0 3px 6px -4px rgba(0, 0, 0, 12%); border-radius: 2px; } .processing-node-menu ul { margin: 0; padding: 0; } .processing-node-menu li { list-style:none; } .each-sub-menu { padding: 6px 12px; width: 100%; } .each-sub-menu:hover { background-color: rgba(0, 0, 0, 4%); } .each-sub-menu a { display: inline-block; width: 100%; height: 16px; font-family: PingFangSC; font-weight: 400; font-size: 12px; color: rgba(0, 0, 0, 65%); } .each-sub-menu span { margin-left: 8px; vertical-align: top; } .each-disabled-sub-menu a { cursor: not-allowed; color: rgba(0, 0, 0, 35%); } .node-mini-logo { display: inline-block; width: 16px; height: 16px; background-repeat: no-repeat; background-position: center; background-size: 100%; vertical-align: top; } @keyframes running-line { to { stroke-dashoffset: -1000; } } `) ``` ### examples/data/data-processing-dag.json ```json { "nodes": [ { "id": "node-0", "shape": "data-processing-dag-node", "x": 0, "y": 100, "ports": [ { "id": "node-0-out", "group": "out" } ], "data": { "name": "数据输入_1", "type": "INPUT", "checkStatus": "sucess" } }, { "id": "node-1", "shape": "data-processing-dag-node", "x": 250, "y": 100, "ports": [ { "id": "node-1-in", "group": "in" }, { "id": "node-1-out", "group": "out" } ], "data": { "name": "数据筛选_1", "type": "FILTER" } }, { "id": "node-2", "shape": "data-processing-dag-node", "x": 250, "y": 200, "ports": [ { "id": "node-2-out", "group": "out" } ], "data": { "name": "数据输入_2", "type": "INPUT" } }, { "id": "node-3", "shape": "data-processing-dag-node", "x": 500, "y": 100, "ports": [ { "id": "node-3-in", "group": "in" }, { "id": "node-3-out", "group": "out" } ], "data": { "name": "数据连接_1", "type": "JOIN" } }, { "id": "node-4", "shape": "data-processing-dag-node", "x": 750, "y": 100, "ports": [ { "id": "node-4-in", "group": "in" } ], "data": { "name": "数据输出_1", "type": "OUTPUT" } } ], "edges": [ { "id": "edge-0", "source": { "cell": "node-0", "port": "node-0-out" }, "target": { "cell": "node-1", "port": "node-1-in" }, "shape": "data-processing-curve", "zIndex": -1, "data": { "source": "node-0", "target": "node-1" } }, { "id": "edge-1", "source": { "cell": "node-2", "port": "node-2-out" }, "target": { "cell": "node-3", "port": "node-3-in" }, "shape": "data-processing-curve", "zIndex": -1, "data": { "source": "node-2", "target": "node-3" } }, { "id": "edge-2", "source": { "cell": "node-1", "port": "node-1-out" }, "target": { "cell": "node-3", "port": "node-3-in" }, "shape": "data-processing-curve", "zIndex": -1, "data": { "source": "node-1", "target": "node-3" } }, { "id": "edge-3", "source": { "cell": "node-3", "port": "node-3-out" }, "target": { "cell": "node-4", "port": "node-4-in" }, "shape": "data-processing-curve", "zIndex": -1, "data": { "source": "node-3", "target": "node-4" } } ] } ``` ### ELK - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/elk/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/elk/ - Source path: examples/showcase/practices/demo/elk.ts ```ts import exampleData from '../../../data/elkdata.json' import { Graph, Cell } from '@antv/x6' import ELK, { ElkNode, ElkEdge, ElkExtendedEdge } from 'elkjs/lib/elk.bundled.js' Graph.registerNode( 'elk-node', { inherit: 'rect', attrs: { body: { fill: '#EFF4FF', stroke: '#5F95FF', strokeWidth: 1, }, label: { refX: 0, refY: -4, textAnchor: 'start', textVerticalAnchor: 'bottom', fontSize: 10, }, }, ports: { groups: { port: { position: { name: 'absolute', }, attrs: { portBody: { magnet: 'passive', fill: '#5F95FF', refWidth: '100%', refHeight: '100%', }, }, markup: [ { tagName: 'rect', selector: 'portBody', }, ], }, }, }, }, true, ) Graph.registerEdge( 'elk-edge', { inherit: 'edge', attrs: { line: { stroke: '#A2B1C3', strokeWidth: 1, targetMarker: { name: 'block', width: 4, height: 4, }, }, }, }, true, ) interface Position { x: number y: number } const portIdToNodeIdMap: Record<string, string> = {} const cells: Cell[] = [] const graph = new Graph({ container: document.getElementById('container')!, interacting: false, }) const elk = new ELK() const addChildren = (children: ElkNode[], pos?: Position) => { children.forEach((child) => { const position = { x: (child.x || 0) + (pos ? pos.x : 0), y: (child.y || 0) + (pos ? pos.y : 0), } let label: string = '' if (typeof child.labels === 'string') { label = child.labels } else if (Array.isArray(child.labels) && child.labels[0]) { label = child.labels[0].text } const node = graph.createNode({ shape: 'elk-node', id: child.id, position, label, size: { width: child.width || 0, height: child.height || 0, }, ports: { items: (child.ports || []).map((item) => { portIdToNodeIdMap[item.id] = child.id return { id: item.id, group: 'port', args: { x: item.x, y: item.y, }, size: { width: item.width || 0, height: item.height || 0, }, } }), }, }) cells.push(node) if (child.children) { addChildren(child.children, position) } if (child.edges) { addEdges(child.edges, position) } }) } const addEdges = (edges: ElkEdge[], pos?: Position) => { edges.forEach((edge: ElkExtendedEdge) => { const { bendPoints = [] } = edge.sections[0] if (pos) { bendPoints.map((bendPoint: Position) => { bendPoint.x += pos.x bendPoint.y += pos.y }) } const sourcePortId = edge.sources[0] const targetPortId = edge.targets[0] const sourceNodeId = portIdToNodeIdMap[sourcePortId] const targetNodeId = portIdToNodeIdMap[targetPortId] cells.push( graph.createEdge({ shape: 'elk-edge', source: { cell: sourceNodeId, port: sourcePortId, }, target: { cell: targetNodeId, port: targetPortId, }, vertices: bendPoints, }), ) }) } Promise.resolve(exampleData) .then((data) => { elk.layout(data).then((res) => { addChildren(res.children || []) addEdges(res.edges || []) graph.resetCells(cells) graph.zoomToFit({ padding: 20, maxScale: 1, }) }) }) ``` ### examples/data/elkdata.json ```json { "id": "n0", "children": [ { "id": "n1", "labels": "DatagramReader", "ports": [ { "id": "p1", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "0" } }, { "id": "p2", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "1" } }, { "id": "p3", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "2" } }, { "id": "p4", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "3" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 55.0, "height": 40.0 }, { "id": "n2", "labels": [ { "text": "Ramp", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 50.0, "height": 12.0 } ], "ports": [ { "id": "p1_g837978", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } }, { "id": "p2_g739884", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "1" } }, { "id": "p3_g539420", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "2" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 55.0, "height": 40.0 }, { "id": "n3", "labels": [ { "text": "QueueControl", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 100.0, "height": 12.0 } ], "ports": [ { "id": "p1_g572291", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } }, { "id": "p2_g653563", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "1" } }, { "id": "p3_g722216", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "2" } }, { "id": "p4_g748499", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "3" } }, { "id": "p5", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "4" } }, { "id": "p6", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "5" } }, { "id": "p7", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "6" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 55.0, "height": 40.0 }, { "id": "n4", "labels": [ { "text": "Display", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 35.0, "height": 12.0 } ], "ports": [ { "id": "p1_g255145", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 35.0, "height": 27.0 }, { "id": "n5", "labels": [ { "text": "Interface - fast", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 10.0, "height": 12.0 } ], "ports": [ { "id": "p1_g820682", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } }, { "id": "p2_g506569", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "1" } } ], "children": [ { "id": "n6", "labels": [ { "text": "Sleep", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 50.0, "height": 12.0 } ], "ports": [ { "id": "p1_g582202", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } }, { "id": "p2_g036217", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "1" } }, { "id": "p3_g791687", "width": 7, "height": 7, "layoutOptions": { "port.side": "SOUTH", "port.index": "2" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 50.0, "height": 35.0 } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "edges": [ { "id": "e15", "sources": ["p1_g820682"], "targets": ["p1_g582202"] }, { "id": "e16", "sources": ["p2_g036217"], "targets": ["p2_g506569"] } ] }, { "id": "n7", "labels": [ { "text": "Interface - slow", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 100.0, "height": 12.0 } ], "ports": [ { "id": "p1_g261431", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } }, { "id": "p2_g518944", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "1" } } ], "children": [ { "id": "n8", "labels": [ { "text": "Sleep", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 50.0, "height": 12.0 } ], "ports": [ { "id": "p1_g587373", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } }, { "id": "p2_g014361", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "1" } }, { "id": "p3_g749816", "width": 7, "height": 7, "layoutOptions": { "port.side": "SOUTH", "port.index": "2" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 50.0, "height": 35.0 } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "edges": [ { "id": "e17", "sources": ["p1_g261431"], "targets": ["p1_g587373"] }, { "id": "e18", "sources": ["p2_g014361"], "targets": ["p2_g518944"] } ] }, { "id": "n9", "labels": [ { "text": "Counter - q1", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 55.0, "height": 12.0 } ], "ports": [ { "id": "p1_g650325", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } }, { "id": "p2_g732355", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "1" } }, { "id": "p3_g158827", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "2" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 55.0, "height": 35.0 }, { "id": "n10", "labels": [ { "text": "Counter - q2", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 55.0, "height": 12.0 } ], "ports": [ { "id": "p1_g006148", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } }, { "id": "p2_g008338", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "1" } }, { "id": "p3_g606176", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "2" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 55.0, "height": 35.0 }, { "id": "n11", "labels": [ { "text": "channel1", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 50.0, "height": 12.0 } ], "ports": [ { "id": "p1_g044769", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } } ], "children": [ { "id": "n12", "labels": [ { "text": "Counter", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 75.0, "height": 12.0 } ], "ports": [ { "id": "p1_g924853", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } }, { "id": "p2_g825798", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "1" } }, { "id": "p3_g627665", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "2" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 55.0, "height": 35.0 }, { "id": "n13", "labels": [ { "text": "RecordAssembler", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 100.0, "height": 12.0 } ], "ports": [ { "id": "p1_g710134", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } }, { "id": "p2_g774554", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "1" } }, { "id": "p3_g935869", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "2" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 10.0, "height": 40.0 }, { "id": "n14", "labels": [ { "text": "Display", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 50.0, "height": 12.0 } ], "ports": [ { "id": "p1_g849516", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 35.0, "height": 27.0 } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "edges": [ { "id": "e19", "sources": ["p1_g044769"], "targets": ["p2_g825798"] }, { "id": "e20", "sources": ["p3_g627665"], "targets": ["p1_g710134"] }, { "id": "e21", "sources": ["p1_g044769"], "targets": ["p2_g774554"] }, { "id": "e22", "sources": ["p3_g935869"], "targets": ["p1_g849516"] } ] }, { "id": "n15", "labels": [ { "text": "channel2", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 50.0, "height": 12.0 } ], "ports": [ { "id": "p1_g598580", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } } ], "children": [ { "id": "n16", "labels": [ { "text": "Counter", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 75.0, "height": 12.0 } ], "ports": [ { "id": "p1_g439211", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } }, { "id": "p2_g586159", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "1" } }, { "id": "p3_g324437", "layoutOptions": { "port.side": "EAST", "port.index": "2" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 55.0, "height": 35.0 }, { "id": "n17", "labels": [ { "text": "RecordAssembler", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 100.0, "height": 12.0 } ], "ports": [ { "id": "p1_g816526", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } }, { "id": "p2_g800534", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "1" } }, { "id": "p3_g930851", "width": 7, "height": 7, "layoutOptions": { "port.side": "EAST", "port.index": "2" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 10.0, "height": 40.0 }, { "id": "n18", "labels": [ { "text": "Display", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 50.0, "height": 12.0 } ], "ports": [ { "id": "p1_g183964", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 35.0, "height": 27.0 } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "edges": [ { "id": "e23", "sources": ["p1_g598580"], "targets": ["p2_g586159"] }, { "id": "e24", "sources": ["p3_g324437"], "targets": ["p1_g816526"] }, { "id": "e25", "sources": ["p1_g598580"], "targets": ["p2_g800534"] }, { "id": "e26", "sources": ["p3_g930851"], "targets": ["p1_g183964"] } ] }, { "id": "n19", "labels": [ { "text": "MonitorValue - q1 length", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 150.0, "height": 12.0 } ], "ports": [ { "id": "p1_g445341", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 55.0, "height": 40.0 }, { "id": "n20", "labels": [ { "text": "MonitorValue - q2 length", "layoutOptions": { "nodeLabels.placement": "[H_LEFT, V_TOP, OUTSIDE]" }, "width": 150.0, "height": 12.0 } ], "ports": [ { "id": "p1_g930680", "width": 7, "height": 7, "layoutOptions": { "port.side": "WEST", "port.index": "0" } } ], "layoutOptions": { "portConstraints": "FIXED_ORDER" }, "width": 55.0, "height": 40.0 } ], "edges": [ { "id": "e0", "sources": ["p3"], "targets": ["p2_g739884"] }, { "id": "e1", "sources": ["p3_g539420"], "targets": ["p2_g653563"] }, { "id": "e2", "sources": ["p5"], "targets": ["p1_g255145"] }, { "id": "e3", "sources": ["p6"], "targets": ["p1_g820682"] }, { "id": "e4", "sources": ["p7"], "targets": ["p1_g261431"] }, { "id": "e5", "sources": ["p6"], "targets": ["p2_g732355"] }, { "id": "e6", "sources": ["p2_g506569"], "targets": ["p1_g650325"] }, { "id": "e7", "sources": ["p7"], "targets": ["p2_g008338"] }, { "id": "e8", "sources": ["p2_g518944"], "targets": ["p1_g006148"] }, { "id": "e9", "sources": ["p2_g506569"], "targets": ["p1_g044769"] }, { "id": "e10", "sources": ["p2_g518944"], "targets": ["p1_g598580"] }, { "id": "e11", "sources": ["p3_g158827"], "targets": ["p1_g445341"] }, { "id": "e12", "sources": ["p1_g445341"], "targets": ["p3_g722216"] }, { "id": "e13", "sources": ["p3_g606176"], "targets": ["p1_g930680"] }, { "id": "e14", "sources": ["p1_g930680"], "targets": ["p1_g572291"] } ] } ``` ### ER 图 - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/er/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/er/ - Source path: examples/showcase/practices/demo/er.tsx ```tsx import exampleData from '../../../data/er.json' import React from 'react' import { Graph, Node, Edge } from '@antv/x6' import insertCss from 'insert-css' import { register } from '@antv/x6-react-shape' // 类型定义 interface TableField { name: string type: string keyType?: 'primary' | 'foreign' | 'unique' ref?: { table: string; field: string } defaultValue?: string } interface TableNodeData { id: string name: string fields: TableField[] } interface RelationshipData { type: '1:1' | '1:N' | 'N:N' } // 图标映射 const ICONS: Record<NonNullable<TableField['keyType']>, string> = { primary: '🔑', foreign: '🔗', unique: '⭐', } // React节点组件 const ERTableNode = React.memo(({ node }: { node: Node }) => { const { name, fields = [] } = node.getData<TableNodeData>() const height = 36 + fields.length * 24 + 12 return ( <div className="er-table" style={{ height }}> <div className="er-table-header">{name}</div> <div className="er-table-fields"> {fields.length === 0 ? ( <div className="er-table-empty">No fields</div> ) : ( fields.map((field, i) => ( <div key={field.name} className={`er-table-field ${i % 2 === 0 ? 'even' : 'odd'} ${ field.keyType || '' }`} > <span className="er-field-icon"> {field.keyType ? ICONS[field.keyType] : ''} </span> <span className="er-field-name">{field.name}</span> <span className="er-field-type">{field.type}</span> </div> )) )} </div> </div> ) }) // 注册节点 register({ shape: 'er-table', width: 240, height: 300, component: ERTableNode, effect: ['data'], ports: { // 连接点配置 groups: { top: { position: 'top', attrs: { circle: { r: 4, magnet: true, stroke: '#1890ff', fill: '#fff' }, }, }, right: { position: 'right', attrs: { circle: { r: 4, magnet: true, stroke: '#1890ff', fill: '#fff' }, }, }, bottom: { position: 'bottom', attrs: { circle: { r: 4, magnet: true, stroke: '#1890ff', fill: '#fff' }, }, }, left: { position: 'left', attrs: { circle: { r: 4, magnet: true, stroke: '#1890ff', fill: '#fff' }, }, }, }, items: [ { id: 'top', group: 'top' }, { id: 'right', group: 'right' }, { id: 'bottom', group: 'bottom' }, { id: 'left', group: 'left' }, ], }, }) // 注销边,防止重复注册报错 Graph.unregisterEdge('er-relationship') // 注册边 Graph.registerEdge('er-relationship', { inherit: 'edge', attrs: { line: { stroke: '#666', strokeWidth: 2, targetMarker: { name: 'classic', size: 8 }, }, }, labels: [ { attrs: { text: { text: '1:1', fill: '#666', fontSize: 12, fontWeight: 'bold' }, rect: { fill: '#fff', stroke: '#666', rx: 3, ry: 3 }, }, }, ], }) // 图配置 const graph: Graph = new Graph({ container: document.getElementById('container')!, background: { color: '#f8f9fa' }, grid: { visible: true, type: 'dot', args: { color: '#e0e0e0' } }, mousewheel: { enabled: true, modifiers: 'ctrl', minScale: 0.3, maxScale: 2 }, panning: { enabled: true, eventTypes: ['leftMouseDown', 'rightMouseDown'] }, connecting: { snap: true, allowBlank: false, allowLoop: false, allowMulti: false, highlight: true, router: { name: 'manhattan' }, connector: { name: 'rounded', args: { radius: 8 } }, createEdge: (): Edge => graph.createEdge({ shape: 'er-relationship', data: { type: '1:1' } as RelationshipData, labels: [{ attrs: { text: { text: '1:1' } } }], }), validateMagnet: ({ magnet }) => magnet.getAttribute('port-group') !== null, }, }) // 工具函数 const transformToGraphData = (tables: TableNodeData[]) => { const layout = { cols: 2, nodeWidth: 240, spacing: 100 } return tables.reduce( (acc, table, index) => { const row = Math.floor(index / layout.cols) const col = index % layout.cols const height = 36 + table.fields.length * 24 + 12 acc.nodes.push({ id: table.id, shape: 'er-table', x: col * (layout.nodeWidth + layout.spacing), y: row * (height + layout.spacing), width: layout.nodeWidth, height, data: table, }) table.fields.forEach((field) => { if (field.keyType === 'foreign' && field.ref) { acc.edges.push({ id: `${table.id}-${field.ref.table}-${field.name}`, shape: 'er-relationship', source: { cell: table.id, port: 'right' }, target: { cell: field.ref.table, port: 'left' }, data: { type: '1:N' } as RelationshipData, labels: [{ attrs: { text: { text: '1:N' } } }], }) } }) return acc }, { nodes: [] as Node[], edges: [] as any[] }, ) } const toggleRelationshipType = (edge: Edge) => { const types: RelationshipData['type'][] = ['1:1', '1:N', 'N:N'] const current = edge.getData<RelationshipData>()?.type || '1:1' const next = types[(types.indexOf(current) + 1) % types.length] edge.setData({ ...edge.getData(), type: next }) edge.setLabels([ { attrs: { text: { text: next, fill: '#666', fontSize: 12, fontWeight: 'bold' }, rect: { fill: '#fff', stroke: '#666', rx: 3, ry: 3 }, }, position: 0.5, }, ]) } // 事件绑定 graph.on('edge:click', ({ edge, e }) => { if (edge.shape === 'er-relationship') { e.stopPropagation() toggleRelationshipType(edge) } }) graph.on('edge:mouseenter', ({ edge }) => { if (edge.shape === 'er-relationship') { edge.attr('line/stroke', '#1890ff') edge.addTools([ { name: 'button-remove', args: { distance: -40, markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 8, fill: '#ff4d4f', stroke: '#fff', strokeWidth: 2, cursor: 'pointer', }, }, { tagName: 'text', selector: 'icon', textContent: '×', attrs: { fill: '#fff', fontSize: 12, fontWeight: 'bold', textAnchor: 'middle', dominantBaseline: 'central', }, }, ], }, }, ]) } }) graph.on('edge:mouseleave', ({ edge }) => { edge.attr('line/stroke', '#666') edge.removeTools() }) // 初始化 const initializeGraph = async () => { const { nodes, edges } = transformToGraphData(exampleData) graph.addNodes(nodes) graph.addEdges(edges) graph.centerContent() } initializeGraph() export default function ERDiagram() { return <div id="container" style={{ width: '100%', height: '100vh' }} /> } // 样式 insertCss(` .er-table { display: flex; flex-direction: column; width: 100%; height: 100%; background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%); border: 2px solid #1890ff; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; transition: all 0.3s ease; } .er-table:hover { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); } .er-table-header { background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); color: white; padding: 8px 12px; font-weight: 600; font-size: 13px; text-align: center; border-radius: 6px 6px 0 0; display: flex; align-items: center; justify-content: center; gap: 6px; } .er-table-fields { flex: 1; overflow: hidden; border-radius: 0 0 6px 6px; } .er-table-empty { padding: 20px; text-align: center; color: #999; font-style: italic; } .er-table-field { display: flex; align-items: center; padding: 4px 8px; font-size: 11px; border-bottom: 1px solid #e8e8e8; transition: background-color 0.2s ease; } .er-table-field:hover { background-color: #f5f5f5; } .er-table-field.even { background-color: #fafafa; } .er-table-field:last-child { border-bottom: none; border-radius: 0 0 6px 6px; } .er-table-field.primary { background-color: #e6f7ff; } .er-table-field.foreign { background-color: #fff7e6; } .er-table-field.unique { background-color: #f6ffed; } .er-field-icon { margin-right: 4px; font-size: 10px; } .er-field-name { flex: 1; font-weight: 500; color: #262626; } .er-field-type { color: #666; font-family: 'Monaco', 'Menlo', monospace; font-size: 10px; } .er-field-not-null { color: #ff4d4f; font-size: 9px; font-weight: bold; margin-left: 4px; } .x6-port-body { opacity: 0; transition: opacity 0.2s ease; } .x6-node:hover .x6-port-body { opacity: 1; } .x6-port-body circle { fill: #fff; stroke: #1890ff; stroke-width: 2; cursor: crosshair; r: 8; } .x6-port:hover .x6-port-body circle { fill: #e6f7ff; stroke: #40a9ff; stroke-width: 3; r: 10; } .x6-port-available .x6-port-body circle { fill: #f6ffed; stroke: #52c41a; stroke-width: 3; r: 10; } .x6-edge:hover path:nth-child(2) { stroke: #1890ff; } `) ``` ### examples/data/er.json ```json [ { "id": "profiles", "name": "profiles", "fields": [ { "name": "id", "type": "bigint", "keyType": "primary" }, { "name": "user_id", "type": "bigint", "keyType": "foreign", "ref": { "table": "users", "field": "id" } }, { "name": "first_name", "type": "varchar(50)" }, { "name": "last_name", "type": "varchar(50)" } ] }, { "id": "users", "name": "users", "fields": [ { "name": "id", "type": "bigint", "keyType": "primary" }, { "name": "email", "type": "varchar(255)", "keyType": "unique" }, { "name": "username", "type": "varchar(50)", "keyType": "unique" }, { "name": "created_at", "type": "timestamp", "defaultValue": "CURRENT_TIMESTAMP" } ] }, { "id": "posts", "name": "posts", "fields": [ { "name": "id", "type": "bigint" }, { "name": "user_id", "type": "bigint", "keyType": "foreign", "ref": { "table": "users", "field": "id" } }, { "name": "title", "type": "varchar(200)" }, { "name": "content", "type": "text" }, { "name": "created_at", "type": "timestamp" } ] } ] ``` ### 流程图 - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/flowchart/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/flowchart/ - Source path: examples/showcase/practices/demo/flowchart.ts ```ts import { Clipboard, Graph, History, Keyboard, Selection, Shape, Snapline, Stencil, Transform, } from '@antv/x6' import insertCss from 'insert-css' // 为了协助代码演示 preWork() // #region 初始化画布 const graph = new Graph({ container: document.getElementById('graph-container') as HTMLElement, grid: true, mousewheel: { enabled: true, zoomAtMousePosition: true, modifiers: 'ctrl', minScale: 0.5, maxScale: 3, }, connecting: { router: 'manhattan', connector: { name: 'rounded', args: { radius: 8, }, }, anchor: 'center', connectionPoint: 'anchor', allowBlank: false, snap: { radius: 20, }, createEdge() { return new Shape.Edge({ attrs: { line: { stroke: '#A2B1C3', strokeWidth: 2, targetMarker: { name: 'block', width: 12, height: 8, }, }, }, zIndex: 0, }) }, validateConnection({ targetMagnet }) { return !!targetMagnet }, }, highlighting: { magnetAdsorbed: { name: 'stroke', args: { attrs: { fill: '#5F95FF', stroke: '#5F95FF', }, }, }, }, }) // #endregion // #region 使用插件 graph .use( new Transform({ resizing: true, rotating: true, }), ) .use( new Selection({ rubberband: true, showNodeSelectionBox: true, }), ) .use(new Snapline()) .use(new Keyboard()) .use(new Clipboard()) .use(new History()) // #endregion // #region 初始化 stencil const stencil = new Stencil({ title: '流程图', target: graph, stencilGraphWidth: 200, stencilGraphHeight: 180, stencilGraphOptions: { panning: true }, collapsable: true, groups: [ { title: '基础流程图', name: 'group1', }, { title: '系统设计图', name: 'group2', graphHeight: 250, layoutOptions: { rowHeight: 70, }, }, ], layoutOptions: { columns: 2, columnWidth: 80, rowHeight: 55, }, }) document .getElementById('stencil') ?.appendChild(stencil.container as HTMLElement) // #endregion // #region 快捷键与事件 graph.bindKey(['meta+c', 'ctrl+c'], () => { const cells = graph.getSelectedCells() if (cells.length) { graph.copy(cells) } return false }) graph.bindKey(['meta+x', 'ctrl+x'], () => { const cells = graph.getSelectedCells() if (cells.length) { graph.cut(cells) } return false }) graph.bindKey(['meta+v', 'ctrl+v'], () => { if (!graph.isClipboardEmpty()) { const cells = graph.paste({ offset: 32 }) graph.cleanSelection() graph.select(cells) } return false }) // undo redo graph.bindKey(['meta+z', 'ctrl+z'], () => { if (graph.canUndo()) { graph.undo() } return false }) graph.bindKey(['meta+shift+z', 'ctrl+shift+z'], () => { if (graph.canRedo()) { graph.redo() } return false }) // select all graph.bindKey(['meta+a', 'ctrl+a'], () => { const nodes = graph.getNodes() if (nodes) { graph.select(nodes) } }) // delete graph.bindKey('backspace', () => { const cells = graph.getSelectedCells() if (cells.length) { graph.removeCells(cells) } }) // zoom graph.bindKey(['ctrl+1', 'meta+1'], () => { const zoom = graph.zoom() if (zoom < 1.5) { graph.zoom(0.1) } }) graph.bindKey(['ctrl+2', 'meta+2'], () => { const zoom = graph.zoom() if (zoom > 0.5) { graph.zoom(-0.1) } }) // 控制连接桩显示/隐藏 const showPorts = (ports: NodeListOf<SVGElement>, show: boolean) => { for (let i = 0, len = ports.length; i < len; i += 1) { ports[i].style.visibility = show ? 'visible' : 'hidden' } } graph.on('node:mouseenter', () => { const container = document.getElementById('graph-container') as HTMLElement const ports = container.querySelectorAll( '.x6-port-body', ) as NodeListOf<SVGElement> showPorts(ports, true) }) graph.on('node:mouseleave', () => { const container = document.getElementById('graph-container') as HTMLElement const ports = container.querySelectorAll( '.x6-port-body', ) as NodeListOf<SVGElement> showPorts(ports, false) }) // #endregion // #region 初始化图形 const ports = { groups: { top: { position: 'top', attrs: { circle: { r: 4, magnet: true, stroke: '#5F95FF', strokeWidth: 1, fill: '#fff', style: { visibility: 'hidden', }, }, }, }, right: { position: 'right', attrs: { circle: { r: 4, magnet: true, stroke: '#5F95FF', strokeWidth: 1, fill: '#fff', style: { visibility: 'hidden', }, }, }, }, bottom: { position: 'bottom', attrs: { circle: { r: 4, magnet: true, stroke: '#5F95FF', strokeWidth: 1, fill: '#fff', style: { visibility: 'hidden', }, }, }, }, left: { position: 'left', attrs: { circle: { r: 4, magnet: true, stroke: '#5F95FF', strokeWidth: 1, fill: '#fff', style: { visibility: 'hidden', }, }, }, }, }, items: [ { group: 'top', }, { group: 'right', }, { group: 'bottom', }, { group: 'left', }, ], } Graph.registerNode( 'custom-rect', { inherit: 'rect', width: 66, height: 36, attrs: { body: { strokeWidth: 1, stroke: '#5F95FF', fill: '#EFF4FF', }, text: { fontSize: 12, fill: '#262626', }, }, ports: { ...ports }, }, true, ) Graph.registerNode( 'custom-polygon', { inherit: 'polygon', width: 66, height: 36, attrs: { body: { strokeWidth: 1, stroke: '#5F95FF', fill: '#EFF4FF', }, text: { fontSize: 12, fill: '#262626', }, }, ports: { ...ports, items: [ { group: 'top', }, { group: 'bottom', }, ], }, }, true, ) Graph.registerNode( 'custom-circle', { inherit: 'circle', width: 45, height: 45, attrs: { body: { strokeWidth: 1, stroke: '#5F95FF', fill: '#EFF4FF', }, text: { fontSize: 12, fill: '#262626', }, }, ports: { ...ports }, }, true, ) Graph.registerNode( 'custom-image', { inherit: 'rect', width: 52, height: 52, markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'image', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { stroke: '#5F95FF', fill: '#5F95FF', }, image: { width: 26, height: 26, refX: 13, refY: 16, }, label: { refX: 3, refY: 2, textAnchor: 'left', textVerticalAnchor: 'top', fontSize: 12, fill: '#fff', }, }, ports: { ...ports }, }, true, ) const r1 = graph.createNode({ shape: 'custom-rect', label: '开始', attrs: { body: { rx: 20, ry: 26, }, }, }) const r2 = graph.createNode({ shape: 'custom-rect', label: '过程', }) const r3 = graph.createNode({ shape: 'custom-rect', attrs: { body: { rx: 6, ry: 6, }, }, label: '可选过程', }) const r4 = graph.createNode({ shape: 'custom-polygon', attrs: { body: { refPoints: '0,10 10,0 20,10 10,20', }, }, label: '决策', }) const r5 = graph.createNode({ shape: 'custom-polygon', attrs: { body: { refPoints: '10,0 40,0 30,20 0,20', }, }, label: '数据', }) const r6 = graph.createNode({ shape: 'custom-circle', label: '连接', }) stencil.load([r1, r2, r3, r4, r5, r6], 'group1') const imageShapes = [ { label: 'Client', image: 'https://gw.alipayobjects.com/zos/bmw-prod/687b6cb9-4b97-42a6-96d0-34b3099133ac.svg', }, { label: 'Http', image: 'https://gw.alipayobjects.com/zos/bmw-prod/dc1ced06-417d-466f-927b-b4a4d3265791.svg', }, { label: 'Api', image: 'https://gw.alipayobjects.com/zos/bmw-prod/c55d7ae1-8d20-4585-bd8f-ca23653a4489.svg', }, { label: 'Sql', image: 'https://gw.alipayobjects.com/zos/bmw-prod/6eb71764-18ed-4149-b868-53ad1542c405.svg', }, { label: 'Clound', image: 'https://gw.alipayobjects.com/zos/bmw-prod/c36fe7cb-dc24-4854-aeb5-88d8dc36d52e.svg', }, { label: 'Mq', image: 'https://gw.alipayobjects.com/zos/bmw-prod/2010ac9f-40e7-49d4-8c4a-4fcf2f83033b.svg', }, ] const imageNodes = imageShapes.map((item) => graph.createNode({ shape: 'custom-image', label: item.label, attrs: { image: { 'xlink:href': item.image, }, }, }), ) stencil.load(imageNodes, 'group2') // #endregion function preWork() { // 这里协助演示的代码,在实际项目中根据实际情况进行调整 const container = document.getElementById('container') as HTMLElement const stencilContainer = document.createElement('div') stencilContainer.id = 'stencil' const graphContainer = document.createElement('div') graphContainer.id = 'graph-container' container.appendChild(stencilContainer) container.appendChild(graphContainer) insertCss(` #container { display: flex; border: 1px solid #dfe3e8; } #stencil { width: 180px; height: 100%; position: relative; border-right: 1px solid #dfe3e8; } #graph-container { width: calc(100% - 180px); height: 100%; } .x6-widget-stencil { background-color: #fff; } .x6-widget-stencil-title { background-color: #fff; } .x6-widget-stencil-group-title { background-color: #fff !important; } .x6-widget-transform { margin: -1px 0 0 -1px; padding: 0px; border: 1px solid #239edd; } .x6-widget-transform > div { border: 1px solid #239edd; } .x6-widget-transform > div:hover { background-color: #3dafe4; } .x6-widget-transform-active-handle { background-color: #3dafe4; } .x6-widget-transform-resize { border-radius: 0; } .x6-widget-selection-inner { border: 1px solid #239edd; } .x6-widget-selection-box { opacity: 0; } `) } ``` ### 思维导图 - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/mindmap/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/mindmap/ - Source path: examples/showcase/practices/demo/mindmap.ts ```ts import { Graph, Cell, Node, Path, Keyboard, Selection } from '@antv/x6' import Hierarchy from '@antv/hierarchy' import insertCss from 'insert-css' // 中心主题或分支主题 Graph.registerNode( 'topic', { inherit: 'rect', markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'image', selector: 'img', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { rx: 6, ry: 6, stroke: '#5F95FF', fill: '#EFF4FF', strokeWidth: 1, }, img: { ref: 'body', refX: '100%', refY: '50%', refY2: -8, width: 16, height: 16, 'xlink:href': 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*SYCuQ6HHs5cAAAAAAAAAAAAAARQnAQ', event: 'add:topic', class: 'topic-image', }, label: { fontSize: 14, fill: '#262626', }, }, }, true, ) // 子主题 Graph.registerNode( 'topic-child', { inherit: 'rect', markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'text', selector: 'label', }, { tagName: 'path', selector: 'line', }, ], attrs: { body: { fill: '#ffffff', strokeWidth: 0, stroke: '#5F95FF', }, label: { fontSize: 14, fill: '#262626', textVerticalAnchor: 'bottom', }, line: { stroke: '#5F95FF', strokeWidth: 2, d: 'M 0 15 L 60 15', }, }, }, true, ) // 连接器 Graph.registerConnector( 'mindmap', (sourcePoint, targetPoint, routerPoints, options) => { const midX = sourcePoint.x + 10 const midY = sourcePoint.y const ctrX = (targetPoint.x - midX) / 5 + midX const ctrY = targetPoint.y const pathData = ` M ${sourcePoint.x} ${sourcePoint.y} L ${midX} ${midY} Q ${ctrX} ${ctrY} ${targetPoint.x} ${targetPoint.y} ` return options.raw ? Path.parse(pathData) : pathData }, true, ) // 边 Graph.registerEdge( 'mindmap-edge', { inherit: 'edge', connector: { name: 'mindmap', }, attrs: { line: { targetMarker: '', stroke: '#A2B1C3', strokeWidth: 2, }, }, zIndex: 0, }, true, ) interface MindMapData { id: string type: 'topic' | 'topic-branch' | 'topic-child' label: string width: number height: number children?: MindMapData[] } interface HierarchyResult { id: string x: number y: number data: MindMapData children?: HierarchyResult[] } const data: MindMapData = { id: '1', type: 'topic', label: '中心主题', width: 160, height: 50, children: [ { id: '1-1', type: 'topic-branch', label: '分支主题1', width: 100, height: 40, children: [ { id: '1-1-1', type: 'topic-child', label: '子主题1', width: 60, height: 30, }, { id: '1-1-2', type: 'topic-child', label: '子主题2', width: 60, height: 30, }, ], }, { id: '1-2', type: 'topic-branch', label: '分支主题2', width: 100, height: 40, }, ], } const graph = new Graph({ container: document.getElementById('container')!, connecting: { connectionPoint: 'anchor', }, }) graph.use(new Selection()) graph.use(new Keyboard()) const render = () => { const result: HierarchyResult = Hierarchy.mindmap(data, { direction: 'H', getHeight(d: MindMapData) { return d.height }, getWidth(d: MindMapData) { return d.width }, getHGap() { return 40 }, getVGap() { return 20 }, getSide: () => { return 'right' }, }) const cells: Cell[] = [] const traverse = (hierarchyItem: HierarchyResult) => { if (hierarchyItem) { const { data, children } = hierarchyItem cells.push( graph.createNode({ id: data.id, shape: data.type === 'topic-child' ? 'topic-child' : 'topic', x: hierarchyItem.x, y: hierarchyItem.y, width: data.width, height: data.height, label: data.label, type: data.type, }), ) if (children) { children.forEach((item: HierarchyResult) => { const { id, data } = item cells.push( graph.createEdge({ shape: 'mindmap-edge', source: { cell: hierarchyItem.id, anchor: data.type === 'topic-child' ? { name: 'right', args: { dx: -16, }, } : { name: 'center', args: { dx: '25%', }, }, }, target: { cell: id, anchor: { name: 'left', }, }, }), ) traverse(item) }) } } } traverse(result) graph.resetCells(cells) graph.centerContent() } const findItem = ( obj: MindMapData, id: string, ): { parent: MindMapData | null node: MindMapData | null } | null => { if (obj.id === id) { return { parent: null, node: obj, } } const { children } = obj if (children) { for (let i = 0, len = children.length; i < len; i += 1) { const res = findItem(children[i], id) if (res) { return { parent: res.parent || obj, node: res.node, } } } } return null } const addChildNode = (id: string, type: NodeType) => { const res = findItem(data, id) const dataItem = res?.node if (dataItem) { let item: MindMapData | null = null const length = dataItem.children ? dataItem.children.length : 0 if (type === 'topic') { item = { id: `${id}-${length + 1}`, type: 'topic-branch', label: `分支主题${length + 1}`, width: 100, height: 40, } } else if (type === 'topic-branch') { item = { id: `${id}-${length + 1}`, type: 'topic-child', label: `子主题${length + 1}`, width: 60, height: 30, } } if (item) { if (dataItem.children) { dataItem.children.push(item) } else { dataItem.children = [item] } return item } } return null } const removeNode = (id: string) => { const res = findItem(data, id) const dataItem = res?.parent if (dataItem && dataItem.children) { const { children } = dataItem const index = children.findIndex((item) => item.id === id) return children.splice(index, 1) } return null } graph.on('add:topic', ({ node }: { node: Node }) => { const { id } = node const type = node.prop('type') if (addChildNode(id, type)) { render() } }) graph.bindKey(['backspace', 'delete'], () => { const selectedNodes = graph.getSelectedCells().filter((item) => item.isNode()) if (selectedNodes.length) { const { id } = selectedNodes[0] if (removeNode(id)) { render() } } }) graph.bindKey('tab', (e) => { e.preventDefault() const selectedNodes = graph.getSelectedCells().filter((item) => item.isNode()) if (selectedNodes.length) { const node = selectedNodes[0] const type = node.prop('type') if (addChildNode(node.id, type)) { render() } } }) render() insertCss(` .topic-image { visibility: hidden; cursor: pointer; } .x6-node:hover .topic-image { visibility: visible; } .x6-node-selected rect { stroke-width: 2px; } `) ``` ### 组织架构图 - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/org/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/org/ - Source path: examples/showcase/practices/demo/org.ts ```ts import { Graph, Node, Point } from '@antv/x6' Graph.registerNode( 'org-node', { width: 180, height: 60, markup: [ { tagName: 'rect', selector: 'body', }, { tagName: 'image', selector: 'avatar', }, { tagName: 'text', selector: 'rank', }, { tagName: 'text', selector: 'name', }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', fill: '#5F95FF', stroke: '#5F95FF', strokeWidth: 1, rx: 10, ry: 10, pointerEvents: 'visiblePainted', }, avatar: { width: 48, height: 48, refX: 8, refY: 6, }, rank: { refX: 0.9, refY: 0.2, fill: '#fff', fontFamily: 'Courier New', fontSize: 14, textAnchor: 'end', textDecoration: 'underline', }, name: { refX: 0.9, refY: 0.6, fill: '#fff', fontFamily: 'Courier New', fontSize: 14, fontWeight: '800', textAnchor: 'end', }, }, }, true, ) Graph.registerEdge( 'org-edge', { zIndex: -1, attrs: { line: { fill: 'none', strokeLinejoin: 'round', strokeWidth: 2, stroke: '#A2B1C3', sourceMarker: null, targetMarker: null, }, }, }, true, ) const graph = new Graph({ container: document.getElementById('container')!, connecting: { anchor: 'orth', }, }) function member( x: number, y: number, rank: string, name: string, image: string, ) { return graph.addNode({ x, y, shape: 'org-node', attrs: { avatar: { opacity: 0.7, 'xlink:href': image, }, rank: { text: rank, wordSpacing: '-5px', letterSpacing: 0, }, name: { text: name, fontSize: 13, fontFamily: 'Arial', letterSpacing: 0, }, }, }) } function link(source: Node, target: Node, vertices: Point.PointLike[]) { return graph.addEdge({ vertices, source: { cell: source, }, target: { cell: target, }, shape: 'org-edge', }) } const male = 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*kUy8SrEDp6YAAAAAAAAAAAAAARQnAQ' const female = 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*f6hhT75YjkIAAAAAAAAAAAAAARQnAQ' const bart = member(300, 70, 'CEO', 'Bart Simpson', male) const homer = member(90, 200, 'VP Marketing', 'Homer Simpson', male) const marge = member(300, 200, 'VP Sales', 'Marge Simpson', female) const lisa = member(500, 200, 'VP Production', 'Lisa Simpson', female) const maggie = member(400, 350, 'Manager', 'Maggie Simpson', female) const lenny = member(190, 350, 'Manager', 'Lenny Leonard', male) const carl = member(190, 500, 'Manager', 'Carl Carlson', male) link(bart, marge, [ { x: 385, y: 180, }, ]) link(bart, homer, [ { x: 385, y: 180, }, { x: 175, y: 180, }, ]) link(bart, lisa, [ { x: 385, y: 180, }, { x: 585, y: 180, }, ]) link(homer, lenny, [ { x: 175, y: 380, }, ]) link(homer, carl, [ { x: 175, y: 530, }, ]) link(marge, maggie, [ { x: 385, y: 380, }, ]) graph.zoomToFit({ padding: 20, maxScale: 1 }) ``` ### 组织架构图(自动布局) - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/orgchart/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/orgchart/ - Source path: examples/showcase/practices/demo/orgchart.ts ```ts import { Graph, Cell, Node, Dom } from '@antv/x6' import dagre from 'dagre' import insertCss from 'insert-css' // 定义样式 insertCss(` .x6-cell { cursor: default; } .x6-node .btn { cursor: pointer; } `) // 自定义节点 Graph.registerNode( 'org-node', { width: 260, height: 88, markup: [ { tagName: 'rect', attrs: { class: 'card', }, }, { tagName: 'image', attrs: { class: 'image', }, }, { tagName: 'text', attrs: { class: 'rank', }, }, { tagName: 'text', attrs: { class: 'name', }, }, { tagName: 'g', attrs: { class: 'btn add', }, children: [ { tagName: 'circle', attrs: { class: 'add', }, }, { tagName: 'text', attrs: { class: 'add', }, }, ], }, { tagName: 'g', attrs: { class: 'btn del', }, children: [ { tagName: 'circle', attrs: { class: 'del', }, }, { tagName: 'text', attrs: { class: 'del', }, }, ], }, ], attrs: { '.card': { rx: 10, ry: 10, refWidth: '100%', refHeight: '100%', fill: '#5F95FF', stroke: '#5F95FF', strokeWidth: 1, pointerEvents: 'visiblePainted', }, '.image': { x: 16, y: 16, width: 56, height: 56, opacity: 0.7, }, '.rank': { refX: 0.95, refY: 0.5, fill: '#fff', fontFamily: 'Courier New', fontSize: 13, textAnchor: 'end', textVerticalAnchor: 'middle', }, '.name': { refX: 0.95, refY: 0.7, fill: '#fff', fontFamily: 'Arial', fontSize: 14, fontWeight: '600', textAnchor: 'end', }, '.btn.add': { refDx: -16, refY: 16, event: 'node:add', }, '.btn.del': { refDx: -44, refY: 16, event: 'node:delete', }, '.btn > circle': { r: 10, fill: 'transparent', stroke: '#fff', strokeWidth: 1, }, '.btn.add > text': { fontSize: 20, fontWeight: 800, fill: '#fff', x: -5.5, y: 7, fontFamily: 'Times New Roman', text: '+', }, '.btn.del > text': { fontSize: 28, fontWeight: 500, fill: '#fff', x: -4.5, y: 6, fontFamily: 'Times New Roman', text: '-', }, }, }, true, ) // 自定义边 Graph.registerEdge( 'org-edge', { zIndex: -1, attrs: { line: { strokeWidth: 2, stroke: '#A2B1C3', sourceMarker: null, targetMarker: null, }, }, }, true, ) const male = 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*kUy8SrEDp6YAAAAAAAAAAAAAARQnAQ' const female = 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*f6hhT75YjkIAAAAAAAAAAAAAARQnAQ' // 布局方向 const dir = 'LR' // LR RL TB BT // 创建画布 const graph = new Graph({ container: document.getElementById('container')!, scroller: true, interacting: false, }) // 监听自定义事件 function setup() { graph.on('node:add', ({ e, node }) => { e.stopPropagation() const member = createNode( 'Employee', 'New Employee', Math.random() < 0.5 ? male : female, ) graph.addCell([member, createEdge(node, member)]) layout() }) graph.on('node:delete', ({ e, node }) => { e.stopPropagation() graph.removeCell(node) layout() }) } // 自动布局 function layout() { const nodes = graph.getNodes() const edges = graph.getEdges() const g = new dagre.graphlib.Graph() g.setGraph({ rankdir: dir, nodesep: 16, ranksep: 16 }) g.setDefaultEdgeLabel(() => ({})) const width = 260 const height = 90 nodes.forEach((node) => { g.setNode(node.id, { width, height }) }) edges.forEach((edge) => { const source = edge.getSource() const target = edge.getTarget() g.setEdge(source.cell, target.cell) }) dagre.layout(g) g.nodes().forEach((id) => { const node = graph.getCellById(id) as Node if (node) { const pos = g.node(id) node.position(pos.x, pos.y) } }) edges.forEach((edge) => { const source = edge.getSourceNode()! const target = edge.getTargetNode()! const sourceBBox = source.getBBox() const targetBBox = target.getBBox() if ((dir === 'LR' || dir === 'RL') && sourceBBox.y !== targetBBox.y) { const gap = dir === 'LR' ? targetBBox.x - sourceBBox.x - sourceBBox.width : -sourceBBox.x + targetBBox.x + targetBBox.width const fix = dir === 'LR' ? sourceBBox.width : 0 const x = sourceBBox.x + fix + gap / 2 edge.setVertices([ { x, y: sourceBBox.center.y }, { x, y: targetBBox.center.y }, ]) } else if ( (dir === 'TB' || dir === 'BT') && sourceBBox.x !== targetBBox.x ) { const gap = dir === 'TB' ? targetBBox.y - sourceBBox.y - sourceBBox.height : -sourceBBox.y + targetBBox.y + targetBBox.height const fix = dir === 'TB' ? sourceBBox.height : 0 const y = sourceBBox.y + fix + gap / 2 edge.setVertices([ { x: sourceBBox.center.x, y }, { x: targetBBox.center.x, y }, ]) } else { edge.setVertices([]) } }) } function createNode(rank: string, name: string, image: string) { return graph.createNode({ shape: 'org-node', attrs: { '.image': { xlinkHref: image }, '.rank': { text: Dom.breakText(rank, { width: 160, height: 45 }), }, '.name': { text: Dom.breakText(name, { width: 160, height: 45 }), }, }, }) } function createEdge(source: Cell, target: Cell) { return graph.createEdge({ shape: 'org-edge', source: { cell: source.id }, target: { cell: target.id }, }) } const nodes = [ createNode('Founder & Chairman', 'Pierre Omidyar', male), createNode('President & CEO', 'Margaret C. Whitman', female), createNode('President, PayPal', 'Scott Thompson', male), createNode('President, Ebay Global Marketplaces', 'Devin Wenig', male), createNode('Senior Vice President Human Resources', 'Jeffrey S. Skoll', male), createNode('Senior Vice President Controller', 'Steven P. Westly', male), ] const edges = [ createEdge(nodes[0], nodes[1]), createEdge(nodes[1], nodes[2]), createEdge(nodes[1], nodes[3]), createEdge(nodes[1], nodes[4]), createEdge(nodes[1], nodes[5]), ] graph.resetCells([...nodes, ...edges]) layout() graph.zoomTo(0.8) graph.centerContent() setup() ``` ### 框选批量拖拽多节点 - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/selectiondrag/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/selectiondrag/ - Source path: examples/showcase/practices/demo/selectionDrag.ts ```ts import { Graph, type Node, Selection } from '@antv/x6' import insertCss from 'insert-css' const host = document.getElementById('container')! host.classList.add('selection-drag') const graph = new Graph({ container: host, grid: true, mousewheel: { enabled: true, factor: 1.1, minScale: 0.1, maxScale: 2, }, panning: false, }) graph.use( new Selection({ rubberband: true, multiple: true, strict: true, // 展示框选节点边框 showNodeSelectionBox: true, }), ) const nodes: Node[] = [] const nodeWidth = 60 const nodeHeight = 30 const cols = 10 const spacingX = 90 const spacingY = 50 const count = 120 for (let i = 0; i < count; i++) { const row = Math.floor(i / cols) const col = i % cols const x = 50 + col * spacingX const y = 50 + row * spacingY const node = graph.addNode({ x, y, width: nodeWidth, height: nodeHeight, attrs: { label: { text: `Node ${i + 1}`, fontSize: 12, fill: '#0958D9', }, body: { fill: i % 2 === 0 ? '#E6F4FF' : '#FFFFFF', stroke: '#1677ff', strokeWidth: 1, rx: 4, ry: 4, }, }, }) nodes.push(node) } for (let i = 0; i < count; i++) { const col = i % cols if (col < cols - 1) { const rightIndex = i + 1 if (rightIndex < nodes.length) { graph.addEdge({ source: nodes[i], target: nodes[rightIndex], attrs: { line: { stroke: '#91d5ff', strokeWidth: 1, }, }, }) } } const bottomIndex = i + cols if (bottomIndex < nodes.length) { graph.addEdge({ source: nodes[i], target: nodes[bottomIndex], attrs: { line: { stroke: '#91d5ff', strokeWidth: 1, }, }, }) } } insertCss(` .selection-drag .x6-widget-selection-rubberband { background-color: rgba(22, 119, 255, 0.12); border: 1.5px solid #1677ff; opacity: 1; } .selection-drag .x6-widget-selection-inner { border: 2px solid #1677ff; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.06); } .selection-drag .x6-widget-selection-box { border: 2px dashed #1677ff; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.06); } .selection-drag .x6-node-selected rect { stroke: #1677ff; stroke-width: 1.8px; } .selection-drag .x6-edge:hover path:nth-child(2){ stroke: #1677ff; } .selection-drag .x6-edge-selected path:nth-child(2){ stroke: #1677ff !important; stroke-width: 1.8px !important; } `) ``` ### 简单时间轴 - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/timeline/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/timeline/ - Source path: examples/showcase/practices/demo/timeline.ts ```ts import exampleData from '../../../data/timeline.json' import { Graph, type Cell } from '@antv/x6' import insertCss from 'insert-css' preWork() // 类型定义 interface TimelineEvent { id: string title: string content: string date: string color: string lightColor: string type: 'normal' | 'milestone' } // 常量配置 const CONFIG = { card: { width: 100, height: 80 }, marker: { diameter: 12 }, diamond: { size: 80 }, layout: { timelineY: 400, cardsY: 250, tipsGap: 160 }, timeline: { startX: 250, endX: 1850 }, text: { fontFamily: 'Inter, Helvetica, Arial, sans-serif' }, } const PORTS = { groups: { top: { position: 'top', attrs: { circle: { r: 4, magnet: true, stroke: '#31d0c6', fill: '#fff', strokeWidth: 2, }, }, }, bottom: { position: 'bottom', attrs: { circle: { r: 4, magnet: true, stroke: '#31d0c6', fill: '#fff', strokeWidth: 2, }, }, }, }, } // 滤镜配置 const FILTERS = { card: [ 'drop-shadow(0 6px 16px rgba(0,0,0,0.12))', 'drop-shadow(0 3px 6px rgba(0,0,0,0.06))', ], tips: [ 'drop-shadow(0 6px 20px rgba(0,0,0,0.25))', 'drop-shadow(0 4px 12px rgba(0,0,0,0.15))', ], diamond: [ 'drop-shadow(0 6px 20px rgba(65,105,225,0.5))', 'drop-shadow(0 4px 12px rgba(65,105,225,0.3))', ], } // 注册节点 Graph.registerNode( 'tips-node', { inherit: 'rect', width: 200, height: 140, markup: [ { tagName: 'rect', selector: 'body' }, { tagName: 'rect', selector: 'header' }, { tagName: 'text', selector: 'headerText' }, { tagName: 'text', selector: 'line1' }, { tagName: 'text', selector: 'line2' }, { tagName: 'text', selector: 'line3' }, { tagName: 'text', selector: 'line4' }, ], attrs: { body: { width: 200, height: 140, fill: '#FFFFFF', stroke: 'none', rx: 6, ry: 6, filter: FILTERS.tips[1], }, header: { width: 200, height: 40, fill: '#1a1a1a', stroke: 'none', rx: 6, ry: 6, }, headerText: { refX: 100, refY: 20, text: 'Timeline', fill: '#FFFFFF', fontSize: 18, fontWeight: 'bold', textAnchor: 'middle', dominantBaseline: 'middle', ...CONFIG.text, }, line1: { refX: 16, refY: 60, text: 'A simple linear', fill: '#666', fontSize: 12, textAnchor: 'start', ...CONFIG.text, }, line2: { refX: 16, refY: 78, text: 'timeline showing', fill: '#666', fontSize: 12, textAnchor: 'start', ...CONFIG.text, }, line3: { refX: 16, refY: 96, text: 'events connected', fill: '#666', fontSize: 12, textAnchor: 'start', ...CONFIG.text, }, line4: { refX: 16, refY: 114, text: 'to horizontal axis.', fill: '#666', fontSize: 12, textAnchor: 'start', ...CONFIG.text, }, }, }, true, ) Graph.registerNode( 'event-card', { inherit: 'rect', width: CONFIG.card.width, height: CONFIG.card.height, markup: [ { tagName: 'rect', selector: 'body' }, { tagName: 'text', selector: 'title' }, { tagName: 'text', selector: 'content' }, ], attrs: { body: { width: CONFIG.card.width, height: CONFIG.card.height, fill: '#EFF4FF', stroke: 'none', rx: 4, ry: 4, filter: FILTERS.card[1], cursor: 'move', }, title: { refX: '50%', refY: 30, fill: '#333', fontSize: 14, fontWeight: '600', textAnchor: 'middle', dominantBaseline: 'middle', ...CONFIG.text, }, content: { refX: '50%', refY: 50, fill: '#666', fontSize: 11, textAnchor: 'middle', dominantBaseline: 'middle', ...CONFIG.text, }, }, ports: PORTS, }, true, ) Graph.registerNode( 'diamond-milestone', { inherit: 'rect', width: CONFIG.diamond.size, height: CONFIG.diamond.size, markup: [ { tagName: 'path', selector: 'body' }, { tagName: 'text', selector: 'label' }, ], attrs: { body: { refD: 'M 40 0 L 80 40 L 40 80 L 0 40 Z', fill: '#4169E1', stroke: 'none', filter: FILTERS.diamond[1], cursor: 'move', }, label: { refX: '50%', refY: '50%', fill: '#FFF', fontSize: 12, fontWeight: '600', textAnchor: 'middle', dominantBaseline: 'middle', ...CONFIG.text, }, }, ports: { groups: { top: { position: { name: 'absolute', args: { x: '50%', y: '0%' } }, attrs: PORTS.groups.top.attrs, }, bottom: { position: { name: 'absolute', args: { x: '50%', y: '100%' } }, attrs: PORTS.groups.bottom.attrs, }, }, }, }, true, ) Graph.registerNode( 'endpoint-label', { inherit: 'rect', width: 100, height: 40, markup: [ { tagName: 'rect', selector: 'body' }, { tagName: 'text', selector: 'label' }, ], attrs: { body: { width: 100, height: 40, fill: '#FFF', stroke: '#555', strokeWidth: 1, rx: 4, ry: 4, }, label: { refX: '50%', refY: '50%', fill: '#333', fontSize: 14, textAnchor: 'middle', dominantBaseline: 'middle', ...CONFIG.text, }, }, ports: { groups: { top: PORTS.groups.top } }, }, true, ) // 创建画布 const graph = new Graph({ container: document.getElementById('graph-container')!, background: { color: '#FFF' }, grid: { size: 24, visible: true, args: { color: '#E0E0E0', thickness: 2 } }, panning: { enabled: true, eventTypes: ['leftMouseDown', 'mouseWheel'] }, mousewheel: { enabled: true, modifiers: 'ctrl', factor: 1.1, maxScale: 3, minScale: 0.1, }, connecting: { anchor: 'center', connectionPoint: 'anchor', router: { name: 'metro', args: { startDirections: ['bottom'], endDirections: ['top'] }, }, connector: { name: 'rounded', args: { radius: 10 } }, }, interacting: { nodeMovable: true, edgeMovable: false }, }) // 工具函数 const createPorts = (id: string) => [ { id: `${id}-top`, group: 'top' }, { id: `${id}-bottom`, group: 'bottom' }, ] const createEndpoint = ( id: string, x: number, label: string, y = CONFIG.layout.timelineY, ) => [ graph.createNode({ id: `${id}-anchor`, shape: 'circle', x: x - 8, y: y - 8, width: 16, height: 16, attrs: { body: { fill: '#000', stroke: 'none' } }, zIndex: 10, }), graph.createNode({ id: `${id}-label`, shape: 'endpoint-label', x: x - 50, y: y + 20, attrs: { label: { text: label } }, ports: [{ id: `${id}-label-top`, group: 'top' }], zIndex: 2, }), graph.createEdge({ id: `${id}-connector`, source: `${id}-anchor`, target: { cell: `${id}-label`, port: `${id}-label-top` }, attrs: { line: { stroke: '#555', strokeWidth: 1.5, targetMarker: null } }, router: { name: 'orth', args: { padding: 1 } }, zIndex: 3, }), ] // 数据转换 function transformTimelineData(data: TimelineEvent[]): Cell[] { const cells: Cell[] = [] const { timeline, layout, card, marker, diamond } = CONFIG const spacing = (timeline.endX - timeline.startX) / (data.length + 1) // 时间轴主线 cells.push( graph.createEdge({ id: 'timeline-main-axis', source: { x: timeline.startX, y: layout.timelineY }, target: { x: timeline.endX, y: layout.timelineY }, attrs: { line: { stroke: '#AAA', strokeWidth: 2, targetMarker: null } }, zIndex: 5, }), ) // 端点 cells.push(...createEndpoint('start', timeline.startX, 'Start Point')) cells.push(...createEndpoint('end', timeline.endX, 'End Point')) // Tips cells.push( graph.createNode({ id: 'tips-node', shape: 'tips-node', x: timeline.startX - 100, y: layout.timelineY - layout.tipsGap - 80, zIndex: 20, }), ) // 事件 data.forEach((event, i) => { const x = timeline.startX + (i + 1) * spacing if (event.type === 'milestone') { const dY = layout.cardsY - diamond.size - 20 const diamondId = `${event.id}-diamond` cells.push( graph.createNode({ id: diamondId, shape: 'diamond-milestone', x: x - diamond.size / 2, y: dY, attrs: { body: { fill: event.color }, label: { text: event.title }, }, ports: createPorts(diamondId), zIndex: 25, }), graph.createEdge({ id: `${event.id}-diamond-to-card`, source: { cell: diamondId, port: `${diamondId}-bottom` }, target: { cell: `${event.id}-card`, port: `${event.id}-card-top` }, attrs: { line: { stroke: event.color, strokeWidth: 2, strokeDasharray: '4 4', targetMarker: null, }, }, router: { name: 'orth', args: { padding: 1 } }, connector: { name: 'rounded', args: { radius: 8 } }, zIndex: 20, }), ) } cells.push( graph.createNode({ id: `${event.id}-card`, shape: 'event-card', x: x - card.width / 2, y: layout.cardsY, attrs: { body: { fill: event.lightColor }, title: { text: event.title }, content: { text: event.content }, }, ports: createPorts(`${event.id}-card`), zIndex: 15, }), graph.createNode({ id: `${event.id}-marker`, shape: 'circle', x: x - marker.diameter / 2, y: layout.timelineY - marker.diameter / 2, width: marker.diameter, height: marker.diameter, attrs: { body: { fill: event.color, stroke: 'none' } }, zIndex: 8, }), graph.createEdge({ id: `${event.id}-card-to-marker`, source: { cell: `${event.id}-card`, port: `${event.id}-card-bottom` }, target: { cell: `${event.id}-marker`, anchor: 'top' }, attrs: { line: { stroke: '#888', strokeWidth: 1.5, targetMarker: null }, }, router: { name: 'orth', args: { padding: 1 } }, connector: { name: 'rounded', args: { radius: 8 } }, zIndex: 3, }), ) }) return cells } // 交互 graph.on('node:mouseenter', ({ node }) => { const shape = node.shape const id = node.id if (shape === 'event-card') node.attr('body/filter', FILTERS.card[0]) else if (id === 'tips-node') node.attr({ 'body/cursor': 'move', 'body/filter': FILTERS.tips[0] }) else if (shape === 'diamond-milestone') node.attr('body/filter', FILTERS.diamond[0]) else if (id?.includes('-marker')) node.attr('body/filter', `drop-shadow(0 0 8px ${node.attr('body/fill')})`) }) graph.on('node:mouseleave', ({ node }) => { const shape = node.shape const id = node.id if (shape === 'event-card') node.attr('body/filter', FILTERS.card[1]) else if (id === 'tips-node') node.attr('body/filter', FILTERS.tips[1]) else if (shape === 'diamond-milestone') node.attr('body/filter', FILTERS.diamond[1]) else if (id?.includes('-marker')) node.attr('body/filter', '') }) graph.on('edge:mouseenter', ({ edge }) => { const id = edge.id if (id?.includes('diamond-to-card')) edge.attr('line/strokeWidth', 3) else if ( id?.includes('-connector') && !id.includes('start') && !id.includes('end') ) edge.attr({ 'line/stroke': '#555', 'line/strokeWidth': 2 }) }) graph.on('edge:mouseleave', ({ edge }) => { const id = edge.id if (id?.includes('diamond-to-card')) edge.attr('line/strokeWidth', 2) else if ( id?.includes('-connector') && !id.includes('start') && !id.includes('end') ) edge.attr({ 'line/stroke': '#888', 'line/strokeWidth': 1.5 }) }) // 自动缩放居中 let isFirstRender = true graph.on('render:done', () => { if (!isFirstRender) return isFirstRender = false graph.zoomToFit({ padding: 20, maxScale: 1 }) showGraph() }) function showGraph() { document.getElementById('graph-container')?.classList.add('graph-visible') } // 加载数据 Promise.resolve(exampleData) .then((d) => graph.resetCells(transformTimelineData(d))) .catch(() => console.error('数据加载失败')) // 预处理 function preWork() { const container = document.getElementById('container') if (!container) return const graphContainer = document.createElement('div') graphContainer.id = 'graph-container' container.appendChild(graphContainer) insertCss(` * { margin: 0; padding: 0; box-sizing: border-box; } html, body { width: 100%; height: 100%; overflow: hidden; } #container { width: 100vw; height: 100vh; background: #FFF; } #graph-container { width: 100%; height: 100%; opacity: 0; transition: opacity 0.3s; } #graph-container.graph-visible { opacity: 1; } `) } ``` ### examples/data/timeline.json ```json [ { "id": "event-1", "title": "Design", "content": "UI/UX design", "date": "2024-02", "color": "#4169E1", "lightColor": "rgba(65, 105, 225, 0.1)", "type": "normal" }, { "id": "event-2", "title": "Build", "content": "Development", "date": "2024-03", "color": "#FF8C00", "lightColor": "rgba(255, 140, 0, 0.1)", "type": "normal" }, { "id": "milestone-1", "title": "V1.0", "content": "First release", "date": "2024-04", "color": "#4169E1", "lightColor": "rgba(65, 105, 225, 0.1)", "type": "milestone" }, { "id": "event-3", "title": "Test", "content": "QA testing", "date": "2024-05", "color": "#FF1493", "lightColor": "rgba(255, 20, 147, 0.1)", "type": "normal" }, { "id": "event-4", "title": "Launch", "content": "Go live", "date": "2024-06", "color": "#2E8B57", "lightColor": "rgba(46, 139, 87, 0.1)", "type": "normal" } ] ``` ### 展开/折叠树 - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/tree/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/tree/ - Source path: examples/showcase/practices/demo/tree.ts ```ts import exampleData from '../../../data/mindmap.json' import { Graph, Node, Edge, Shape } from '@antv/x6' // 定义节点 class TreeNode extends Node { private collapsed: boolean = false protected postprocess() { this.toggleCollapse(false) } isCollapsed() { return this.collapsed } toggleButtonVisibility(visible: boolean) { this.attr('buttonGroup', { display: visible ? 'block' : 'none', }) } toggleCollapse(collapsed?: boolean) { const target = collapsed == null ? !this.collapsed : collapsed if (!target) { this.attr('buttonSign', { d: 'M 1 5 9 5 M 5 1 5 9', strokeWidth: 1.6, }) } else { this.attr('buttonSign', { d: 'M 2 5 8 5', strokeWidth: 1.8, }) } this.collapsed = target } } TreeNode.config({ zIndex: 2, markup: [ { tagName: 'g', selector: 'buttonGroup', children: [ { tagName: 'rect', selector: 'button', attrs: { 'pointer-events': 'visiblePainted', }, }, { tagName: 'path', selector: 'buttonSign', attrs: { fill: 'none', 'pointer-events': 'none', }, }, ], }, { tagName: 'rect', selector: 'body', }, { tagName: 'text', selector: 'label', }, ], attrs: { body: { refWidth: '100%', refHeight: '100%', strokeWidth: 1, fill: '#EFF4FF', stroke: '#5F95FF', }, label: { textWrap: { ellipsis: true, width: -10, }, textAnchor: 'middle', textVerticalAnchor: 'middle', refX: '50%', refY: '50%', fontSize: 12, }, buttonGroup: { refX: '100%', refY: '50%', }, button: { fill: '#5F95FF', stroke: 'none', x: -10, y: -10, height: 20, width: 30, rx: 10, ry: 10, cursor: 'pointer', event: 'node:collapse', }, buttonSign: { refX: 5, refY: -5, stroke: '#FFFFFF', strokeWidth: 1.6, }, }, }) // 定义边 class TreeEdge extends Shape.Edge { isHidden() { const node = this.getTargetNode() as TreeNode return !node || !node.isVisible() } } TreeEdge.config({ zIndex: 1, attrs: { line: { stroke: '#A2B1C3', strokeWidth: 1, targetMarker: null, }, }, }) // 注册 Node.registry.register('tree-node', TreeNode, true) Edge.registry.register('tree-edge', TreeEdge, true) // 初始化画布 const graph = new Graph({ container: document.getElementById('container')!, panning: true, interacting: false, connecting: { anchor: 'orth', connector: 'rounded', connectionPoint: 'boundary', router: { name: 'er', args: { offset: 24, direction: 'H', }, }, }, }) graph.on('node:collapse', ({ node }: { node: TreeNode }) => { node.toggleCollapse() const collapsed = node.isCollapsed() const run = (pre: TreeNode) => { const succ = graph.getSuccessors(pre, { distance: 1 }) if (succ) { succ.forEach((node: TreeNode) => { node.toggleVisible(!collapsed) if (!node.isCollapsed()) { run(node) } }) } } run(node) }) Promise.resolve(exampleData) .then((data) => { const nodes = data.nodes.map(({ leaf, ...metadata }: any) => { const node = new TreeNode(metadata) if (leaf) { node.toggleButtonVisibility(leaf === false) } return node }) const edges = data.edges.map( (edge: any) => new TreeEdge({ source: edge.source, target: edge.target, }), ) graph.resetCells([...nodes, ...edges]) graph.zoomToFit({ padding: 24 }) }) ``` ### examples/data/mindmap.json ```json { "nodes": [ { "id": 69, "shape": "tree-node", "width": 70, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "fathers" } } }, "x": 35, "y": 97 }, { "id": 70, "shape": "tree-node", "width": 28, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "0" } } }, "x": 184, "y": 97 }, { "id": 71, "shape": "tree-node", "width": 70, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "id" } } }, "x": 333, "y": 13 }, { "id": 72, "shape": "tree-node", "width": 28, "height": 26, "leaf": true, "attrs": { "label": { "textWrap": { "text": 0 } } }, "x": 503, "y": 13 }, { "id": 73, "shape": "tree-node", "width": 70, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "married" } } }, "x": 333, "y": 69 }, { "id": 74, "shape": "tree-node", "width": 70, "height": 26, "leaf": true, "attrs": { "label": { "textWrap": { "text": false } } }, "x": 503, "y": 69 }, { "id": 75, "shape": "tree-node", "width": 70, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "name" } } }, "x": 333, "y": 125 }, { "id": 76, "shape": "tree-node", "width": 70, "height": 26, "leaf": true, "attrs": { "label": { "textWrap": { "text": "Eric Taylor" } } }, "x": 503, "y": 125 }, { "id": 77, "shape": "tree-node", "width": 70, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "daughters" } } }, "x": 333, "y": 405 }, { "id": 78, "shape": "tree-node", "width": 28, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "0" } } }, "x": 503, "y": 181 }, { "id": 79, "shape": "tree-node", "width": 70, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "age" } } }, "x": 673, "y": 153 }, { "id": 80, "shape": "tree-node", "width": 28, "height": 26, "leaf": true, "attrs": { "label": { "textWrap": { "text": 30 } } }, "x": 843, "y": 153 }, { "id": 81, "shape": "tree-node", "width": 70, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "name" } } }, "x": 673, "y": 209 }, { "id": 82, "shape": "tree-node", "width": 70, "height": 26, "leaf": true, "attrs": { "label": { "textWrap": { "text": "Sarah" } } }, "x": 843, "y": 209 }, { "id": 83, "shape": "tree-node", "width": 28, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "1" } } }, "x": 503, "y": 293 }, { "id": 84, "shape": "tree-node", "width": 70, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "age" } } }, "x": 673, "y": 265 }, { "id": 85, "shape": "tree-node", "width": 28, "height": 26, "leaf": true, "attrs": { "label": { "textWrap": { "text": 6 } } }, "x": 843, "y": 265 }, { "id": 86, "shape": "tree-node", "width": 70, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "name" } } }, "x": 673, "y": 321 }, { "id": 87, "shape": "tree-node", "width": 70, "height": 26, "leaf": true, "attrs": { "label": { "textWrap": { "text": "Cynthia" } } }, "x": 843, "y": 321 }, { "id": 88, "shape": "tree-node", "width": 28, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "2" } } }, "x": 503, "y": 405 }, { "id": 89, "shape": "tree-node", "width": 70, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "age" } } }, "x": 673, "y": 377 }, { "id": 90, "shape": "tree-node", "width": 28, "height": 26, "leaf": true, "attrs": { "label": { "textWrap": { "text": 15 } } }, "x": 843, "y": 377 }, { "id": 91, "shape": "tree-node", "width": 70, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "name" } } }, "x": 673, "y": 433 }, { "id": 92, "shape": "tree-node", "width": 70, "height": 26, "leaf": true, "attrs": { "label": { "textWrap": { "text": "Linda" } } }, "x": 843, "y": 433 }, { "id": 93, "shape": "tree-node", "width": 28, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "3" } } }, "x": 503, "y": 517 }, { "id": 94, "shape": "tree-node", "width": 70, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "age" } } }, "x": 673, "y": 489 }, { "id": 95, "shape": "tree-node", "width": 28, "height": 26, "leaf": true, "attrs": { "label": { "textWrap": { "text": 7 } } }, "x": 843, "y": 489 }, { "id": 96, "shape": "tree-node", "width": 70, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "name" } } }, "x": 673, "y": 545 }, { "id": 97, "shape": "tree-node", "width": 70, "height": 26, "leaf": true, "attrs": { "label": { "textWrap": { "text": "Barbara" } } }, "x": 843, "y": 545 }, { "id": 98, "shape": "tree-node", "width": 28, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "4" } } }, "x": 503, "y": 629 }, { "id": 99, "shape": "tree-node", "width": 70, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "age" } } }, "x": 673, "y": 601 }, { "id": 100, "shape": "tree-node", "width": 28, "height": 26, "leaf": true, "attrs": { "label": { "textWrap": { "text": 18 } } }, "x": 843, "y": 601 }, { "id": 101, "shape": "tree-node", "width": 70, "height": 26, "leaf": false, "attrs": { "label": { "textWrap": { "text": "name" } } }, "x": 673, "y": 657 }, { "id": 102, "shape": "tree-node", "width": 70, "height": 26, "leaf": true, "attrs": { "label": { "textWrap": { "text": "Margaret" } } }, "x": 843, "y": 657 } ], "edges": [ { "source": 69, "target": 70, "shape": "tree-edge" }, { "source": 70, "target": 71, "shape": "tree-edge" }, { "source": 71, "target": 72, "shape": "tree-edge" }, { "source": 70, "target": 73, "shape": "tree-edge" }, { "source": 73, "target": 74, "shape": "tree-edge" }, { "source": 70, "target": 75, "shape": "tree-edge" }, { "source": 75, "target": 76, "shape": "tree-edge" }, { "source": 70, "target": 77, "shape": "tree-edge" }, { "source": 77, "target": 78, "shape": "tree-edge" }, { "source": 78, "target": 79, "shape": "tree-edge" }, { "source": 79, "target": 80, "shape": "tree-edge" }, { "source": 78, "target": 81, "shape": "tree-edge" }, { "source": 81, "target": 82, "shape": "tree-edge" }, { "source": 77, "target": 83, "shape": "tree-edge" }, { "source": 83, "target": 84, "shape": "tree-edge" }, { "source": 84, "target": 85, "shape": "tree-edge" }, { "source": 83, "target": 86, "shape": "tree-edge" }, { "source": 86, "target": 87, "shape": "tree-edge" }, { "source": 77, "target": 88, "shape": "tree-edge" }, { "source": 88, "target": 89, "shape": "tree-edge" }, { "source": 89, "target": 90, "shape": "tree-edge" }, { "source": 88, "target": 91, "shape": "tree-edge" }, { "source": 91, "target": 92, "shape": "tree-edge" }, { "source": 77, "target": 93, "shape": "tree-edge" }, { "source": 93, "target": 94, "shape": "tree-edge" }, { "source": 94, "target": 95, "shape": "tree-edge" }, { "source": 93, "target": 96, "shape": "tree-edge" }, { "source": 96, "target": 97, "shape": "tree-edge" }, { "source": 77, "target": 98, "shape": "tree-edge" }, { "source": 98, "target": 99, "shape": "tree-edge" }, { "source": 99, "target": 100, "shape": "tree-edge" }, { "source": 98, "target": 101, "shape": "tree-edge" }, { "source": 101, "target": 102, "shape": "tree-edge" } ] } ``` ### 多节点虚拟渲染 - zh page: https://x6.antv.antgroup.com/zh/examples/showcase/practices/virtualrender/ - en page: https://x6.antv.antgroup.com/en/examples/showcase/practices/virtualrender/ - Source path: examples/showcase/practices/demo/virtualRender.ts ```ts import { Graph, type Node, Scroller } from '@antv/x6' import insertCss from 'insert-css' const host = document.getElementById('container')! host.classList.add('virtual-render') const graph = new Graph({ container: host, mousewheel: true, // 开启虚拟渲染 virtual: true, grid: true, }) graph.use(new Scroller({ enabled: true, pannable: true })) const nodes: Node[] = [] const nodeWidth = 80 const nodeHeight = 30 const cols = 20 const spacingX = 120 const spacingY = 50 const count = 1000 const statuses = ['加载中…', '同步中…', '处理任务…', '拉取数据…', '发布中…'] const loaderVariants = [ 'loader-dots', 'loader-bar', 'loader-wave', 'loader-glow', 'loader-ants', ] for (let i = 0; i < count; i++) { const row = Math.floor(i / cols) const col = i % cols const x = 50 + col * spacingX const y = 50 + row * spacingY const node = graph.addNode({ x, y, width: nodeWidth, height: nodeHeight, position: { x, y }, attrs: { label: { text: statuses[i % statuses.length], fill: '#0958D9', fontSize: 12, }, body: { fill: i % 2 === 0 ? '#E6F4FF' : '#FFFFFF', stroke: '#5f95ff', strokeWidth: 1, rx: 4, ry: 4, class: loaderVariants[i % loaderVariants.length], }, }, }) nodes.push(node) } for (let i = 0; i < count; i++) { const col = i % cols if (col < cols - 1 && i + 1 < nodes.length) { graph.addEdge({ source: nodes[i], target: nodes[i + 1], attrs: { line: { stroke: '#91CAFF', strokeWidth: 1, strokeDasharray: '6 4', class: 'marching-line', }, }, }) } const bottomIndex = i + cols if (bottomIndex < nodes.length) { graph.addEdge({ source: nodes[i], target: nodes[bottomIndex], attrs: { line: { stroke: '#91CAFF', strokeWidth: 1, strokeDasharray: '6 4', class: 'marching-line', }, }, }) } } insertCss(` @keyframes ants { to { stroke-dashoffset: -100; } } @keyframes dotsFlow { to { stroke-dashoffset: -24; } } @keyframes progressSweep { to { stroke-dashoffset: -220; } } @keyframes waveWidth { 0%, 100% { stroke-width: 1; } 50% { stroke-width: 2.2; } } @keyframes glowFade { 0%, 100% { stroke-opacity: 0.8; } 50% { stroke-opacity: 0.45; } } .virtual-render .x6-edge path.marching-line { animation: ants 6s linear infinite; } .virtual-render .x6-edge:hover path.marching-line { stroke: #5f95ff; stroke-width: 1.3px; } .virtual-render .x6-edge-selected path.marching-line { stroke: #5f95ff; stroke-width: 1.8px !important; } .virtual-render .x6-node rect.loader-dots { transform-box: fill-box; transform-origin: center; stroke-dasharray: 2 6; animation: dotsFlow 2.4s linear infinite; } .virtual-render .x6-node rect.loader-bar { transform-box: fill-box; transform-origin: center; stroke-dasharray: 80 140; animation: progressSweep 4.8s linear infinite; } .virtual-render .x6-node rect.loader-wave { transform-box: fill-box; transform-origin: center; animation: waveWidth 3s ease-in-out infinite; } .virtual-render .x6-node rect.loader-glow { transform-box: fill-box; transform-origin: center; animation: glowFade 2.8s ease-in-out infinite; } .virtual-render .x6-node rect.loader-ants { transform-box: fill-box; transform-origin: center; stroke-dasharray: 6 4; animation: ants 6s linear infinite; } .virtual-render .x6-node-selected rect { stroke: #5f95ff; stroke-width: 2px; } `) ```