使用d3.js快速实现拓扑图的绘制

  1. D3的简介
    D3 全名为 Data Drive Document,即通过 Data 操作 Document,而在做数据可视化时,Data 最常 Drive 的 Document 便是 SVG。刚了解到D3时,看到D3官网非常丰富且酷炫的Demo,便觉得 D3 应该有着无限可能的图形开发能力,所以在学习完基础的API和SVG的基础后,就开始着手绘制D3的节点拓扑图了;
  2. 绘制简易的可拖拽节点拓扑图
    2.1 准备工作:
  1. 安装D3:
1
npm install d3 --save

  1. 项目中导入D3:
1
import * as d3 from "d3"
  1. 准备模拟好的节点数据options并导入:
    options对象含两个属性data和edges;data保存节点信息,edges保存节点之间的关系
    data:
    在这里插入图片描述
    edges:
    在这里插入图片描述
    2.2 开始绘制
    1.在html结构中准备好svg画布
1
2
3
4
5
<template>
  <div >
    <svg id="togo" width="1800" height="700" />
  </div>
</template>
  1. 在mounted生命周期中:
    1)先定义好准备使用的常量
1
2
3
4
const fontSize = 10;
const symbolSize = 40;
const padding = 10;
const that = this

2)我们定义一个名为Topo的类,将所有关于拓扑图绘制的方法和属性都写在其中

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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
class Topo {
      constructor(svg, option) {
        this.data = option.data;
        this.edges = option.edges;
        this.svg = d3.select(svg);
      }

      //初始化节点位置
      initPosition () {
        let width = this.svg.attr('width');
        let height = this.svg.attr('height');
        let points = this.getVertices(this.data.length);
        this.data.forEach((item, i) => {
          item.x = points[i].x + width / 4;
          item.y = points[i].y + height / 9;
        })
      }

      //根据节点的个数,生成矩形阵列(即配置节点的摆放位置),返回的points为节点的定位坐标[{x:..,y:...},...]
      getVertices (n) {
        if (typeof n !== 'number') return;
        var i = 0;
        var j = 0;
        var k = 0
        var points = [];
        while (k < n) {
          points.push({
            x: 100 + 300 * i,
            y: 100 + 300 * j,
          });
          if (i < 2) {
            i++;
          } else {
            i = 0
            j++
          }
          k++
        }
        return points;
      }

      // 计算两点的中心点(用于确认摆放在连接线上的文字的位置)
      getCenter (x1, y1, x2, y2) {
        return [(x1 + x2) / 2, (y1 + y2) / 2]
      }

      // 计算两点角度
      getAngle (x1, y1, x2, y2) {
        var x = Math.abs(x1 - x2);
        var y = Math.abs(y1 - y2);
        var z = Math.sqrt(x * x + y * y);
        return Math.round((Math.asin(y / z) / Math.PI * 180));
      }

      // 初始化缩放器
      initZoom () {
        let self = this;
        let zoom = d3.zoom()
          .scaleExtent([0.7, 3])
          .on('zoom', function () {
            self.onZoom(this)
          });
        this.svg.call(zoom)
      }

