D3.js实现拓扑图的示例代码
lihualong 人气:0最近写项目需要画出应用程序调用链的网路拓扑图,完全自己写需要花费些时间,那么首先想到的是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:/https://img.qb5200.com/download-x/d3js.orghttps://img.qb5200.com/download-x/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, // }, ] }
加载全部内容