视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
如何用D3.js实现拓扑图
2020-11-27 19:33:54 责编:小采
文档
这篇文章主要介绍了关于如何用D3.js实现拓扑图,有着一定的参考价值,现在分享给大家,有需要的朋友可以参考一下

最近写项目需要画出应用程序调用链的网路拓扑图,完全自己写需要花费些时间,那么首先想到的是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-160V0c0 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','M 224a448 160 0 1 0 6 0 448 160 0 1 0-6 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-2.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,
 // },
 ]
}

下载本文
显示全文
专题