      // 初始化图标库
      initDefineSymbol () {
        // defs用于预定义一个元素使其能够在SVG图像中重复使用,我们defs标签中的g元素必须在<g>元素上设置一个ID,通过ID来引用它。
        let defs = this.container.append('svg:defs');
        // 向defs中添加箭头图标
        defs
          .selectAll('marker')
          .data(this.edges)
          .enter()
          .append('svg:marker')
          .attr('id', (link, i) => 'marker-' + i)
          .attr('markerUnits', 'userSpaceOnUse')
          .attr('viewBox', '0 -5 10 10')
          .attr('refX', symbolSize / 2 + padding * 1.5)
          .attr('refY', 0)
          .attr('markerWidth', 14)
          .attr('markerHeight', 14)
          .attr('orient', 'auto')
          .attr('stroke-width', 2)
          .append('svg:path')
          .attr('d', 'M2,0 L0,-3 L9,0 L0,3 M2,0 L0,-3')
          .attr('class', 'arrow')

        // 向defs中添加数据库图标
        defs.append('g')
          .attr('id', 'database')
          .attr('transform', 'scale(0.042)').append('path')
          .attr('d', 'M512 800c-247.42 0-448-71.63-448-160v160c0 88.37 200.58 160 448 160s448-71.63 448-160V640c0 88.37-200.58 160-448 160z M512 608c-247.42 0-448-71.63-448-160v160c0 88.37 200.58 160 448 160s448-71.63 448-160V448c0 88.37-200.58 160-448 160z M512 416c-247.42 0-448-71.63-448-160v160c0 88.37 200.58 160 448 160s448-71.63 448-160V256c0 88.37-200.58 160-448 160z M64 224a448 160 0 1 0 896 0 448 160 0 1 0-896 0Z')
          .attr('style', "fill:#297aff")

        // 向defs中添加云图标
        defs.append('g')
          .attr('id', 'cloud')
          .attr('transform', 'scale(0.042)')
          .append('path')
          .attr('d', 'M709.3 285.8C668.3 202.7 583 145.4 484 145.4c-132.6 0-241 102.8-250.4 233-97.5 27.8-168.5 113-168.5 213.8 0 118.9 98.8 216.6 223.4 223.4h418.9c138.7 0 251.3-118.8 251.3-265.3 0-141.2-110.3-256.2-249.4-264.5z')

        // 向defs中添加应用图标
        defs.append('g')
          .attr('id', 'myapp')
          .attr('transform', 'scale(0.042)')
          .append('path')
          .attr('d', 'M544 552.325V800a32 32 0 0 1-32 32 31.375 31.375 0 0 1-32-32V552.325L256 423.037a32 32 0 0 1-11.525-43.512A31.363 31.363 0 0 1 288 368l224 128 222.075-128a31.363 31.363 0 0 1 43.525 11.525 31.988 31.988 0 0 1-11.525 43.513L544 551.038z m0 0,M64 256v512l448 256 448-256V256L512 0z m832 480L512 960 128 736V288L512 64l384 224z m0 0')

        // 向defs中添加地球图标
        let earth = defs.append('g')
          .attr('id', "earth")
          .attr('transform', 'scale(0.042)');
        earth.append("path")
          .attr("d", 'm 973.70505,457.95556 c -9.82626,-86.10909 -43.70101,-167.56364 -97.74545,-235.57172 -12.54142,6.07677 -24.69495,12.15353 -36.7192,17.71313 3.23233,14.48081 6.46465,29.34949 9.30909,45.12323 10.73132,58.69899 18.61819,131.7495 17.71314,218.76364 36.71919,-13.83434 72.53333,-28.70303 107.44242,-46.02828 z M 224.32323,247.59596 c -2.84444,8.40404 -6.07677,17.19596 -8.79192,26.50505 -17.71313,59.08687 -30.25454,134.46465 -26.50505,227.55556 39.04647,15.38585 84.29899,30.25454 134.98182,41.8909 57.66465,13.05859 126.57778,22.75556 204.8,22.36768 V 310.9495 h -8.40404 c -101.49495,0 -202.47273,-21.46263 -296.08081,-63.35354 z M 550.14141,48.355556 V 289.87475 c 80.03233,-3.23233 168.98586,-20.94546 264.40405,-61.41414 -7.88687,-31.67677 -17.71314,-62.3192 -28.83233,-92.57374 C 716.41212,85.20404 635.34546,54.949495 550.14141,48.355556 Z M 52.622222,432.87273 c 21.850505,13.96363 61.414138,37.23636 115.846468,59.99192 -2.84445,-92.18586 10.21414,-167.04647 27.92727,-226.26263 2.84444,-9.82626 6.07677,-19.13535 9.30909,-27.92727 -20.42828,-9.82627 -37.23636,-19.52324 -49.77778,-27.02223 C 102.4,275.13535 66.973738,351.41818 52.622222,432.87273 Z')

        earth.append("path")
          .attr('d', 'm 845.31717,511.09495 c 1.42222,-104.72727 -9.82626,-192.25859 -25.6,-262.46465 -96.8404,40.98586 -187.60404,58.69899 -269.05858,61.93132 v 255.09495 c 100.07272,-2.97374 199.2404,-21.59192 294.65858,-54.56162 z')

        earth.append("path")
          .attr('d', 'M 845.70505,727.53131 C 882.94142,708.00808 918.75556,685.6404 952.7596,660.0404 969.0505,612.07273 976.93737,562.29495 976.93737,512 c 0,-10.73131 -0.51717,-21.46263 -1.42222,-32.06465 -37.23636,18.10101 -73.95555,33.09899 -110.28687,46.02829 -1.8101,67.49091 -8.27474,134.98181 -19.52323,201.56767 z M 169.50303,516.13737 C 128.12929,499.32929 88.048485,478.90101 49.389899,455.62828 41.890909,516.65455 46.545455,578.45657 62.836364,637.67273 107.05455,667.4101 153.08283,694.04444 201.56768,716.8 182.9495,642.84444 172.73535,576.25859 169.50303,516.13737 Z m 360.2101,283.5394 V 586.47273 h -3.23232 c -114.55354,0 -228.5899,-20.94546 -335.64445,-61.93132 4.13738,60.50909 14.86869,128.51718 35.42627,202.9899 35.81414,15.77374 74.9899,30.25455 117.26868,42.40808 60.50909,17.58384 122.82829,27.41011 186.18182,29.73738 z M 208.93737,742.4 C 162.00404,720.93737 116.88081,696.7596 73.050505,668.83232 108.86465,768.38788 177.26061,852.68687 267.11919,908.02424 244.36364,854.10909 224.8404,798.77172 208.93737,742.4 Z m 320.77576,76.8 c -65.16364,-1.8101 -129.8101,-11.63636 -192.25859,-29.73737 -35.42626,-10.21415 -69.81818,-22.36768 -103.30505,-36.33132 15.77374,53.52728 36.7192,111.19192 63.35354,171.70101 65.68081,34.90909 139.63636,52.62222 214.10909,52.62222 6.07677,0 12.15354,-0.51717 18.10101,-0.51717 z M 213.20404,219.28081 c 15.77374,-40.46869 34.39192,-74.9899 52.62222,-102.91717 -35.42626,21.8505 -67.49091,48.87272 -95.93535,79.12727 11.24848,6.98182 25.6,15.38586 43.31313,23.7899 z m 628.36364,532.94545 c -6.46465,35.42627 -13.96364,71.62829 -23.27273,109.89899 52.10505,-46.02828 93.60808,-102.91717 121.01818,-166.65858 -30.77172,20.81616 -63.35353,39.95151 -97.74545,56.75959 z')

        earth.append("path")
          .attr('d', 'm 550.14141,585.95556 v 213.59191 c 95.41819,0 186.69899,-20.42828 272.80809,-61.02626 13.05858,-74.47272 19.52323,-142.86869 21.46262,-205.70505 -95.0303,32.71111 -194.19798,50.42424 -294.27071,53.1394 z')

        earth.append("path")
          .attr('d', "m 818.81212,762.82828 c -84.81616,37.23637 -175.96768,56.88889 -268.67071,56.88889 v 155.92727 c 87.53132,-6.98181 171.31314,-39.04646 241.5192,-92.18585 10.73131,-41.50303 20.0404,-81.97172 27.15151,-120.63031 z m 15.25657,-543.15959 c 9.30909,-4.13738 18.61818,-8.79192 27.92727,-13.44647 -13.96364,-16.29091 -29.34949,-31.15959 -45.12323,-45.12323 6.07677,17.58384 11.63636,37.23636 17.19596,58.5697 z m -602.24647,8.40404 c 93.60808,42.40808 194.97374,63.35353 297.37374,62.31919 V 47.062626 C 453.30101,43.830303 377.40606,59.60404 309.0101,92.70303 c -25.08283,31.54748 -53.91515,76.67071 -77.18788,135.3697 z")

        earth.selectAll("path")
          .attr('style', "fill:#297aff")

        // 向defs中添加docker图标
        let docker = defs.append('g')
          .attr('id', "docker")
          .attr('transform', 'scale(0.042)');
        docker.append("path")
          .attr('d', "M 1006.7627,438.61333 A 163.24267,163.24267 0 0 0 885.84533,427.648 a 160.85333,160.85333 0 0 0 -65.408,-102.4 L 807.552,315.09333 796.58667,327.46667 a 135.12533,135.12533 0 0 0 -25.6,97.45066 c 1.70666,23.63734 10.15466,46.37867 24.448,65.49334 -11.22134,6.272 -22.99734,11.52 -35.2,15.40266 a 233.38667,233.38667 0 0 1 -72.448,11.69067 H 3.754667 L 2.304,532.82133 a 285.48267,285.48267 0 0 0 24.106667,148.992 l 9.386666,18.51734 1.024,1.70666 c 64.213337,106.19734 192.554667,161.152 315.519997,161.152 238.16534,0 419.328,-113.70666 509.61067,-332.20266 60.33067,3.072 121.94133,-14.29334 151.552,-70.31467 l 7.5093,-14.29333 -14.2506,-7.97867 z m -803.66937,276.352 A 53.546667,53.546667 0 0 1 205.14133,608 53.333333,53.333333 0 0 1 258.048,661.504 54.229333,54.229333 0 0 1 203.09333,714.96533 Z")
          .attr("style", "fill:#039bc5")
        docker.append("path")
          .attr("d", "m 203.09333,633.25867 a 28.16,28.16 0 1 0 28.928,28.24533 28.501333,28.501333 0 0 0 -8.704,-20.13867 27.946667,27.946667 0 0 0 -20.224,-8.10666 z")
          .attr("style", "fill:#38504f")
        docker.append("path")
          .attr("d", "m 54.869333,387.88267 h 97.109337 v 97.152 H 54.912 V 387.84 m 129.49333,0 h 97.10934 v 97.152 h -97.152 V 387.84 m 0,-129.49333 H 281.472 v 97.152 h -97.10933 v -97.10934 m 129.57866,0 h 97.152 v 97.152 h -97.152 v -97.152 m 0,129.49334 h 97.152 v 97.152 h -97.152 V 387.84 m 129.49334,0 h 97.152 v 97.152 h -97.152 V 387.84 m 129.57866,0 h 97.152 v 97.152 h -97.152 V 387.84 M 443.43467,258.34667 h 97.152 v 97.152 h -97.152 v -97.10934 m 0,-129.57866 h 97.152 v 97.152 h -97.152 v -97.152")
          .attr("style", "fill:#2bb558")
      }

