用d3+vue实现一个可折叠的树状图

1 篇文章 0 订阅

用d3+vue实现一个可折叠的树状图

背景

前两天接到了一个需求,监控系统要实现一个可以折叠展开的树状图来展示完整的接口调用链,同时右侧要展示一些接口的相关信息诸如耗时、调用时间、返回值之类的。

Demo

先在网上找了个demo,完美符合了需求。
在这里插入图片描述
Demo地址:点击跳转demo

效果预览

接下来就是在demo的基础上加一些自定义的东西丰富下树状图,先给大家看一下效果:
展开状态:
在这里插入图片描述
折叠状态:
在这里插入图片描述

代码

设计思路很简单,先把demo封装成一个方法,返回值就是html代码,直接在父组件里通过操作dom替换成新的树状图,这样就可以完成展开折叠包括数据刷新后树状图的更新,不过感觉不是封装的很好,之后准备把一些展示的列相关的数据配置成变量。
下面是tree.js的代码:

import * as d3 from 'd3'

export const Tree = (data, { width = 1000, }, funcs) => {
    let i = 0;
    const root = d3.hierarchy(data).eachBefore(d => d.index = i++);
    const nodes = root.descendants();
    //node size
    const nodeSize = 20;
    //链路图右侧数据展示配置
    const columns = [
        {
            label: 'StartTime',
            value: d => {
                return d.traceRawData.startTime
            },
            format: (d, type) => {
                if (type === 'value') {
                    let startTime = d.data.traceRawData.startTime;
                    if (startTime === null) {
                        return '-';
                    }
                    else {
                        return startTime.slice(0, 10) + ' ' + startTime.slice(11, 19);
                    }
                }
                if (type === 'color') {
                    return 'black'
                }
                if (type === 'font-weight') {
                    return 'none'
                }
            },
            x: 420
        },
        {
            label: 'RetCode',
            value: d => {
                return d.traceRawData.tretCode
            },
            format: (d, type) => {
                if (type === 'value') {
                    let retCode = d.data.traceRawData.tretCode;
                    if (retCode === null) {
                        return '-';
                    }
                    else {
                        return retCode.slice(0, 10) + ' ' + retCode.slice(11, 19);
                    }
                }
                if (type === 'color') {
                    return 'black'
                }
                if (type === 'font-weight') {
                    return 'none'
                }
            },
            x: 570
        }
    ]
    //链路图右侧耗时展示
    const cost = [
        {
            label: 'Cost',
            value: d => {
                return d.traceRawData.duration
            },
            format: (d, type) => {
                if (type === 'value') {
                    let duration = d.data.traceRawData.duration;
                    if (duration === null) {
                        return '-'
                    }
                    if (duration > 1000) {
                        if (duration > 1000000) {
                            return (duration / 1000000).toFixed(1) + 's'
                        }
                        else {
                            return (duration / 1000).toFixed(1) + 'ms'
                        }
                    }
                    else {
                        return duration + 'µs'
                    }
                }
                if (type === 'color') {
                    let duration = d.data.traceRawData.duration;
                    if (duration === null) {
                        return '#555'
                    }
                    if (duration <= 600000) {
                        return '#2CA02C'
                    }
                    else {
                        if (duration > 600000 && duration <= 3500000) {
                            return '#FF7F0E'
                        }
                        else {
                            if (duration > 3500000) {
                                return 'rgb(217 28 28)'
                            }
                        }
                    }
                }
                if (type === 'font-weight') {
                    return 'bold'
                }
                if (type === 'bar-width') {
                    let duration = d.data.traceRawData.duration;
                    let length = 30;
                    if (duration === null) {
                        return 0;
                    }
                    if (duration <= 600000) {
                        return duration / 600000 * length;
                    }
                    else {
                        if (duration > 600000 && duration <= 3500000) {
                            return (duration - 600000) / 2900000 * length + length;
                        }
                        else {
                            if (duration > 3500000 && duration <= 15000000) {
                                return (duration - 3500000) / 11500000 * length + length * 2;
                            }
                            return length * 3
                        }
                    }
                }
            },
            x: 280
        }
    ]
    //缩放工具配置
    const zoom = [
        {
            label: '',
            value: d => {
                return d.traceRawData.duration
            },
            format: (d, type) => {
                if (type === 'symbol') {
                    if (d.data.children && d.data.children.length != 0) {
                        return '-'
                    }
                    else {
                        if (d.data._children && d.data._children.length != 0) {
                            return '+'
                        }
                    }
                }
                else {
                    if (type === 'circle') {
                        if (d.data.children && d.data.children.length != 0) {
                            return '#999'
                        }
                        else {
                            if (d.data._children && d.data._children.length != 0) {
                                return '#999'
                            }
                        }
                        return ''
                    }
                }
            },
            x: 8
        }
    ]

    //树状图定义
    const svg = d3.create('svg')
        .attr('viewBox', [-nodeSize / 2, -nodeSize * 3 / 2, width, (nodes.length + 1) * nodeSize])
        .attr('font-family', 'sans-serif')
        .attr('font-size', 10)
        .style('overflow', 'visible')

    const link = svg.append('g')
        .attr('fill', 'none')
        .attr('stroke', '#1F77B4')
        .selectAll('path')
        .data(root.links())
        .join('path')
        .attr('d', d =>
          `
            M${d.source.depth * nodeSize + 20},${d.source.index * nodeSize}
            V${d.target.index * nodeSize}
            h${nodeSize}
          `
        );

    const node = svg.append('g')
        .selectAll('g')
        .data(nodes)
        .join('g')
        .attr('transform', d => `translate(0,${d.index * nodeSize})`);

    node.append('circle')
        .attr('cx', d => d.depth * nodeSize + 20)
        .attr('r', 4)
        .attr('fill', d => {
            if (d.data.traceRawData.tkind === 'producer') {
                return '#2CA02C'
            }
            return '#FF7F0E'
        })

    node.append('text')
        .attr('dy', '0.32em')
        .attr('x', d => d.depth * nodeSize + 26)
        .text(d => d.data.name)

    node.append('title')
        .text(d => d.ancestors().reverse().map(d => d.data.name).join('/'))

    for (const { label, value, format, x } of columns) {
        svg.append('text')
            .attr('dy', '0.32em')
            .attr('y', -nodeSize)
            .attr('x', x)
            .attr('text-anchor', 'start')
            .attr('font-weight', 'bold')
            .text(label);

        node.append('text')
            .attr('dy', '0.32em')
            .attr('x', x)
            .attr('text-anchor', 'start')
            .attr('font-weight', d => format(d, 'font-weight'))
            .attr('fill', d => format(d, 'color'))
            .data(root.copy().sum(value).descendants())
            .text(d => format(d, 'value'));
    }

    for (const { label, value, format, x } of zoom) {
        svg.append('text')
            .attr('dy','0.32em')
            .attr('y',-nodeSize)
            .attr('x',x)
            .attr('text-anchor','end')
            .attr('font-weight','bold')
            .text(label);
        
        node.append('text')
            .attr('dy','0.32em')
            .attr('x',x)
            .attr('text-anchor','middle')
            .attr('font-weight','bold')
          .data(root.sum(value).descendants())
            .text(d=>format(d,'symbol'))
            .on('click',(event,d)=>funcs.handleNodeClick(d));
    }

    for (const { label, value, format, x } of cost) {
        svg.append('text')
            .attr('dy','0.32em')
            .attr('y',-nodeSize)
            .attr('x',x)
            .attr('text-anchor','end')
            .attr('font-weight','bold')
            .text(label);
        
        node.append('text')
            .attr('dy','0.32em')
            .attr('x',x)
            .attr('text-anchor','end')
            .attr('font-weight',d=>format(d,'font-weight'))
            .attr('fill',d=>format(d,'color'))
          .data(root.copy().sum(value).descendants())
            .text(d=>format(d,'value'));
        
        node.append('rect')
            .attr('x',x+5)
            .attr('y',-2)
            .attr('width',d=>format(d,'bar-width'))
            .attr('height',5)
            .attr('fill',d=>format(d,'color'))
    }

    return svg.node();
}

