效果图如下:
效果图
使用的是D3的v7版本,需要具有一定svg基础。
树图布局API:
d3.layout.tree():创建一个树图布局。
tree.size():设置树图的容器的宽高。
tree.separation([separation])设置相邻节点间隔。
tree.nodes(root)根据root计算获取节点数组。
tree.links(nodes)根据nodes计算获取连线数组。
总结:根据两点绘制一条线段的结论得出,links 个数总比nodes个数 少一个。
2.节点(nodes)对象包含以下属性:
parent:父节点。
children:子节点。
depth:节点深度。
x:节点的x坐标。
y:节点的y坐标。
3.节点间连线(links)对象,包含以下属性:
source:源节点(连线的前半段节点)。
target:目标节点(连线的后半段节点)。
先把数据分成左右两组, 选中页面给页面添加svg标签;设置Svg绘制区域的宽和高;添加g元素(svg的group分组标签元素)并设置位置。
2.生成树状布局,设置树图布局容器尺寸。
3..对角线生成器,并旋转90度。
4.请求数据:
4.1获取nodes节点数组和links连线数组。
4.2生成连线。
4.3生成节点。
4.4给节点添加圆圈,设置半径。
4.5给节点添加文本,设置文本的样式位置。
import * as d3 from "d3";
import { uid } from "uid";
const children = [
{
name: "123",
},
{
name: "神鼎飞丹砂",
},
{
name: "sdfsd胜多负少的",
},
{
name: "水电费水电费是",
},
];
const treeData = {
r: {
name: "",
children: [
{
name: "xx部",
children: [...children],
},
{
name: "aa部",
children: [...children],
},
{
name: "cc部",
children: [...children],
},
],
},
l: {
name: "",
children: [
{
name: "34234",
children: [...children],
},
{
name: "vv部",
children: [...children],
},
{
name: "hh部",
children: [...children],
},
{
name: "rr部",
children: [...children],
},
],
},
};
export default {
data() {
return {
loading: false,
searchModel: {
year: {
label: `填报年份`,
type: "select",
options: [
{
value: 2022,
label: 2022,
},
{
value: 2023,
label: 2023,
},
{
value: 2024,
label: 2024,
},
],
inputProps: {
placeholder: `请选择填报年份`,
},
},
promoterName: {
label: `指标填报标题`,
type: "input",
inputProps: {
placeholder: `请输入指标填报标题`,
},
},
},
container: null, //容器svg>g
duration: 750, //动画持续时间
scaleRange: [0.2, 4], //container缩放范围
direction: ["r", "l"], //分为左右2个方向
centralPoint: [0, 0], //画布中心点坐标x,y
root: { r: {}, l: {} }, //左右2块数据源
rootNodeLength: 0, //根节点名称长度
rootName: ["束带结发你上课的今年发就开始你是的", "工作任务清单(修订版)"], //根节点名称
textSpace: 20, //多行文字间距
themeColor: "#2196F3", //主色
titleColor: "#cfe7e8", //标题色
nodeSize: [70, 150], //节点间距(高/水平)
fontSize: 16, //字体大小,也是单字所占宽高
rectMinWidth: 50, //节点方框默认最小,
textPadding: 5, //文字与方框间距,注:固定值5
circleR: 5, //圆圈半径
textWidth: 500, // 文本宽度
};
},
computed: {
treeMap() {
//树布局
return d3
.tree()
.nodeSize(this.nodeSize)
.separation((a, b) => {
let result =
a.parent === b.parent && !a.children && !b.children ? 1 : 2;
if (result > 1) {
let length = 0;
length = a.children ? length + a.children.length : length;
length = b.children ? length + b.children.length : length;
result = length / 2 + 0.5;
}
return result;
});
},
},
mounted() {
this.treeInit();
},
methods: {
search() {},
//初始化
treeInit() {
const margin = { top: 0, right: 150, bottom: 150, left: 0 };
const treeWidth = document.body.clientWidth - margin.left - margin.right; //tree容器宽
const treeHeight =
document.body.clientHeight - margin.top - margin.bottom; //tree容器高
const centralY = treeWidth / 2 + margin.left;
const centralX = treeHeight / 2 + margin.top;
this.centralPoint = [centralX, centralY]; //中心点坐标
//根节点字符所占宽度
this.rootNodeLength = this.rootName[0].length * this.fontSize + 30;
//svg标签
const svg = d3
.select("#treeRoot")
.append("svg")
.attr("class", "tree-svg")
.attr("width", treeWidth)
.attr("height", treeHeight)
.attr("font-size", this.fontSize)
.attr("fill", "#555");
//g标签
this.container = svg
.append("g")
.attr("class", "container")
.attr("transform", `translate(${margin.left},${margin.top}) scale(1)`);
//画出根节点
this.drawRoot();
//指定缩放范围
const zoom = d3
.zoom()
.scaleExtent(this.scaleRange)
.on("zoom", (e) => {
this.container.attr("transform", e.transform);
});
//动画持续时间
this.container
.transition()
.duration(this.duration)
.call(zoom.transform, d3.zoomIdentity);
svg.call(zoom);
//数据处理
this.dealData();
},
//数据处理
dealData() {
this.direction.forEach((item) => {
this.root[item] = d3.hierarchy(treeData[item]);
this.root[item].x0 = this.centralPoint[0]; //根节点x坐标
this.root[item].y0 = this.centralPoint[1]; //根节点Y坐标
this.root[item].descendants().forEach((d) => {
d._children = d.children; //添加_children属性,用于实现点击收缩及展开功能
d.id = item + uid(); //绑定唯一标识ID 随机数,用于绑定id
});
this.update(this.root[item], item);
});
},
//画根节点
drawRoot() {
const title = this.container
.append("g")
.attr("id", "rootTitle")
.attr(
"transform",
`translate(${this.centralPoint[1]},${this.centralPoint[0]})`
);
title
.append("svg:rect")
.attr("class", "rootTitle")
.attr("y", 0)
.attr("x", -this.rootNodeLength / 2)
.attr("width", this.rootNodeLength)
.attr("height", 0)
.attr("rx", 5) //圆角
.style("fill", this.titleColor);
this.rootName.forEach((name, index) => {
title
.append("text")
.attr("fill", "black")
.attr("y", index * this.textSpace - 2)
.attr("text-anchor", "middle")
.text(name);
let lineHeight = (index + 2) * this.textSpace;
//修改rootTitle rect 的高度
d3.select("#rootTitle rect")
.attr("height", lineHeight)
.attr("y", -lineHeight / 2);
});
},
//开始绘图
update(source, direction) {
const dirRight = direction === "r" ? 1 : -1; //方向为右/左
const className = `${direction}gNode`;
const tree = this.treeMap(this.root[direction]);
const nodes = tree.descendants(); //返回后代节点数组,第一个节点为自身,然后依次为所有子节点的拓扑排序
const links = tree.links(); //返回当前 node 的 links 数组, 其中每个 link 定义了 source父节点, target 子节点属性。
nodes.forEach((d) => {
//左右2部分,设置以中心点为圆点(默认左上角为远点)
d.y = dirRight * (d.y + this.rootNodeLength / 2) + this.centralPoint[1];
d.x = d.x + this.centralPoint[0];
});
//根据class名称获取左或者右的g节点,达到分块更新
const node = this.container
.selectAll(`g.${className}`)
.data(nodes, (d) => d.id);
//新增节点,tree会根据数据内的children扩展相关节点
const nodeEnter = node
.enter()
.append("g")
.attr("id", (d) => `g${d.id}`)
.attr("class", className)
.attr("transform", (d) => `translate(${source.y0},${source.x0})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0)
.on("click", (e, d) => {
d.depth !== 0 && this.clickNode(d, direction); //根节点不执行点击事件
});
nodeEnter.each((d) => {
if (d.depth > 0) {
//非根节点且无子节点
this.drawText(`g${d.id}`, dirRight, d); //画文本
this.drawRect(`g${d.id}`, dirRight); //画方框
// d3.select(`#g${d.id} rect`).attr('stroke-width',15).attr('filter',`url(#fg${d.id})`);//给rect绑定阴影
}
if (d.depth > 0 && d._children) {
//非根节点且有子节点
const width = Math.max(
d.data.name.length * (this.fontSize + 2),
this.rectMinWidth
);
let right = dirRight > 0; //右为1,左为-1
let xDistance = right ? width : -width;
//修改rect属性
d3.select(`#g${d.id} rect`)
.attr("width", width + this.textPadding * 2)
.attr("height", this.fontSize + this.textPadding * 2)
.attr("x", right ? 0 : -width)
.attr("fill", (d) => this.getTsTextColor(d.data.name))
.style("stroke", (d) => this.getTsTextColor(d.data.name));
//修改文本属性
d3.select(`#g${d.id} text`)
.attr("text-anchor", right ? "end" : "start")
.attr("font-weight", "bold")
.attr(
"x",
right
? xDistance - this.circleR + this.textPadding
: xDistance + this.circleR + this.textPadding
)
.attr("y", this.fontSize / 2)
.style("cursor", "pointer");
// //修改圆圈属性
// d3.select(`#g${d.id} g`).attr(
// "transform",
// `translate(${xDistance},0)`
// );
}
});
// 更新节点:节点enter和exit时都会触发tree更新
const nodeUpdate = node
.merge(nodeEnter)
.transition()
.duration(this.duration)
.attr(
"transform",
(d) => `translate(${d.y - (dirRight * this.rectMinWidth) / 2},${d.x})`
)
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1);
// 移除节点:tree移除掉数据内不包含的节点(即,children = false)
const nodeExit = node
.exit()
.transition()
.duration(this.duration)
.remove()
.attr("transform", (d) => `translate(${source.y},${source.x})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);
// Update the links 根据 className来实现分块更新
const link = this.container
.selectAll(`path.${className}`)
.data(links, (d) => d.target.id);
// Enter any new links at the parent's previous position.
//insert是在g标签前面插入,防止连接线挡住G节点内容
const linkEnter = link
.enter()
.insert("path", "g")
.attr("class", className)
.attr("d", (d) => {
const o = { x: source.x0, y: source.y0 };
return this.diagonal({ source: o, target: o });
})
.attr("fill", "none")
.attr("stroke-width", 2)
.attr("stroke", ({ source, target }) => {
return this.getTsTextColor(source.data.name || target.data.name);
});
// Transition links to their new position.
link
.merge(linkEnter)
.transition()
.duration(this.duration)
.attr("d", this.diagonal);
// Transition exiting nodes to the parent's new position.
link
.exit()
.transition()
.duration(this.duration)
.remove()
.attr("d", (d) => {
const o = { x: source.x, y: source.y };
return this.diagonal({ source: o, target: o });
});
// Stash the old positions for transition.
this.root[direction].eachBefore((d) => {
d.x0 = d.x;
d.y0 = d.y;
});
},
//画连接线
diagonal({ source, target }) {
let s = source,
d = target;
let direction = (s.y + d.y) / 2 > s.y ? "r" : "l";
let xDistance = this.fontSize * 2;
let lastX = d.x + this.fontSize - 1;
// console.log(
// d.data?.names,
// (d.data?.names?.length > 0 ? d.data?.names?.length - 1 : 0) *
// this.fontSize
// );
if (!d._children && d.data) {
// 叶节点
// console.log(source, target);
return `M ${s.y} ${s.x}
L ${(s.y + d.y) / 2} ${s.x},
L ${(s.y + d.y) / 2} ${lastX > s.x ? lastX - 15 : lastX + 15},
C ${(s.y + d.y) / 2} ${lastX},
${(s.y + d.y) / 2} ${lastX}
${d.y} ${lastX},
L ${
direction === "r"
? d.y + this.textWidth
: d.y - this.textWidth
} ${lastX}`;
// return `M ${s.y} ${s.x}
// C ${(s.y + d.y) / 2} ${s.x},
// ${(s.y + d.y) / 2} ${lastX},
// ${
// direction === "r"
// ? (s.y + d.y) / 2 + xDistance
// : (s.y + d.y) / 2 - xDistance + 10
// } ${lastX}
// L ${
// direction === "r" ? d.y + this.textWidth : d.y - this.textWidth
// } ${lastX}`;
} else {
return `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x},
${(s.y + d.y) / 2} ${lastX},
${
direction === "r"
? (s.y + d.y) / 2 + xDistance + 20
: (s.y + d.y) / 2 - xDistance - 10
} ${lastX}
L ${d.y} ${lastX}`;
}
},
//画文本
drawText(id, dirRight, d) {
dirRight = dirRight > 0; //右为1,左为-1
let texts = d3
.select(`#${id}`)
.append("text")
.attr("class", (d) => {
return `text-id${id} ${!d._children ? "leaf-node" : ""}`;
})
.attr("x", (d) =>
dirRight ? this.textPadding : -this.textPadding - this.textWidth
)
.attr("text-anchor", dirRight ? "start" : "start")
.style("font-size", this.fontSize)
.style("fill", (d) => {
if (!d._children && !d.children) {
//无子节点
return this.themeColor;
}
});
// 设置text文字自动换行
texts.each((item) => {
let text = this.container.selectAll("text.text-id" + id);
item.data.names = this.insertEnter(item.data.name, this.textWidth);
let x = +text.attr("x"),
y = +text.attr("y");
let lineHight = this.fontSize + 4;
if (item.data.names.length > 1) {
item.data.height = lineHight * item.data.names.length - 1 + 10;
// 需要换行的文字
for (let i = 0; i < item.data.names.length; i++) {
text
.append("tspan")
.attr("x", x)
.attr("y", (d) => {
// 计算每行字的坐标
let textX = y + lineHight * i - this.textPadding * 2;
return item.data.names.length > 4
? textX - this.textSpace
: textX;
})
.style("font-size", this.fontSize - 2)
.text((d) => {
if (i <= 2) {
if (item.data.names.length > 4 && i == 2) {
d.data.names[i] += "...";
}
return d.data.names[i];
}
});
}
} else {
text.text((d) => d.data.name);
}
});
return texts;
},
// 拆分文字字符串 (文字字符串,外框宽度)
insertEnter(name, width) {
// 文字宽度
let nameLen = name.length * this.fontSize;
// 每行字数,超过换行
let num = 5;
// 文字宽度大于rect宽度时,计算每行最大字数
// console.log("nameLen", nameLen, width, name);
if (nameLen > width) {
num = Math.floor(width / this.fontSize);
} else {
num = Math.floor(nameLen / this.fontSize);
}
if (!num) num = 1;
var s = name,
reg = new RegExp(`.{1,${num}}`, "g"),
rs = s.match(reg);
if (name.length <= num) {
return [name];
} else {
rs.push(s.substring(rs.join("").length));
}
return rs;
},
//画方框
drawRect(id, dirRight) {
let realw = document.getElementById(id).getBBox().width + 10; //获取g实际宽度后,设置rect宽度
let realh = document.getElementById(id).getBBox().height;
return d3
.select(`#${id}`)
.insert("rect", "text")
.attr("x", dirRight > 0 ? 0 : -realw)
.attr("y", (d) => {
if (!d._children) {
return 0;
} else {
return -this.textSpace + this.textPadding * 2;
}
})
.attr("width", realw)
.attr("height", realh)
.attr("rx", 2) //圆角
.attr("opacity", (d) => {
if (!d._children) {
return 0;
} else {
return 1;
}
});
// .style("stroke", "#dddddd")
},
//点击某个节点
clickNode(d, direction) {
if (!d._children && !d.children) {
//无子节点
console.log(d);
this.$router.push({
name: "accountCheckDetail",
});
return;
}
//根据当前节点是否有children来判断是展开还是收缩,true收缩,false展开
//tree会根据节点内是否有children来向下扩展
d.children = d.children ? null : d._children;
// d3.select(`#g${d.id} .node-circle .node-circle-vertical`)
// .transition()
// .duration(this.duration)
// .attr("stroke-width", d.children ? 0 : 1); //控制节点伸缩时的标识圆圈
this.update(d, direction);
},
//子文本颜色配置
getTsTextColor(name) {
switch (name) {
default:
return "#000";
}
},
},
};