- D3的简介
D3 全名为 Data Drive Document,即通过 Data 操作 Document,而在做数据可视化时,Data 最常 Drive 的 Document 便是 SVG。刚了解到D3时,看到D3官网非常丰富且酷炫的Demo,便觉得 D3 应该有着无限可能的图形开发能力,所以在学习完基础的API和SVG的基础后,就开始着手绘制D3的节点拓扑图了; - 绘制简易的可拖拽节点拓扑图
2.1 准备工作:
- 安装D3:
1 | npm install d3 --save |
- 项目中导入D3:
1 | import * as d3 from "d3" |
- 准备模拟好的节点数据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> |
- 在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标签内插入图标素材,根据不同数据插入不同的图片和标题),