      //初始化链接线
      initLink () {
        this.drawLinkLine();
        this.drawLinkText();
      }

      //初始化节点
      initNode () {
        var self = this;
        //节点容器
        this.nodes = this.container.selectAll(".node")
          .data(this.data)
          .enter()
          .append("g")
          .attr("transform", function (d) {
            return "translate(" + d.x + "," + d.y + ")";
          })
          .call(d3.drag()
            // 给每一个节点添加拖拽事件
            .on("drag", function (d) {
              self.onDrag(this, d)
            })
          )
          // 给每一个节点添加点击事件
          .on('click', function () {
            that.dialogVisible = true
          })
        //节点背景默认背景层
        this.nodes.append('circle')
          .attr('r', symbolSize / 1.5 + padding)
          .attr('class', 'node-bg').attr("opacity", "1");

        //节点图标
        this.drawNodeSymbol();
        //节点标题
        this.drawNodeTitle();
        // 节点旁边的小图标
        this.drawNodeCode();
      }

      // 绘制配置完成标识
      drawNodeCode () {
        this.nodeCodes = this.nodes.filter(item => item.isConfig == "true")
          .append('g')
          .attr('class', 'node-code')
          .attr('transform', 'translate(' + -symbolSize / 2 + ',' + symbolSize / 3 + ')')

        this.nodeCodes
          .append('circle')
          .attr('r', () => fontSize * 1)

        this.nodeCodes
          .append('text')
          .attr('dy', fontSize / 1.4)
          // .text(item => item.code);
          .attr("style", "font-size:14px;line-height:14px")
          .text("√");
      }

