D3.js实现拓扑图的示例代码
程序员文章站
2023-10-18 09:17:34
最近写项目需要画出应用程序调用链的网路拓扑图,完全自己写需要花费些时间,那么首先想到的是echarts,但echarts的自定义写法写起来非常麻烦,而且它的文档都是基于配置...
最近写项目需要画出应用程序调用链的网路拓扑图,完全自己写需要花费些时间,那么首先想到的是echarts,但echarts的自定义写法写起来非常麻烦,而且它的文档都是基于配置说明的,对于自定义开发不太方便,尝试后果断放弃,改用d3.js,自己完全可控。
我们先看看效果
我把代码分享下,供和我一样刚接触d3的同学参考,不对的地方欢迎指正!
完整代码:
html:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>title</title> <script type="text/javascript" src="http://d3js.org/d3.v5.min.js"> </script> <style> body{ overflow: hidden; } #togo{ width: 800px; height:500px; border:1px solid #ccc; user-select: none; } #togo text{ font-size:10px;/*和js里保持一致*/ fill:#1a2c3f; text-anchor: middle; } #togo .node-other{ text-anchor: start; } #togo .health1{ stroke:#92e1a2; } #togo .health2{ stroke:orange; } #togo .health3{ stroke:red; } #togo #cloud,#togo #database{ fill:#ccc; } #togo .link{ stroke:#e4e8ed; } #togo .node-title{ font-size: 14px; } #togo .node-code circle{ fill:#3f86f5; } #togo .node-code text{ fill:#fff; } #togo .node-bg{ fill:#fff; } #togo .arrow{ fill:#e4e8ed; } </style> <script src="data.js"></script> </head> <body> <svg id="togo" width="800" height="500"> </svg> <script src="togo.js"></script> <script> </script> <script> let t=new togo('#togo',__options); t.render(); </script> </body> </html>
js:
const fontsize = 10; const symbolsize = 40; const padding = 10; /* * 调用 new togo(svg,option).render(); * */ class togo { /**/ constructor(svg, option) { this.data = option.data; this.edges = option.edges; this.svg = d3.select(svg); } //主渲染方法 render() { this.scale = 1; this.width = this.svg.attr('width'); this.height = this.svg.attr('height'); this.container = this.svg.append('g') .attr('transform', 'scale(' + this.scale + ')'); this.initposition(); this.initdefinesymbol(); this.initlink(); this.initnode(); this.initzoom(); } //初始化节点位置 initposition() { let origin = [this.width / 2, this.height / 2]; let points = this.getvertices(origin, math.min(this.width, this.height) * 0.3, this.data.length); this.data.foreach((item, i) => { item.x = points[i].x; item.y = points[i].y; }) } //根据多边形获取定位点 getvertices(origin, r, n) { if (typeof n !== 'number') return; var ox = origin[0]; var oy = origin[1]; var angle = 360 / n; var i = 0; var points = []; var tempangle = 0; while (i < n) { tempangle = (i * angle * math.pi) / 180; points.push({ x: ox + r * math.sin(tempangle), y: oy + r * math.cos(tempangle), }); i++; } return points; } //两点的中心点 getcenter(x1, y1, x2, y2) { return [(x1 + x2) / 2, (y1 + y2) / 2] } //两点的距离 getdistance(x1, y1, x2, y2) { return math.sqrt(math.pow(x2 - x1, 2) + math.pow(y2 - y1, 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() { let defs=this.container.append('svg:defs'); //箭头 const marker = 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) .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') //数据库 let database =defs.append('g') .attr('id','database') .attr('transform','scale(0.042)'); database.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') database.append('path') .attr('d','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') ; database.append('path') .attr('d','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') ; database.append('path') .attr('d','m64 224a448 160 0 1 0 896 0 448 160 0 1 0-896 0z'); //云 let cloud=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') } //初始化链接线 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 () { alert() }) //节点背景默认覆盖层 this.nodes.append('circle') .attr('r', symbolsize / 2 + padding) .attr('class', 'node-bg'); //节点图标 this.drawnodesymbol(); //节点标题 this.drawnodetitle(); //节点其他说明 this.drawnodeother(); this.drawnodecode(); } //画节点语言标识 drawnodecode() { this.nodecodes = this.nodes.filter(item => item.type == 'app') .append('g') .attr('class','node-code') .attr('transform', 'translate(' + -symbolsize / 2 + ',' + symbolsize / 3 + ')') this.nodecodes .append('circle') .attr('r', d => fontsize / 2 * d.code.length / 2 + 3) this.nodecodes .append('text') .attr('dy', fontsize / 2) .text(item => item.code); } //画节点图标 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') 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 }) } //画节点右侧信息 drawnodeother() { //如果是应用的时候 this.nodeothers = this.nodes.filter(item => item.type == 'app') .append("text") .attr("x", symbolsize / 2 + padding) .attr("y", -5) .attr('class','node-other') this.nodeothers.append('tspan') .text(d => d.time + 'ms'); this.nodeothers.append('tspan') .text(d => d.rpm + 'rpm') .attr('x', symbolsize / 2 + padding) .attr('dy', '1em'); this.nodeothers.append('tspan') .text(d => d.epm + 'epm') .attr('x', symbolsize / 2 + padding) .attr('dy', '1em') } //画节点标题 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; }) .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 ) .on('click', () => { alert() }) } 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, i) => this.data[d.source].linetime + 'ms,' + this.data[d.source].linerpm + 'rpm'); this.linetexts .append('tspan') .text((d, i) => this.data[d.source].lineprotocol) .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(d) { this.drawlinkline(); this.drawlinktext(); } //拖拽方法 ondrag(ele, d) { d.x = d3.event.x; d.y = d3.event.y; d3.select(ele) .attr('transform', "translate(" + d3.event.x + "," + d3.event.y + ")") this.update(d); } //缩放方法 onzoom(ele) { var transform = d3.zoomtransform(ele); this.scale = transform.k; this.container.attr('transform', "translate(" + transform.x + "," + transform.y + ")scale(" + transform.k + ")") } }
数据:
let __options={ data:[{ type:'app', name: 'monitor-web-server', time: 30, rpm: 40, epm: 50, active: 3, total: 5, code: 'java', health: 1, lineprotocol: 'http', linetime: 12, linerpm: 34, }, { type:'database', name: 'mysql', time: 30, rpm: 40, epm: 50, active: 3, total: 5, code: 'java', health: 2, lineprotocol: 'http', linetime: 12, linerpm: 34, }, { type:'app', name: 'redis', time: 30, rpm: 40, epm: 50, active: 3, total: 5, code: 'java', health: 3, lineprotocol: 'http', linetime: 12, linerpm: 34, }, { type:'cloud', name: 'es', time: 30, rpm: 40, epm: 50, active: 3, total: 5, code: 'java', health: 1, lineprotocol: 'http', linetime: 12, linerpm: 34, value: 100 } ], edges: [ { source: 0, target: 3, }, { source: 1, target: 2, } , { source: 1, target: 3, }, { source: 0, target: 1, }, { source: 0, target: 2, } // { // source: 3, // target: 2, // }, ] }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。