然后就是父组件,主要是写了了树状图展开折叠回调方法和图片刷新的方法,下面是tree.vue的代码:

<template>
  <div id="tree-chain"></div>
</template>

<script>
import { Tree } from "./tree";
import { getTreeData } from '@/api/trace'

export default {
  props: {
    params: {
      type: Object,
      default: () => ({}),
    },
  },
  watch: {
    params: {
      deep: true,
      handler: function (val) {
        this.onLoad();
      },
    },
  },
  data() {
    return {
      traceData: {},
      count: 0,
    };
  },
  mounted() {
    this.onLoad();
  },
  methods: {
    //数据加载
    onLoad() {
        getTreeData(this.params).then(res=>{
            res = res.data;
            this.count=0;
            this.addIndex(res);
            this.traceData=res;
            this.draw();
        })
    },
    //绘制方法
    draw() {
      let parent = document.getElementById("tree-chain");
      if (parent.children[0]) {
        parent.removeChild(parent.children[0]);
      }
      parent.appendChild(
        Tree(
          this.traceData,
          { width: 1152 },
          { handleNodeClick: this.handleNodeClick }
        )
      );
    },
    //节点点击事件
    handleNodeClick(d) {
      this.count = 0;
      this.findChildren(this.traceData, d.index);
      this.addIndex(this.traceData);
      this.draw();
    },
    //寻找子节点并处理
    findChildren(d, index) {
      if (d.index != index) {
        if (d.children && d.children.length != 0) {
          for (let a of d.children) {
            this.findChildren(a, index);
          }
        }
      } else {
        if (d._children === undefined) {
          d._children = d.children;
          d.children = undefined;
        } else {
          d.children = d._children;
          d._children = undefined;
        }
      }
    },
    //添加索引
    addIndex(d) {
      d.index = this.count++;
      if (d.children && d.children.length != 0) {
        for (let a of d.children) {
          this.addIndex(a);
        }
      }
    },
  },
};
</script>

最后

新人作者,求点赞+收藏+关注,感谢大家多多支持(抱拳

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jerry24ever

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值