最近写项目需要画出应用程序调用链的网路拓扑图,完全自己写需要花费些时间,那么首先想到的是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, // }, ] }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持小牛知识库。
本文向大家介绍d3.js实现简单的网络拓扑图实例代码,包括了d3.js实现简单的网络拓扑图实例代码的使用技巧和注意事项,需要的朋友参考一下 前言 了解了D3.js的基本开发和组件以后,我们开始应用它激动人心之处:绚丽的预定义图形,应用D3.js,我们在它的示例文件的基础上稍加变动即可应用于我们的数据可视化工作中,D3.js将后台的运算已经预定义好,我们只需少量代码和规范的数据,就能做出很花哨(请原
请问这种拓扑图如何画呢?纯css好实现吗?
我正在研究一个storm拓扑,需要为不同的客户端位置构建多个拓扑。 谢谢你的回复。
真的很感谢你的帮助。 谢谢
本文向大家介绍D3.js实现简洁实用的动态仪表盘的示例,包括了D3.js实现简洁实用的动态仪表盘的示例的使用技巧和注意事项,需要的朋友参考一下 本文介绍了D3.js实现简洁实用的动态仪表盘的示例,分享给大家,具体如下: 动态效果图: 仪表盘效果图 细看上面的动态效果图,可以发现: 一个值变换到一个新的值时,是一个渐变的过程; 圆弧末尾有一个竖线,作为仪表盘的指针,在仪表盘数值变化时,有一个弹性的动
我刚来暴风,所以温柔点:-) 什么是实现这一目标的最佳方式?