D3.js实现拓扑图
阅读原文时间:2023年07月10日阅读:2

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

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章