      //绘制节点图标
      drawNodeSymbol () {
        this.nodes.filter(item => item.type == 'app')
          .append("circle")
          .attr("r", symbolSize / 2)
          .attr("fill", '#fff')
          .attr('class', function (d) {
            return 'health' + d.health;
          })
          .attr('stroke-width', '5px')

        // 在<defs>元素中定义的图形不会直接显示在SVG图像上。要显示它们需要使用<use>元素来引入它们
        // <use>元素通过xlink:href属性来引入<g>元素。注意在ID前面要添加一个#。
        //绘制数据库图标
        this.nodes.filter(item => item.type == 'database')
          .append('use')
          .attr('xlink:href', '#database')
          .attr('x', function () {
            return -this.getBBox().width / 2
          })
          .attr('y', function () {
            return -this.getBBox().height / 2
          })

        //绘制云图标
        this.nodes.filter(item => item.type == 'cloud')
          .append('use')
          .attr('xlink:href', '#cloud')
          .attr('x', function () {
            return -this.getBBox().width / 2
          })
          .attr('y', function () {
            return -this.getBBox().height / 2
          })
        // 绘制地球图标
        this.nodes.filter(item => item.type == 'earth')
          .append('use')
          .attr('xlink:href', '#earth')
          .attr('x', function () {
            return -this.getBBox().width / 2
          })
          .attr('y', function () {
            return -this.getBBox().height / 2
          })
        // 绘制应用图标
        this.nodes.filter(item => item.type == 'myapp')
          .append('use')
          .attr('xlink:href', '#myapp')
          .attr('x', function () {
            return -this.getBBox().width / 2
          })
          .attr('y', function () {
            return -this.getBBox().height / 2
          })
        // 绘制docker图标
        this.nodes.filter(item => item.type == 'docker')
          .append('use')
          .attr('xlink:href', '#docker')
          .attr('x', function () {
            return -this.getBBox().width / 2
          })
          .attr('y', function () {
            return -this.getBBox().height / 2
          })
      }

