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的写法基础分析
Jan 17 Javascript
js控制滚动条缓慢滚动到顶部实现代码
Mar 20 Javascript
原生Ajax 和jQuery Ajax的区别示例分析
Dec 17 Javascript
JavaScript使用Replace进行字符串替换的方法
Apr 14 Javascript
详解JavaScript正则表达式中的global属性的使用
Jun 16 Javascript
全面解析Bootstrap图片轮播效果
Dec 03 Javascript
JavaScript函数柯里化原理与用法分析
Mar 31 Javascript
Angular 4.x 动态创建表单实例
Apr 25 Javascript
理解 javascript 中的函数表达式与函数声明
Jul 07 Javascript
详解React Native顶|底部导航使用小技巧
Sep 14 Javascript
webpack+vue中使用别名路径引用静态图片地址
Nov 20 Javascript
vue.js 嵌套循环、if判断、动态删除的实例
Mar 07 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
PHP排序之二维数组的按照字母排序实现代码
2011/08/13 PHP
解析PHP中的正则表达式以及模式匹配
2013/06/19 PHP
php 生成短网址原理及代码
2014/01/23 PHP
php中hashtable实现示例分享
2014/02/13 PHP
ThinkPHP视图查询详解
2014/06/30 PHP
PHP使用PDO 连接与连接管理操作实例分析
2020/04/21 PHP
PHP接入支付宝接口失效流程详解
2020/11/10 PHP
extjs fckeditor集成代码
2009/05/10 Javascript
jquery 得到当前页面高度和宽度的两个函数
2010/02/21 Javascript
js当一个变量为函数时 应该注意的一点细节小结
2011/12/29 Javascript
JQuery插件Style定制化方法的分析与比较
2012/05/03 Javascript
jQuery在html有效在jsp无效的原因及解决方法
2013/08/02 Javascript
javascripit实现密码强度检测代码分享
2013/12/12 Javascript
JavaScript的原型继承详解
2015/02/15 Javascript
js实现适用于素材网站的黑色多级菜单导航条效果
2015/08/24 Javascript
js实现动态加载脚本的方法实例汇总
2015/11/02 Javascript
jQuery使用中可能被XSS攻击的一些危险环节提醒
2016/05/24 Javascript
详解微信小程序入门五: wxml文件引用、模版、生命周期
2017/01/20 Javascript
Angular 2 ngForm中的ngModel、[ngModel]和[(ngModel)]的写法
2017/06/29 Javascript
解决Mac安装thrift因bison报错的问题
2018/05/17 Javascript
详解Webpack如何引入CDN链接来优化编译后的体积
2019/06/21 Javascript
微信小程序吸底区域适配iPhoneX的实现
2020/04/09 Javascript
vue axios封装httpjs,接口公用配置拦截操作
2020/08/11 Javascript
使用python实现递归版汉诺塔示例(汉诺塔递归算法)
2014/04/08 Python
python自定义异常实例详解
2017/07/11 Python
基于windows下pip安装python模块时报错总结
2018/06/12 Python
python使用turtle库与random库绘制雪花
2018/06/22 Python
flask中过滤器的使用详解
2018/08/01 Python
Python中的四种交换数值的方法解析
2019/11/18 Python
Python web如何在IIS发布应用过程解析
2020/05/27 Python
英国排名第一的LED灯泡网站:LED Bulbs
2019/09/03 全球购物
英国最好的包装供应商:Priory Direct
2019/12/17 全球购物
一些Solaris面试题
2015/12/22 面试题
管理部部长岗位职责
2013/12/05 职场文书
Js类的构建与继承案例详解
2021/09/15 Javascript
进阶篇之linux环境下安装MySQL数据库
2022/04/09 MySQL