x6实践

# 入门

# 引入

对于生产环境,我们推荐使用一个明确的版本号,以避免新版本造成的不可预期的破坏:

  • https://unpkg.com/@antv/x6@1.1.1/dist/x6.js
  • https://cdn.jsdelivr.net/npm/@antv/x6@1.1.1/dist/x6.js
  • https://cdnjs.cloudflare.com/ajax/libs/antv-x6/1.1.1/x6.js
<script src="https://unpkg.com/@antv/x6@1.1.1/dist/x6.js"></script>
1

# helloword

  • 创建容器
  • 准备数据
  • 渲染画布
import React from "react";
import { Graph } from "@antv/x6";
import "./app.css";
const data = {
  nodes: [
    {
      id: "node1",
      x: 40,
      y: 40,
      width: 80,
      height: 40,
      label: "Hello",
      attrs: {
        body: {
          fill: "#2ECC71",
          stroke: "#000",
          strokeDasharray: "10,2"
        },
        label: {
          text: "Hello",
          fill: "#333",
          fontSize: 13
        }
      }
    },
    {
      id: "node2",
      shape: "ellipse", // 使用 ellipse 渲染
      x: 160,
      y: 180,
      width: 80,
      height: 40,
      label: "World",
      attrs: {
        body: {
          fill: "#F39C12",
          stroke: "#000",
          rx: 16,
          ry: 16
        },
        label: {
          text: "World",
          fill: "#333",
          fontSize: 18,
          fontWeight: "bold",
          fontVariant: "small-caps"
        }
      }
    }
  ],
  edges: [
    {
      source: "node1",
      target: "node2",
      shape: 'double-edge',
      attrs: {
        line: {
          stroke: 'orange',
        },
      },
    }
  ]
};

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

  componentDidMount() {
    const graph = new Graph({
      container: this.container,
      background: {
        color: "#fffbe6" // 设置画布背景颜色
      },
      grid: {
        size: 10, // 网格大小 10px
        visible: true // 渲染网格背景
      }
    });

    graph.fromJSON(data);
    // graph.zoom(-0.5) // 缩小 50%
    graph.translate(80, 40);
  }

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

  render() {
    return (
      <div className="app">
        <div className="app-content" ref={this.refContainer} />
      </div>
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

# 实践x6模型开发

# 初始化项目

mkdir x6-model-demo
npx @umijs/create-umi-app
1
2

# 安装相关依赖

npm i -S antd @antv/x6 @antv/x6-react-components @antv/x6-react-shape classnames
1

# package.json调整

  "name": "x6-model-demo",
  "version": "0.0.1",
  
    "dependencies": {
    "@ant-design/pro-layout": "^6.5.0",
     "@antv/g6": "^3.8.2",
    "@antv/x6": "^1.12.10",
    "@antv/x6-react-shape": "^1.2.3",
    "antd": "^4.9.4",
    "classnames": "^2.2.6",
    "react": "^16.12.0",
    "react-dom": "^16.12.0"
  },
  "devDependencies": {
    "@types/react": "^16.9.0",
    "@types/react-dom": "^16.9.0",
    "@umijs/test": "^3.3.3",
    "@umijs/preset-react": "1.x",
    "lint-staged": "^10.0.7",
    "prettier": "^2.2.0",
    "umi": "^3.3.3",
    "typescript": "^4.1.2",
    "yorkie": "^2.0.0"
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 本地验证

发布之前,可以通过 serve 做本地验证,

$ yarn global add serve
$ serve ./dist
1
2

# 迁移代码

迁移官方x6-app-er (opens new window), 结合本地demo调整;

# Getting Started

Install dependencies,

$ yarn
1

Start the dev server,

$ yarn start
1

# 功能点

# ts封装抽象

# 集成时样式

styles引入

# 数据同步问题

节点拖动数据跟外部的node,edge的数据保持一致;

  private registerEvent = () => {
    this.graph.on('node:dblclick', ({ e, x, y, node, view }) => {
      this.currentCell = node;
      this.handleMenuPro();
    });
    this.graph.on('node:added', ({ node, index, options }) => {
      // 添加时,这里面的节点拿不到xy信息;现在通过选中contextmenu时,补充x,y数据;
      this.baseGraph.cellController.nodes.push(node);
      if (this.props.nodeCb) this.props.nodeCb('added', this.assemblData(node));
    });
    this.graph.on('node:removed', ({ node, index, options }) => {
      // 删除时,这里面的节点可拿到xy信息;已经没有意义了;
      this.baseGraph.cellController.removeNode(node);
      if (this.props.nodeCb) this.props.nodeCb('removed', node);
    });

    this.graph.on('edge:added', ({ edge, index, options }) => {
      // this.baseGraph.cellController.addEdge()
      this.baseGraph.cellController.edges.push(edge);
      if (this.props.edgeCb) this.props.edgeCb('added', this.assemblData(edge));
    });
    this.graph.on('edge:removed', ({ edge, index, options }) => {
      this.baseGraph.cellController.removeEdge(edge);
      if (this.props.edgeCb) this.props.edgeCb('removed', edge);
    });

    this.graph.on('cell:change:*', ({ e }) => {
      const str = this.graph.toJSON({ diff: true });
      console.log(JSON.stringify(str, null, 2));
    });
  };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 自定义组件

import '@antv/x6-react-shape';
1

示范

https://codesandbox.io/s/j9qyg?file=/src/app.tsx

# 高度问题

组件内设置了高度,在里面只设置100%,就可以;

也可以在里面固定高度;

# 定义节点及边

# 滚动缩放平移

普通画布(未开启 scroller (opens new window) 模式)通过开启 panning 选项来支持拖拽平移。

const graph = new Graph({
  panning: true,
})

// 等同于
const graph = new Graph({
  panning: {
    enabled: true,
  },
})
1
2
3
4
5
6
7
8
9
10

拖拽可能和其他操作冲突,此时可以设置 modifiers 参数,设置修饰键后需要按下修饰键并点击鼠标才能触发画布拖拽。

const graph = new Graph({
  panning: {
    enabled: true,
    modifiers: 'shift',
  },
})
1
2
3
4
5
6

普通画布(未开启 scroller (opens new window) 模式)通过开启 mousewheel (opens new window) 选项来支持画布缩放。这里说明怎么通过代码来进行画布缩放:

graph.zoom() // 获取缩放级别
graph.zoom(0.2) // 在原来缩放级别上增加 0.2
graph.zoom(-0.2) // 在原来缩放级别上减少 0.2
1
2
3

常用的就是将画布内容中心与视口中心对齐,使用方式:

graph.centerContent()
1

更多的居中方法可以查看 Transform (opens new window)

# 右击菜单

# 实现方式一 (opens new window)[推荐]

通过事件冒泡处理;height设置高后,也可以滚动处理;

this.baseGraph.graph.on('cell:contextmenu', ({ e }) => {
  const { x, y } = this.baseGraph.graph.clientToLocal(e.clientX, e.clientY);
  this.contextMenuAnchor.style.display = 'inline-block';
  this.contextMenuAnchor.style.left = `${x}px`;
  this.contextMenuAnchor.style.top = `${y}px`;
  const clickEvent = document.createEvent('HTMLEvents');
  clickEvent.initEvent('click', true, true);
  this.contextMenuAnchor.dispatchEvent(clickEvent);
});
this.baseGraph.graph.on('node:mouseenter', ({ e }) => {
  const ports = this.graphContainer.querySelectorAll(
    '.x6-port-body',
  ) as NodeListOf<SVGAElement>;
  this.showPorts(ports, true);
});
this.baseGraph.graph.on('node:mouseleave', ({ e }) => {
  const ports = this.graphContainer.querySelectorAll(
    '.x6-port-body',
  ) as NodeListOf<SVGAElement>;
  this.showPorts(ports, false);
});
this.baseGraph.graph.on('cell:change:*', ({ e }) => {
  const str = JSON.stringify(
    this.baseGraph.graph.toJSON({ diff: true }),
    null,
    2,
  );
  console.log(str);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

做相应的优化

居中处理及加入滚轮;只做mousewheel不做scroller处理;

this.baseGraph = new BaseGraph({
  container: this.graphContainer,
  grid: true,
  // scroller: {
  //   enabled: true,
  //   pannable: true,
  // },
  mousewheel: {
    enabled: true,
    zoomAtMousePosition: false,
    factor: 1.1,
  },
  ...this.getContainerSize(),
});

this.graph.batchUpdate('updateGraph', () => {
  if (addNodesData && addNodesData.length > 0) {
    this.cellController.addNodes(addNodesData);
  }
  if (addEdgesData && addEdgesData.length > 0) {
    this.cellController.addEdges(addEdgesData);
  }
  this.graph.centerContent();
});

//加入滚轮监听
const p1 = this.graph.pageToLocal(e.clientX, e.clientY);
const p2 = this.graph.localToGraph(p1);
const left = `${p2.x}px`;
const top = `${p2.y}px`;
this.contextMenuAnchor.style.left = left;
this.contextMenuAnchor.style.top = top;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
this.currentCell = cell;
// this.currentCell.x = x;//现在是居中处理,不用再重新复制
// this.currentCell.y = y;
//做了居中处理后,不用在存x,y啦
private assemblData(data) {
  const newData = data.getData() || {};
  newData.id = data.id || 0;
  // newData.x = data.x || 100;
  // newData.y = data.y || 100; //x,y 只有对节点有用;
  console.log('---assemblData------newData----', newData);
  return newData;
}
1
2
3
4
5
6
7
8
9
10
11
12
# 实现方式二 (opens new window)

通过registerEdgeTool实现;,可以i处理scroller: true的问题;

# 最后实现

通过上面的方式一,再修改相关设置;

设置可以滚动及鼠标放大缩小;

this.baseGraph = new BaseGraph({
  container: this.graphContainer,
  grid: true,
  mousewheel: {
    enabled: true,
    zoomAtMousePosition: false,
    factor: 1.1,
  },
  scroller: {
    enabled: true,
    pageVisible: false,
    pageBreak: false,
    pannable: true,
  },
  ...this.getContainerSize(),
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

修改监听后的偏移;

this.graph.on('cell:contextmenu', ({ e, cell, view, x, y }) => {
  this.currentCell = cell;
  // const p1 = this.graph.pageToLocal(e.clientX, e.clientY);
  // const p2 = this.graph.localToGraph(p1);
  // const p1 = {x,y}; // 不滚动时是可以的;
  const p2 = { x: e.clientX, y: e.clientY }; // 滚动时的处理
  const left = `${p2.x - 320}px`; //去除左边的间距;
  const top = `${p2.y - 64 - 150}px`;// 移除上面及下面的间距;
  this.contextMenuAnchor.style.display = 'inline-block';
  this.contextMenuAnchor.style.left = left;
  this.contextMenuAnchor.style.top = top;
  const clickEvent = document.createEvent('HTMLEvents');
  clickEvent.initEvent('click', true, true);
  this.contextMenuAnchor.dispatchEvent(clickEvent);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 最终效果

image-20210122155015612

# 相关链接

https://antv-x6.gitee.io/zh/docs/tutorial/getting-started

上次更新: 2022/04/15, 05:41:30
×