      //画节点标题
      drawNodeTitle () {
        //节点标题
        this.nodes.append("text")
          .attr('class', 'node-title')
          .text(function (d) {
            return d.name;
          })
          .attr("dy", symbolSize)
        // 处理节点图标中的百分比
        this.nodes.filter(item => item.type == 'app').append("text")
          .text(function (d) {
            return (d.active / d.total) * 100 + "%";
          })
          .attr('dy', fontSize / 2)
          .attr('class', 'node-call')
      }

      // 画节点链接线
      drawLinkLine () {
        let data = this.data;
        if (this.lineGroup) {
          this.lineGroup.selectAll('.link')
            .attr(
              'd', link => genLinkPath(link),
            )
        } else {
          this.lineGroup = this.container.append('g')
          this.lineGroup.selectAll('.link')
            .data(this.edges)
            .enter()
            .append('path')
            .attr('class', 'link')
            .attr(
              'marker-end', (link, i) => 'url(#' + 'marker-' + i + ')'
            ).attr(
              'd', link => genLinkPath(link),
            ).attr(
              'id', (link, i) => 'link-' + i
            )
        }

        // 确认连接线的路径
        function genLinkPath (d) {
          let sx = data[d.source].x;
          let tx = data[d.target].x;
          let sy = data[d.source].y;
          let ty = data[d.target].y;
          return 'M' + sx + ',' + sy +
            ' L' + tx + ',' + ty
        }
      }

