拓扑图插件

https://visjs.org/

antv g6

https://jsplumbtoolkit.com/

# Demo

先来看一段代码

import G6 from '@antv/g6';

const nodes = [];
const edges = [];

// Center node
const centerNode = {
  id: 'center',
  x: 500,
  y: 300,
  type: 'center-node',
  size: 20,
};
nodes.push(centerNode);
// Add 4 nodes on the left
for (let i = 0; i < 4; i++) {
  const id = 'primary' + i;
  nodes.push({
    id,
    // x: 250,
    // y: (i + 1) * 100 + 50,
    // x: Math.random() * 200, // 随机 x 坐标,适当调整范围
    // y: Math.random() * 500, // 随机 y 坐标
    type: 'leaf-node',
    text: '已定义的MDM主机',
    label: '10.0.0.213',
    style: { fill: '#1677FF', stroke: '#fff' }
  });
  edges.push({ source: id, target: 'center', type: 'can-running' });
}
// Add 6 nodes on the right
for (let i = 0; i < 6; i++) {
  const id = 'right' + i;
  nodes.push({
    id,
    // x: 750,
    // y: i * 100 + 50,
    type: 'leaf-node',
    text: '有挂载数据的MDM主机',
    label: '10.0.0.213',
    style: { fill: 'orange', stroke: '#fff' }
  });
  edges.push({ source: 'center', target: id, type: 'can-running' });
}

for (let i = 0; i < 3; i++) {
  const id = 'success' + i;
  nodes.push({
    id,
    // x: 750,
    // y: i * 100 + 50,
    type: 'leaf-node',
    text: '已验证的主机',
    label: '10.0.0.213',
    style: { fill: '#52c41a', stroke: '#fff' },
  });
  edges.push({ source: 'center', target: id, type: 'can-running' });
}
for (let i = 0; i < 2; i++) {
  const id = 'fail' + i;
  nodes.push({
    id,
    // x: 750,
    // y: i * 100 + 50,
    type: 'leaf-node',
    text: '无法连接的主机',
    label: '10.0.0.213',
    style: { fill: '#ff4d4f', stroke: '#fff' }
  });
  edges.push({ source: 'center', target: id, type: 'can-running' });
}


// 注册叶子节点
G6.registerNode(
  'leaf-node',
  {
    afterDraw(cfg, group) {
      // group.addShape('circle', {
      //   attrs: {
      //     x: 0,
      //     y: 0,
      //     r: 5,
      //     fill: cfg.color || '#5B8FF9',
      //   },
      //   // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
      //   name: 'circle-shape',
      // });
      group.addShape('text', {
        attrs: {
          x: 0,
          y: 25,
          textAlign: 'center',
          textBaseline: 'middle',
          text: cfg.text || '默认文本', // 使用 cfg.label 或默认文本
          fill: '#000', // 文本颜色
        },
        name: 'text-shape',
      })
    },
    getAnchorPoints() {
      return [
        [0, 0.5],
        [1, 0.5],
      ];
    },
  },
  'rect',
);
// 注册中心节点
G6.registerNode(
  'center-node',
  {
    afterDraw(cfg, group) {
      const r = cfg.size / 2;
      group.addShape('circle', {
        zIndex: -3,
        attrs: {
          x: 0,
          y: 0,
          r: r + 10,
          fill: 'gray',
          opacity: 0.4,
        },
        // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
        name: 'circle-shape1',
      });
      group.addShape('circle', {
        zIndex: -2,
        attrs: {
          x: 0,
          y: 0,
          r: r + 20,
          fill: 'gray',
          opacity: 0.2,
        },
        // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
        name: 'circle-shape2',
      });
      group.sort();
    },
    getAnchorPoints() {
      return [
        [0, 0.5],
        [1, 0.5],
      ];
    },
  },
  'circle',
);

// lineDash array
const lineDash = [4, 2, 1, 2];

G6.registerEdge(
  'can-running',
  {
    setState(name, value, item) {
      const shape = item.get('keyShape');
      if (name === 'running') {
        if (value) {
          let index = 0;
          shape.animate(
            () => {
              index++;
              if (index > 9) {
                index = 0;
              }
              const res = {
                lineDash,
                lineDashOffset: -index,
              };
              // return the params for this frame
              return res;
            },
            {
              repeat: true,
              duration: 3000,
            },
          );
        } else {
          shape.stopAnimate();
          shape.attr('lineDash', null);
        }
      }
    },
  },
  'cubic-horizontal',
);

