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 相关文章推荐
简单实用的全选反选按钮例子
Oct 18 Javascript
JavaScript中的console.trace()函数介绍
Dec 29 Javascript
jQuery+PHP实现微信转盘抽奖功能的方法
May 25 Javascript
微信小程序 使用picker封装省市区三级联动实例代码
Oct 28 Javascript
基于BootstrapValidator的Form表单验证(24)
Dec 12 Javascript
Bootstrap表格使用方法详解
Feb 17 Javascript
Angular.js去除页面中显示的空行方法示例
Mar 30 Javascript
ES5学习教程之Array对象
Apr 01 Javascript
vue.js实现带日期星期的数字时钟功能示例
Aug 28 Javascript
vue完成项目后,打包成静态文件的方法
Sep 03 Javascript
js实现图片3D轮播效果
Sep 21 Javascript
Echarts实现单条折线可拖拽效果
Dec 19 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
Apache2 httpd.conf 中文版
2006/11/17 PHP
E路文章系统PHP
2006/12/11 PHP
php上传文件中文文件名乱码的解决方法
2013/11/01 PHP
详解Yii2 之 生成 URL 的方法
2017/06/16 PHP
Extjs TimeField 显示正常时间格式的代码
2011/06/28 Javascript
js中将HTMLCollection/NodeList/伪数组转换成数组的代码
2011/07/31 Javascript
jQuery学习笔记 获取jQuery对象
2012/09/19 Javascript
JS添加删除一组文本框并对输入信息加以验证判断其正确性
2013/04/11 Javascript
基于jquery的网站幻灯片切换效果焦点图代码
2013/09/15 Javascript
js与运算符和或运算符的妙用
2014/02/14 Javascript
node.js中的http.response.write方法使用说明
2014/12/14 Javascript
js控制网页前进和后退的方法
2015/06/08 Javascript
分享jQuery插件的学习笔记
2016/01/14 Javascript
浅谈JavaScript的闭包函数
2016/12/08 Javascript
jquery 实时监听输入框值变化的完美方法(必看)
2017/01/26 Javascript
利用js的闭包原理做对象封装及调用方法
2017/04/07 Javascript
微信小程序中使用javascript 回调函数
2017/05/11 Javascript
Vue.js结合Ueditor富文本编辑器的实例代码
2017/07/11 Javascript
Angular 4中如何显示内容的CSS样式示例代码
2017/11/06 Javascript
vue实现消息的无缝滚动效果的示例代码
2017/12/05 Javascript
浅谈vue 单文件探索
2018/09/05 Javascript
微信小程序单选radio及多选checkbox按钮用法示例
2019/04/30 Javascript
Vue实现菜单切换功能
2020/11/08 Javascript
三剑客:offset、client和scroll还傻傻分不清?
2020/12/04 Javascript
Python中防止sql注入的方法详解
2017/02/25 Python
特征脸(Eigenface)理论基础之PCA主成分分析法
2018/03/13 Python
Windows下pycharm创建Django 项目(虚拟环境)过程解析
2019/09/16 Python
tensorflow ckpt模型和pb模型获取节点名称,及ckpt转pb模型实例
2020/01/21 Python
全球销量第一生发产品:Viviscal
2017/12/21 全球购物
《掌声》教学反思
2014/02/23 职场文书
庆祝新中国成立65周年“向国旗敬礼”网上签名寄语
2014/09/27 职场文书
出租车拒载检讨书
2015/01/28 职场文书
2015年社区妇联工作总结
2015/04/21 职场文书
小学生六年级作文之关于感恩
2019/08/16 职场文书
JAVA springCloud项目搭建流程
2022/05/11 Java/Android
JS前端监控采集用户行为的N种姿势
2022/07/23 Javascript