      //画节点链接线文字
      drawLinkText () {
        let data = this.data;
        let self = this;
        if (this.lineTextGroup) {
          this.lineTexts
            .attr('transform', getTransform)
        } else {
          this.lineTextGroup = this.container.append('g')

          this.lineTexts = this.lineTextGroup
            .selectAll('.linetext')
            .data(this.edges)
            .enter()
            .append('text')
            .attr('dy', -2)
            .attr('transform', getTransform)
            .on('click', () => { alert() })

          this.lineTexts
            .append('tspan')
            .text((d) => this.data[d.source].upwardText);

          this.lineTexts
            .append('tspan')
            .text((d) => this.data[d.source].underText)
            .attr('dy', '1em')
            .attr('dx', function () {
              return -this.getBBox().width / 2
            })
        }

        function getTransform (link) {
          let s = data[link.source];
          let t = data[link.target];
          let p = self.getCenter(s.x, s.y, t.x, t.y);
          let angle = self.getAngle(s.x, s.y, t.x, t.y);
          if (s.x > t.x && s.y < t.y || s.x < t.x && s.y > t.y) {
            angle = -angle
          }
          return 'translate(' + p[0] + ',' + p[1] + ') rotate(' + angle + ')'
        }
      }

      // 更新视图(图标位置和连接线)
      update () {
        this.drawLinkLine();
        this.drawLinkText();
      }

      //拖拽方法
      onDrag (ele, d) {
        console.log("触发拖拽onDrag")
        d.x = d3.event.x;
        d.y = d3.event.y;
        d3.select(ele)
          .attr('transform', "translate(" + d3.event.x + "," + d3.event.y + ")")
        this.update();
      }

      //缩放方法
      onZoom (ele) {
        this.width = this.svg.attr('width');
        var transform = d3.zoomTransform(ele);
        this.scale = transform.k;
        // this.scale>1则为放大, <1为缩小
        this.container.attr('transform', "translate(" + transform.x + "," + transform.y + ")scale(" + transform.k + ")")
      }

      //主渲染方法
      render () {
        this.scale = 1;
        // 操作svg画布
        this.container = this.svg.append('g')
          .attr('transform', 'scale(' + this.scale + ')')

        // 执行类中定义的方法
        // 1.获取所有节点位置数据
        this.initPosition();
        // 2.初始化图标数据
        this.initDefineSymbol();
        // 3.初始化连接线的信息
        this.initLink();
        // 4.初始化节点
        this.initNode();
        // 5.初始化缩放
        this.initZoom();
      }
    }

3)创建一个topo类

1
2
    let t = new Topo('#topo', options);
    t.render();

3.样式部分

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
<style >
#topo {
  border: 1px solid #ccc;
  user-select: none;
}

#topo text {
  font-size: 10px;
  /*和js里保持一致*/
  fill: #1a2c3f;
  text-anchor: middle;
}

#topo .node-other {
  text-anchor: start;
}

#topo .health1 {
  stroke: #92e1a2;
}

#topo .health2 {
  stroke: orange;
}

#topo .health3 {
  stroke: red;
}

#topo #cloud,
#topo #database {
  fill: #ccc;
}

#topo .link {
  stroke: black;
}

#topo .node-title {
  font-size: 14px;
}

#topo .node-code circle {
  fill: green;
}

#topo .node-code text {
  fill: #fff;
}

#topo .node-bg {
  fill: #fff;
}

#topo .arrow {
  fill: black;
}
</style>

