d3.js实现树形结构过渡展开、折叠
d3相关api
本文为一个最简单的树形结构过渡效果展开的核心功能,没有添加连接线等逻辑
hierarchy
family = d3.hierarchy({
name: "root",
children: [
{name: "child #1"},
{
name: "child #2",
children: [
{name: "grandchild #1"},
{name: "grandchild #2"},
{name: "grandchild #3"}
]
}
]
})
hierarchy接收一个树形结构的对象,d3会根据这个树形结构生成一个新的树形结构,这个新的树形结构中有几个主要的属性
- data: 原始树形结构对应位置的数据
- parent:父级节点的引用
- depth:当前节点的层级,根节点为0
- height:当前节点到当前路径叶子节点的节点数,叶子节点的height为0
可以看到上面的图片则为新构建树形结构。hierarchy的第二个参数是一个函数,函数接收原始数据为参数,返回一个数组类型,默认为children,也就是利用children属性构建层级结构,如果我们返回其他字段,就会利用相应字段构建层级结构。
const family = hierarchy(
{
name: "root",
parents: [
{ name: "parent #1" },
{
name: "parent #2",
children: [
{ name: "grandparent#1" },
{ name: "grandparent#2" },
{ name: "grandparent#3" },
],
},
],
},
(d) => {
return d.parents;
}
);
从上图可以看出,新结构的children是利用原始数据的parents构建的
d3.tree()
根据配置给树形结构添加坐标
tree.nodeSize() 参数为一个数组,分别是每个节点的水平距离和垂直距离
transition
selection.transition().duration(毫秒过渡时间)
为当前selection节点设置属性时添加过渡效果
开始绘制
定义一个插件类DrawGraphPlugin,参数为数据data和el挂载目标对象
class DrawGraphPlugin {
constructor(config) {
this.el = config.el;
this.daddta = config.data;
}
}
在onMounted生命周期函数中调用
<template>
<div class="svgContainer"></div>
</template>
<script setup>
import { onMounted, ref } from "vue";
const data = {
name: "根节点",
children: [
{
name: "一级节点 1",
children: [{ name: "二级节点 1-1" }, { name: "二级节点 1-2" }],
},
{
name: "一级节点 2",
children: [{ name: "二级节点 2-1" }, { name: "二级节点 2-2" }],
},
],
};
onMounted(() => {
new DrawGraphPlugin({
el: ".svgContainer",
data,
});
});
</script>
开始编写绘制插件DrawGraphPlugin
初始化dom结构
initDomStructure() {
// 生成svg
const svg = create("svg")
.attr("xmlns", "http://www.w3.org/2000/svg")
.attr("height", this.gInfo.height)
.attr("width", this.gInfo.width)
.attr("viewBox", () => {
return [
-this.gInfo.width / 2,
-200,
this.gInfo.width,
this.gInfo.height,
];
})
.style("user-select", "none")
.style("cursor", "move");
this.rootContainer = svg.append("g").attr("class", "containerG");
// 添加zoom,放大、伸缩的效果
svg
.call(
zoom()
.scaleExtent([0.2, 5])
.on("zoom", () => {
const { x, y, k } = event.transform;
this.rootContainer.attr("transform", () => {
return `translate(${x},${y}) scale(${k})`;
});
})
)
// 取消双击放大的事件
.on("dblclick.zoom", null);
// 将新生成的svg添加到el节点中
select(this.el).node().appendChild(svg.node());
this.svg = svg;
}
初始化树结构数据
class DrawGraphPlugin {
constructor(config) {
...
}
// 初始化树结构数据
initTreeStructure(data) {
// 添加层级关系
const hierarchyData = hierarchy(data);
// 添加位置坐标
const descendantsData = tree().nodeSize([100, 200])(hierarchyData);
return descendantsData;
}
}
更新方法
// 绘制、更新 方法
// source为点击时记录的位置,用于从点击的位置展开或者折叠到点击的位置
draw(source) {
// 给rootContainer绑定数据
const nodesSelection = this.rootContainer
.selectAll(".itemG")
.data(this.treeData.descendants(), (d) => {
return d.data.name;
});
const that = this;
const itemG = nodesSelection.enter().append("g");
// 先移动到点击点位置
itemG.attr("class", "itemG").attr("transform", (d) => {
// 先直接移动到点击点的位置
return `translate(${source.x},${source.y})`;
});
// 再从点击点开始 动画移动到目标点
itemG
.transition()
.duration(200)
.attr("transform", function (d) {
return `translate(${d.x},${d.y})`;
});
// 添加点击事件 折叠、展开
itemG.on("click", function (d) {
// 如果存在children则需要折叠,将children设置为null,同时用_children保存
if (d.children) {
d._children = d.children;
d.children = null;
}
// 不存在children需要展开,将children恢复为_children
else {
d.children = d._children;
d._children = null;
}
// 重新绘制,将当前节点信息作为参数传入(需要当前节点的x、y坐标)
that.draw(d);
});
// 绘制节点中的内容 方块、文本
itemG
.append("rect")
.attr("width", 80)
.attr("height", 50)
.attr("stroke", "rgb(64, 137, 230)")
.attr("fill", "#fff");
itemG
.append("text")
.text((d) => d.data.name)
.attr("font-size", 12)
.attr("text-anchor", "middle")
.attr("transform", (d) => {
return `translate(${40},${30})`;
});
// 退出状态动画,将点击节点的子节点移动到点击的节点,隐藏
nodesSelection
.exit()
.transition()
.duration(200)
.attr("transform", function () {
return `translate(${source.x},${source.y})`;
})
.style("opacity", 0)
.remove();
}
完整代码
<template>
<div class="svgContainer"></div>
</template>
<script setup>
import { onMounted } from "vue";
import { create, select, tree, hierarchy, zoom, event } from "d3";
const data = {
name: "根节点",
children: [
{
name: "一级节点 1",
children: [{ name: "二级节点 1-1" }, { name: "二级节点 1-2" }],
},
{
name: "一级节点 2",
children: [{ name: "二级节点 2-1" }, { name: "二级节点 2-2" }],
},
],
};
onMounted(() => {
new DrawGraphPlugin({
el: ".svgContainer",
data,
});
});
class DrawGraphPlugin {
constructor(config) {
this.el = config.el;
this.data = config.data;
this.svg = null;
this.gInfo = {
height: document.documentElement.clientHeight,
width: document.documentElement.clientWidth,
};
this.treeData = this.initTreeStructure(this.data);
this.initDomStructure();
this.draw({ x: 0, y: 0 });
}
initDomStructure() {
// 生成svg
const svg = create("svg")
.attr("xmlns", "http://www.w3.org/2000/svg")
.attr("height", this.gInfo.height)
.attr("width", this.gInfo.width)
.attr("viewBox", () => {
return [
-this.gInfo.width / 2,
-200,
this.gInfo.width,
this.gInfo.height,
];
})
.style("user-select", "none")
.style("cursor", "move");
this.rootContainer = svg.append("g").attr("class", "containerG");
// 添加zoom,放大、伸缩的效果
svg
.call(
zoom()
.scaleExtent([0.2, 5])
.on("zoom", () => {
const { x, y, k } = event.transform;
this.rootContainer.attr("transform", () => {
return `translate(${x},${y}) scale(${k})`;
});
})
)
// 取消双击放大的事件
.on("dblclick.zoom", null);
// 将新生成的svg添加到el节点中
select(this.el).node().appendChild(svg.node());
this.svg = svg;
}
initTreeStructure(data) {
const hierarchyData = hierarchy(data);
const descendantsData = tree().nodeSize([100, 200])(hierarchyData);
return descendantsData;
}
// 绘制、更新 方法
// source为点击时记录的位置,用于从点击的位置展开或者折叠到点击的位置
draw(source) {
// 给rootContainer绑定数据
const nodesSelection = this.rootContainer
.selectAll(".itemG")
.data(this.treeData.descendants(), (d) => {
return d.data.name;
});
const that = this;
const itemG = nodesSelection.enter().append("g");
// 先移动到点击点位置
itemG.attr("class", "itemG").attr("transform", (d) => {
// 先直接移动到点击点的位置
return `translate(${source.x},${source.y})`;
});
// 再从点击点开始 动画移动到目标点
itemG
.transition()
.duration(200)
.attr("transform", function (d) {
return `translate(${d.x},${d.y})`;
});
// 添加点击事件 折叠、展开
itemG.on("click", function (d) {
// 如果存在children则需要折叠,将children设置为null,同时用_children保存
if (d.children) {
d._children = d.children;
d.children = null;
}
// 不存在children需要展开,将children恢复为_children
else {
d.children = d._children;
d._children = null;
}
// 重新绘制,将当前节点信息作为参数传入(需要当前节点的x、y坐标)
that.draw(d);
});
// 绘制节点中的内容 方块、文本
itemG
.append("rect")
.attr("width", 80)
.attr("height", 50)
.attr("stroke", "rgb(64, 137, 230)")
.attr("fill", "#fff");
itemG
.append("text")
.text((d) => d.data.name)
.attr("font-size", 12)
.attr("text-anchor", "middle")
.attr("transform", (d) => {
return `translate(${40},${30})`;
});
// 退出状态动画,将点击节点的子节点移动到点击的节点,隐藏
nodesSelection
.exit()
.transition()
.duration(200)
.attr("transform", function () {
return `translate(${source.x},${source.y})`;
})
.style("opacity", 0)
.remove();
}
}
</script>
转载自:https://juejin.cn/post/7142017273195134983