// tooltip
const tooltip = new G6.Tooltip({
  offsetX: 10,
  offsetY: 20,
  getContent(e) {
    const outDiv = document.createElement('div');
    outDiv.style.width = '180px';
    outDiv.innerHTML = `
      <ul>
        <li>ip地址: ${e.item.getModel().label || e.item.getModel().id}</li>
        <li>端口: 22</li>
      </ul>`
    return outDiv
  },
  itemTypes: ['node']
});

const container = document.getElementById('container');
const width = container.scrollWidth;
const height = container.scrollHeight || 500;
const graph = new G6.Graph({
  container: 'container',
  width,
  height,
  layout: {
    type: 'radial', // 使用力导向布局
    preventOverlap: true, // 防止节点重叠
    linkDistance: 300, // 节点之间的距离
    nodeStrength: -100, // 节点之间的斥力
    edgeStrength: 0.7, // 边的弹簧系数
  },
  defaultNode: {
    style: {
      fill: '#DEE9FF',
      stroke: '#5B8FF9',
    },
  },
  defaultEdge: {
    style: {
      stroke: '#b5b5b5',
    },
  },
  modes: {
    default: ['drag-canvas', 'zoom-canvas'],
    edit: ['click-select'],
  },
  plugins: [tooltip],
});
graph.data({ nodes, edges });
graph.render();
graph.fitView();

// set hover state
graph.on('node:mouseenter', (ev) => {
  const node = ev.item; // 获取触发事件的节点
  // const edges = node.getEdges();
  // edges.forEach((edge) => graph.setItemState(edge, 'running', true));
  const model = node.getModel(); // 获取节点的数据模型
  const label = model.label || '未命名'; // 获取节点的标签,如果没有标签则使用默认值
  // 显示弹框
  tooltip.showTooltip({
    x: ev.clientX,
    y: ev.clientY,
    content: label, // 弹框内容,可以是 HTML 字符串
  });
});
graph.on('node:mouseleave', (ev) => {
  const node = ev.item;
  const edges = node.getEdges();
  edges.forEach((edge) => graph.setItemState(edge, 'running', false));
});

if (typeof window !== 'undefined')
  window.onresize = () => {
    if (!graph || graph.get('destroyed')) return;
    if (!container || !container.scrollWidth || !container.scrollHeight) return;
    graph.changeSize(container.scrollWidth, container.scrollHeight);
  };
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268

# Graph

Graph 是 G6 图表的载体,所有的 G6 节点实例操作以及事件,行为监听都在 Graph 实例上进行。Graph 的初始化通过 new 进行实例化,实例化时需要传入需要的参数。

const graph = new G6.Graph({
  container: 'container',
  width,
  height,
  // 设置画布的交互模式,存在 default 和 edit 两种 mode(交互模式)
  modes: {
    default: ['drag-canvas'],
  },
  // GraphOptions.layout.type指定图的布局算法
  
  layout: {
    type: 'radial',
    unitRadius: 50,
    center: [500, 300],
  },
  // 默认节点
  defaultNode: {
    style: {
      fill: '#DEE9FF',
      stroke: '#5B8FF9',
    },
  },
  defaultEdge: {
    style: {
      stroke: '#b5b5b5',
    },
  },
});
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

# GraphOptions.layout

常见的布局类型包括:

  • 'force':力导向布局。
  • 'circular':环形布局。
  • 'grid':网格布局。
  • 'radial':径向布局。
  • 'dagre':使用 Dagre 库的有向无环图 (DAG) 布局。
graph.data({ nodes, edges });
graph.render();
graph.fitView();
1
2
3

graph.data({ nodes, edges });:

  • 这一行代码用于将图的数据传递给 G6 图实例。其中 nodes 和 edges 是包含节点和边信息的数组对象。这一步是为了将数据与图实例关联起来,以便 G6 知道要渲染的内容。

graph.render();:

  • 这一行代码用于渲染图。一旦数据传递给图实例,通过调用 render 方法,G6 将根据提供的数据在图容器中绘制相应的节点和边。

graph.fitView();:

  • 这一行代码用于调整视图,使整个图适应容器的大小。它会根据当前图的布局和节点位置,自动调整缩放和平移,以确保整个图形在可视区域内可见。