4.拓扑图最终效果
在这里插入图片描述
5.总结:
该demo虽然元素比较齐全,但是节点之间的连线点到点直线相连的,若数据量一大节点数量多的话整个topo图的线路会比较杂乱影响观感
3. 完善拓扑图
在调研D3.js过程中,找到了基于D3的类库-dagre-d3;在学习和使用中,随着不断的深入,对于这个类库有了充分的了解,在查看完相关文档之后将其总结一下。
3.1 关于dagre-d3:
Dagre是一个能够在客户端轻松创建流程图的JavaScript类库,而dagre-d3可以理解为是Dagre的前端,它使用D3来进行渲染。
3.2 dagre-d3主要函数:
具体的用法建议直接看d3-dagre源码,这样不会有漏,这里列举下主要函数:

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
添加节点setNode(v, {label: 'VVV'})
添加边setEdge(v, s)
删除节点removeNode(v)
删除边removeEdge(v,s)
//得到流程图绘制对象
this.graph = new dagreD3.graphlib.Graph().setGraph({
    rankdir: this.direction  // 控制方向
    edgesep: 50, // 连接线水平方向的长度
    ranksep: 50  // 连接线竖直方向的长度
}).setDefaultEdgeLabel(function () { return {} })
//绘制节点
this.graph.setNode()
//绘制连接:
this.graph.setEdge(edges.source, edges.target,{
   label: 'text',            // 设置连接线上的文字
   class: `classname`,       // 设置连接线的class名
   style: `stroke: ...; fill: none;opacity:1`, // 设置行内样式
   arrowheadStyle: `fill: ${color};stroke: ${color};`,
   arrowhead: 'vee',         // 设置连接线箭头的样式
   id: "idname",             // 设置连接线的id名
   lineInterpolate: "basis"  // 节点之间使用曲线连接
})
//删除节点
this.graph.removeNode(v)
//删除边
this.graph.removeEdge(v,s)

demo中的拖拽、缩放功能还是通过D3实现,
3.1 准备工作:
安装dagre-d3:

1
npm i dagre-d3 --save

3.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
1.html结构
<template>
  <div>
    <button @click="turnDir('LR')">从左至右</button>
    <button @click="turnDir('RL')">从右至左</button>
    <button @click="turnDir('TB')">从上至下</button>
    <button @click="turnDir('BT')">从下至上</button>
    <el-dialog
      center
      title="节点详情"
      :visible.sync="dialogVisible"
      width="50%"
      @closed="subDialogVisible=false"
      @open="subDialogVisible=true"
    >
      <sub-topo v-if="subDialogVisible" />
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="dialogVisible = false">确 定</el-button>
      </span>
    </el-dialog>
    <svg width="1800" height="700">
      <g />
    </svg>
  </div>
</template>

