D3.js实现拓扑图的示例代码


Posted in Javascript onJune 30, 2018

最近写项目需要画出应用程序调用链的网路拓扑图,完全自己写需要花费些时间,那么首先想到的是echarts,但echarts的自定义写法写起来非常麻烦,而且它的文档都是基于配置说明的,对于自定义开发不太方便,尝试后果断放弃,改用D3.js,自己完全可控。

我们先看看效果

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,
  // },
 ]
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
js有序数组的连接问题
Oct 01 Javascript
JS判断、校验MAC地址的2个实例
May 05 Javascript
js构造函数、索引数组和属性的实现方式和使用
Nov 16 Javascript
详解JavaScript ES6中的Generator
Jul 28 Javascript
Javascript的表单验证-揭开正则表达式的面纱
Mar 18 Javascript
jQuery短信验证倒计时功能实现方法详解
May 25 Javascript
Ionic + Angular.js实现图片轮播的方法示例
May 21 Javascript
webpack-dev-server自动更新页面方法
Feb 22 Javascript
vue页面切换到滚动页面显示顶部的实例
Mar 13 Javascript
VSCode中如何利用d.ts文件进行js智能提示
Apr 13 Javascript
React如何解决fetch跨域请求时session失效问题
Nov 02 Javascript
Vue组件化开发之通用型弹出框的实现
Feb 28 Javascript
详解angular如何调用HTML字符串的方法
Jun 30 #Javascript
angular6.0使用教程之父组件通过url传递id给子组件的方法
Jun 30 #Javascript
基于webpack4搭建的react项目框架的方法
Jun 30 #Javascript
AngularJs分页插件使用详解
Jun 30 #Javascript
ionic grid(栅格)九宫格制作详解
Jun 30 #Javascript
vue检测对象和数组的变化分析
Jun 30 #Javascript
浅析vue.js数组的变异方法
Jun 30 #Javascript
You might like
判“新”函数:得到今天与明天的秒数
2006/10/09 PHP
解析php中的fopen()函数用打开文件模式说明
2013/06/20 PHP
php正则替换处理HTML页面的方法
2015/06/17 PHP
Thinkphp页面跳转设置跳转等待时间的操作
2019/10/16 PHP
WEB高性能开发之疯狂的HTML压缩
2010/06/19 Javascript
jQuery UI Dialog 创建友好的弹出对话框实现代码
2012/04/12 Javascript
js读取配置文件自写
2014/02/11 Javascript
jQuery 无限级菜单的简单实例
2014/02/21 Javascript
js加密解密字符串可自定义密码因子
2014/05/13 Javascript
jQuery晃动层特效实现方法
2015/03/09 Javascript
javascript基本包装类型介绍
2015/04/10 Javascript
JavaScript+html5 canvas制作色彩斑斓的正方形效果
2016/01/27 Javascript
详解nodejs的express如何自动生成项目框架
2017/07/12 NodeJs
Node.Js中实现端口重用原理详解
2018/05/03 Javascript
vue 项目中使用Loading组件的示例代码
2018/08/31 Javascript
Pyramid Mako模板引入helper对象的步骤方法
2013/11/27 Python
Python 常用 PEP8 编码规范详解
2017/01/22 Python
Python多进程与服务器并发原理及用法实例分析
2018/08/21 Python
Python操作远程服务器 paramiko模块详细介绍
2019/08/07 Python
不到20行实现Python代码即可制作精美证件照
2020/04/24 Python
Django之富文本(获取内容,设置内容方式)
2020/05/21 Python
Python logging模块进行封装实现原理解析
2020/08/07 Python
HTML5 Web Database 数据库的SQL语句的使用方法
2012/12/09 HTML / CSS
大学生村官事迹材料
2014/01/21 职场文书
企业文化建设实施方案
2014/03/22 职场文书
监督检查工作方案
2014/05/28 职场文书
我们的节日中秋活动方案
2014/08/19 职场文书
学校党的群众路线教育实践活动对照检查材料
2014/09/24 职场文书
群众路线党员个人剖析材料
2014/10/08 职场文书
学术会议通知
2015/04/15 职场文书
旅游项目合作意向书
2015/05/08 职场文书
接收函
2019/04/22 职场文书
2019年最新证婚词精选集!
2019/06/28 职场文书
纯CSS实现酷炫的霓虹灯效果
2021/04/13 HTML / CSS
解析目标检测之IoU
2021/06/26 Python
Python通用验证码识别OCR库ddddocr的安装使用教程
2022/07/07 Python