# 布局

# force布局-力导向布局

# radial布局-辐射布局

# grid布局

{
  preventOverlap: false,// 是否防止重叠,必须配合属性 nodeSize ,只有设置了与当前图节点大小相同的 nodeSize 值,才能够进行节点重叠的碰撞检测。若未设置 nodeSize ,则将根据节点数据中的 size 进行碰撞检测。若二者都未设置,则默认以 30 为节点大小进行碰撞检测
  
}
1
2
3
4

# Node节点

节点的通用属性 (opens new window)

type是节点的类型,形状,默认circle圆形

# 设置默认节点为图片

可以在初始化graph时设置defaultNode

defaultNode: {
    style: {
      fill: '#DEE9FF',
      stroke: '#5B8FF9',
    },
    width: 20,
    height: 20,
    size: 30,
    // label: '123rr',
    img: 'https://gw.alipayobjects.com/zos/rmsportal/XuVpGqBFxXplzvLjJBZB.svg',
    type: 'image',
  },
1
2
3
4
5
6
7
8
9
10
11
12

效果图如下:

image-20231129141254295

# node.style

设置节点的样式

nodes.push({
    id,
    x: 750,
    y: i * 100 + 50,
    type: 'leaf-node',
    style: { 
      fill: '#5B8FF9', // 填充色
      stroke: '#CED4D9' // 描边颜色
    }
});
1
2
3
4
5
6
7
8
9
10

# Antv G6

# G6.registerNode

完整的生命周期:https://antv-g6.gitee.io/zh/docs/api/registerItem/#%E7%94%A8%E6%B3%95

G6.registerNode(nodeType, config, extendShape);

  • nodeType (字符串): 表示节点类型的名称。

  • config (对象): 包含节点的配置信息,例如节点的形状、样式、大小等。

  • extendShape (字符串): 可选参数,表示节点的形状是否继承自已注册的其他节点类型。

    可选值有circle、rect、triangle

G6.registerNode用于注册自定义节点类型,过这个方法,你可以定义新的节点样式和行为,并在图中使用这些自定义节点类型。