2.js部分
treetopo中模拟的数据结构:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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
<script>
import list from "../../jsondata/treetopo"
import dagreD3 from "dagre-d3";
import * as d3 from "d3";
import subTopo from "./component/subtopo"
import $ from "jquery"
export default {
  components: {
    subTopo
  },
  data () {
    return {
      dialogVisible: false,
      subDialogVisible: false,
      direction: "TB"
    };
  },
  methods: {
    turnDir (dir) {
      this.direction = dir
      this.drawTopo()
    },
    drawTopo () {
      //获取D3
      var g = new dagreD3.graphlib.Graph().setGraph({
        rankdir: this.direction,
        edgesep: 50,
        ranksep: 50
      }).setDefaultEdgeLabel(function () { return {}; });
      function drawNode (arr) {
        // 添加节点(设置节点的特性)
        arr.forEach((item) => {
          g.setNode(item.id, { labelType: "html", label: `<i class="${item.type} ${item.isFinished ? "finished" : "unfinished"}"><b>${item.label}</b></i>` });
        });
      }
      drawNode(list.nodeInfos)

      // 链接关系(连线的属性)
      function drawLine (arr, color, opacity, textobj) {
        arr.forEach((item, index) => {
          g.setEdge(item.source, item.target, {
            label: textobj[index],
            lineInterpolate: 'basis',
            class: `${item.source}-${item.target}`,
            style: `stroke: ${color}; fill: none;opacity:${opacity}`,
            arrowheadStyle: `fill: ${color};stroke: ${color};stroke-width:0.1`,
            arrowhead: 'vee',
            id: "status" + index
          });
        });
      }
      drawLine(list.edges, '#7c7c7c', 1, list.dataFlow);

      //绘制图形
      var svg = d3.select("svg"),
        inner = svg.select("g");

      //缩放
      var zoom = d3.zoom().on("zoom", function () {
        inner.attr("transform", d3.event.transform);
      });
      svg.call(zoom);
      var render = new dagreD3.render();
      render(inner, g);
      // let code;
      //鼠标悬浮事件
      inner.selectAll("g.node").on("mouseover", e => {
        // 先获取所有的线段,并将这些线段都设置透明度为0.2
        $(`g.edgePath`).attr("style", "opacity:0.2")
        // 当前的节点名字为e,将所有与e有关的线段添加类名active,进行高亮显示
        list.edges.forEach(item => {
          $(`.${e}-${item.target}`).addClass("active")
          $(`.${item.target}-${e}`).addClass("active2")
          $(`.${e}-${item.source}`).addClass("active")
          $(`.${item.source}-${e}`).addClass("active")
        })
      }).on("click ", () => {
        this.dialogVisible = true
        this.subDialogVisible = true
        // d3.event.sourceEvent.stopPropagation();
        return false
      }).on("mouseout", () => {
        drawLine(list.edges, '#7c7c7c', 1, list.dataFlow);
        var render = new dagreD3.render();
        render(inner, g);
      })
      var initialScale = 1;
      svg.call(
        zoom.transform,
        d3.zoomIdentity
          .translate(
            (svg.attr("width") - g.graph().width * initialScale) / 2,
            50
          )
          .scale(initialScale)
      );
      svg.attr("height", g.graph().height * initialScale + 40);
    }
  },
  mounted () {
    this.drawTopo()
  }
};
</script>

3.css样式

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
<style lang="less">
svg {
  font-size: 14px;
  height: 700px;
  width: 1800px;
  border: 1px solid #000;
}

foreignObject {
  width: 50px;
  height: 63px;
  background-color: transparent;
}
.node circle,
.node ellipse,
.node rect {
  fill: transparent;
  stroke-width: 0px;
  stroke: red;
}
.nodes .node rect {
  width: 70px;
  height: 70px;
}
.edgePath path {
  width: 0;
  stroke: #606266;
  fill: #333;
  stroke-width: 1.5px;
}
.database,
.defend,
.service {
  display: inline-block;
  width: 40px;
  height: 40px;
  background-size: contain;
  position: relative;
  overflow: visible;
  background-repeat: no-repeat;
  b {
    position: absolute;
    bottom: -25px;
    left: 50%;
    transform: translateX(-50%);
    font-style: normal;
  }
}
.active,
.active2 {
  stroke-width: 2px;
  opacity: 1 !important;
  path {
    stroke: #f48771 !important;
  }
}
.active2 {
  path {
    stroke: green !important;
  }
}
.database {
  background-image: url("../../assets/database.png");
}
.defend {
  background-image: url("../../assets/defend.png");
}
.service {
  background-image: url("../../assets/service.png");
}
.finished::before {
  content: "√";
  display: inline-block;
  width: 50%;
  height: 50%;
  border-radius: 50%;
  background-color: green;
  position: absolute;
  bottom: 0;
  left: -5px;
  color: #fff;
  text-align: center;
  line-height: 160%;
  font-size: 16px;
}
</style>

在这里插入图片描述
总体设计思路:
1.先使用dagre-d3渲染出基础的拓扑图,
2.每一个节点内显示的内容使用i标签代替;并在i标签内插入图标素材,根据不同数据插入不同的图片和标题),