用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>
最后
新人作者,求点赞+收藏+关注,感谢大家多多支持(抱拳