G6.registerNode(
  'leaf-node',  // 自定义节点类型的名称
  {
    afterDraw(cfg, group) {
      // 在节点绘制后执行的操作
      group.addShape('circle', {
        attrs: {
          x: 0,
          y: 0,
          r: 5,
          fill: cfg.color || '#5B8FF9',
        },
        name: 'circle-shape',
      });
    },
    getAnchorPoints() {
      return [
        [0, 0.5],
        [1, 0.5],
      ];
    },
  },
  'rect'  // 扩展自 'rect' 类型的节点
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 增加图片

draw(cfg, group) {
                        // 自定义图形的绘制逻辑
                        return group.addShape("image", {
                            attrs: {
                                x: 0,
                                y: 0,
                                width: 35,
                                height: 35,
                                img: "/static/asstes/computer.svg", // 设置自定义图片的 URL
                            },
                        });
                    },
1
2
3
4
5
6
7
8
9
10
11
12

# afterDraw

afterDraw(cfg, group)

afterDraw方法是自定义节点绘制后执行的回调函数,该函数有两个参数:cfg 和 group。

  • cfg 是一个包含节点数据的对象。这个对象包含了节点的各种属性,例如节点的位置 (x, y)、颜色、大小、标签等。在 afterDraw 函数中,你可以通过 cfg 来获取节点的配置信息,以便根据这些信息进行绘制或执行其他操作。

    image-20231225161056401

  • group 是一个 G6 的图形分组(group)对象。这个分组对象是用于包装节点中的图形元素,例如圆形、矩形等。通过 group,你可以向节点中添加新的图形元素,如在上述例子中添加的圆形。

export type ShapeStyle = Partial<{
    x: number;
    y: number;
    r: number;
    radius: number;
    width: number;
    height: number;
    offset: number | number[];
    stroke: string | null;
    strokeOpacity: number;
    fill: string | null;
    fillOpacity: number;
    lineWidth: number;
    lineAppendWidth: number; // 边的命中范围,数值越大,还没靠近节点就会命中
    lineDash: number[];
    path: string | object[];
    points: object[];
    matrix: number[];
    opacity: number;
    size: number | number[];
    endArrow: boolean | ArrowConfig;
    startArrow: boolean | ArrowConfig;
    shadowColor: string;
    shadowBlur: number;
    shadowOffsetX: number;
    shadowOffsetY: number;
    cursor: string;
    position: string;
    fontSize: number;
    keepVisualSize: boolean;
    [key: string]: any;
}>;
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

# 组件销毁及监听窗口缩放事件

if (typeof window !== 'undefined')
  window.onresize = () => {
    if (!graph || graph.get('destroyed')) return;
    if (!container || !container.scrollWidth || !container.scrollHeight) return;
    graph.changeSize(container.scrollWidth, container.scrollHeight);
  };
1
2
3
4
5
6

这段代码是用于监听窗口大小变化事件,并在窗口大小发生变化时,调整图形的大小以适应新的窗口尺寸。

if (typeof window !== 'undefined') 检查是否浏览器环境

然后监听window.onresize事件,在回调函数中,首先检查图实例 graph 是否存在且未被销毁(graph.get('destroyed')),以避免在图已销毁的情况下执行操作。

接着,检查容器元素 container 是否存在且具有有效的滚动宽度和高度。

最后,通过 graph.changeSize 方法,根据容器的当前滚动宽度和高度,调整图的大小以适应新的窗口尺寸。

# tooltip

注册

// tooltip
const tooltip = new G6.Tooltip({
  offsetX: 10,
  offsetY: 20,
  getContent(e) {
    const outDiv = document.createElement('div');
    outDiv.style.width = '180px';
    outDiv.innerHTML = `
      <ul>
        <li>ip地址: ${e.item.getModel().label || e.item.getModel().id}</li>
        <li>端口: 22</li>
      </ul>`
    return outDiv
  },
  itemTypes: ['node']
});
const graph = new G6.Graph({
  plugins: [tooltip],
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

使用

graph.on('node:mouseenter', (ev) => {
  const node = ev.item; // 获取触发事件的节点
  const model = node.getModel(); // 获取节点的数据模型
  const label = model.label || '未命名'; // 获取节点的标签,如果没有标签则使用默认值
  // 显示弹框
  tooltip.showTooltip({
    x: ev.clientX,
    y: ev.clientY,
    content: label, // 弹框内容,可以是 HTML 字符串
  });
});
1
2
3
4
5
6
7
8
9
10
11

# draw和afterDraw有什么区别

在 AntV G6 中,draw 和 afterDraw 是节点生命周期的两个阶段,分别对应节点的创建和绘制过程。

  1. draw 阶段: 在 draw 阶段,你可以定义节点的基本形状和属性,但这时候节点的图形元素还没有真正添加到画布上。这个阶段用于创建节点的图形元素。
  2. afterDraw 阶段: 在 afterDraw 阶段,节点的图形元素已经添加到画布上。在这个阶段,你可以进一步对图形元素进行操作,比如添加文本、动画效果等。这个阶段用于对已创建的图形元素进行额外的定制。
import G6 from '@antv/g6';

G6.registerNode('custom-node', {
  draw(cfg, group) {
    // 在 draw 阶段创建图形元素
    const shape = group.addShape('rect', {
      attrs: {
        x: 0,
        y: 0,
        width: 80,
        height: 40,
        fill: '#5B8FF9',
      },
    });

    // 将创建的图形元素保存在 group 中,以便在 afterDraw 阶段继续操作
    group.set('customShape', shape);

    return shape;
  },
  afterDraw(cfg, group) {
    // 在 afterDraw 阶段对已创建的图形元素进行额外的操作
    const shape = group.get('customShape');

    // 添加文本
    group.addShape('text', {
      attrs: {
        x: 40,
        y: 20,
        textAlign: 'center',
        textBaseline: 'middle',
        text: cfg.label || 'Label',
        fill: '#fff',
      },
    });
  },
});

// 创建图形实例
const graph = new G6.Graph({
  container: 'container',
  width: 800,
  height: 600,
  modes: {
    default: ['drag-node'], // 允许拖拽节点
  },
});

// 添加自定义节点
graph.add('node', {
  id: 'node1',
  x: 100,
  y: 100,
  shape: 'custom-node', // 使用自定义节点类型
  label: 'Custom Node',
});

// 渲染图形
graph.render();
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

# 让所有节点都展示在画布中而不用缩放

GraphOptions.fitView (opens new window)

是否开启画布自适应。开启后图自动适配画布大小。