树形图,节点是矩形。 文字长度过长,省略号展示,点击有 选中状态
<template>
<el-card style="margin-top: 10px;">
<div class="tree-container">
<ul class="controls">
<li @click="onMagnifySVG">放大</li>
<li @click="onShrinkSVG">缩小</li>
<li @click="onResetSVG">重置</li>
</ul>
<div ref="tree" class="tree-view"></div>
</div>
</el-card>
</template>
<script>
import * as d3 from "d3";
export default {
name: "D3TreeView",
props: {
productInfo: {
type: Object,
required: true
}
},
data() {
return {
dataset: {
name: "中国",
children: [
{
name: "浙江",
children: [
{ name: "杭州" },
{ name: "宁波" },
{ name: "温州" },
{ name: "绍兴" }
]
},
{
name: "广西",
children: [
{
name: "桂林",
children: [
{ name: "秀峰区" },
{ name: "叠彩区" },
{ name: "象山区" },
{ name: "七星区" }
]
},
{ name: "南宁" },
{ name: "柳州" },
{ name: "防城港" }
]
},
{
name: "黑龙321231江",
children: [
{ name: "哈尔滨" },
{ name: "齐齐哈尔" },
{ name: "牡丹江" },
{ name: "大庆" }
]
},
{
name: "新疆",
children: [
{ name: "乌鲁木齐" },
{ name: "克拉玛依" },
{ name: "吐鲁番" },
{ name: "哈密" }
]
}
]
},
svg: null,
g: null,
root: null,
zoomHandler: null,
width: 0,
height: 0,
duration: 750,
rectWidth: 100,
rectHeight: 50
};
},
mounted() {
this.$nextTick(() => {
this.initTree();
})
},
methods: {
// 初始化树形图
initTree() {
const winH = window.innerHeight - 64 - 100
this.width = this.$refs.tree.clientWidth;
this.height = this.$refs.tree.clientHeight || winH;
// 创建 SVG 容器
this.svg = d3
.select(this.$refs.tree)
.append("svg")
.attr("width", this.width)
.attr("height", this.height);
// 添加 g 容器,用于保存树节点
this.g = this.svg
.append("g")
.attr("transform", `translate(${this.width / 2 - 200},${this.height / 2}) scale(1)`);
// .attr("transform", `translate(${this.width / 2}, 40)`);
// 设置缩放功能
this.zoomHandler = d3
.zoom()
.scaleExtent([0.5, 5])
.on("zoom", (event) => {
// 使用 event 传递事件对象
this.g.attr("transform", event.transform);
});
// 应用缩放功能
this.svg
.call(this.zoomHandler)
// .call(this.zoomHandler.translateBy, this.width / 2, 40)
.call(this.zoomHandler.translateBy, this.width / 2 - 200, this.height / 2)
.on("dblclick.zoom", null); // 禁用双击缩放
this.processData();
},
// 处理数据并生成节点层级
processData() {
this.root = d3.hierarchy(this.dataset);
this.root.descendants().forEach((d) => {
d._children = d.children; // 折叠所有子节点
d.id = this.generateUUID(); // 绑定唯一 ID
});
this.updateTree(this.root);
},
// 更新树形结构
updateTree(source) {
const treeLayout = d3
.tree()
.nodeSize([100, 200])
.separation((a, b) => (a.parent === b.parent && !a.children && !b.children ? 1 : 2))(this.root);
// 处理节点
const nodes = this.g
.selectAll("g.node")
.data(this.root.descendants(), (d) => d.id);
// 新增节点
const nodeEnter = nodes
.enter()
.append("g")
.attr("id", d => `g${d.id}`) // 手动为每个 g 节点添加 id
.attr("class", "node")
.attr("transform", () => `translate(${source.y0 || 0},${source.x0 || 0})`)
.attr("opacity", 0)
.on("click", (d) => this.toggleChildren(d));
nodeEnter.each((d) => {
this.drawRect(d)
this.drawText(d);
});
// 移除旧节点
nodes
.exit()
.transition()
.duration(this.duration)
.attr("transform", () => `translate(${source.y},${source.x})`)
.remove()
.attr("opacity", 0);
// 更新节点位置
nodes
.merge(nodeEnter)
.transition()
.duration(this.duration)
.attr("transform", (d) => `translate(${d.y - 16},${d.x})`)
.attr("opacity", 1);
// 处理连接线
const links = this.g
.selectAll("path.link")
.data(this.root.links(), (d) => d.target.id);
// 新增连接线
const linkEnter = links
.enter()
.insert("path", "g")
.attr("class", "link")
.attr("d", () => {
const o = { x: source.x0 || 0, y: source.y0 || 0 };
return this.diagonal({ source: o, target: o });
})
.attr("fill", "none")
.attr("stroke-width", 1)
.attr("stroke", "#dddddd");
// 移除旧连接线
links
.exit()
.transition()
.duration(this.duration)
.remove()
.attr("d", () => {
const o = { x: source.x, y: source.y };
return this.diagonal({ source: o, target: o });
});
// 更新连接线位置
links.merge(linkEnter).transition().duration(this.duration).attr("d", this.diagonal);
// 记录当前位置
this.root.each((d) => {
d.x0 = d.x;
d.y0 = d.y;
});
},
// 生成连接线的路径
diagonal({ source, target }) {
return `M ${source.y} ${source.x}
L ${(source.y + target.y) / 2} ${source.x},
L ${(source.y + target.y) / 2} ${target.x},
${target.y} ${target.x}`;
},
// 生成 UUID
generateUUID() {
return `${this.s4()}${this.s4()}-${this.s4()}-${this.s4()}-${this.s4()}-${this.s4()}${this.s4()}${this.s4()}`;
},
s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
},
// 切换子节点的展开状态
toggleChildren(d) {
if (!d.children && !d._children) return;
d.children = d.children ? null : d._children;
this.updateTree(d);
},
drawRect(d) {
const { rectWidth, rectHeight } = this
const rect = d3.select(`#g${d.id}`)
.append("rect")
.attr("x", -rectWidth / 2 -10)
.attr("y", -rectHeight / 2)
.attr("width", rectWidth)
.attr("height", rectHeight)
.style("cursor", "pointer")
.style("fill", "#eff3fd")
.style("stroke", "#FFF")
.style("stroke-width", "1px")
.style("box-shadow", "0 5px 12.6px 0 rgba(114, 128, 160, 0.13)")
.style("rx", "5px") // 添加圆角
.style("ry", "5px") // 添加圆角
.on("click", function () {
// 移除其他节点的 active 样式
d3.selectAll("rect").classed("active", false);
// 为当前节点添加 active 样式
d3.select(this).classed("active", true);
});
// 添加或移除 active 样式
rect.classed("active", d.active);
},
// 绘制节点文字
drawText(d) {
const { rectWidth } = this
const padding = 15; // 留白宽度
const charWidth = 14; // 中文字符宽度
const maxChars = Math.floor((rectWidth - padding) / charWidth); // 最大可展示字符数
let displayName = d.data.name;
if (d.data.name.length > maxChars) {
// 截取文本并添加省略号
displayName = `${d.data.name.substring(0, maxChars)}...`;
}
d3.select(`#g${d.id}`)
.append("text")
.attr("x", -rectWidth / 2 )
.attr("y", 5)
.attr("fill", "#858ea8;")
.style("cursor", "pointer")
.text(displayName)
.append("title")
.text(`${d.data.name}`);
},
// 放大
onMagnifySVG() {
this.zoomHandler.scaleBy(this.svg, 1.1);
},
// 缩小
onShrinkSVG() {
this.zoomHandler.scaleBy(this.svg, 0.9);
},
// 重置
onResetSVG() {
this.svg.remove();
this.initTree();
}
}
};
</script>
<style scoped>
.tree-container {
width: 100%;
height: 100%;
border: 1px solid #dcdcdc;
position: relative;
}
.tree-view {
width: 100%;
height: 100%;
}
.main-title {
font-size: 16px;
color: #333;
font-weight: 600;
margin-bottom: 20px;
}
.controls {
list-style: none;
padding: 0;
margin: 0;
position: absolute;
top: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.8);
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.controls li {
padding: 5px 10px;
cursor: pointer;
border-bottom: 1px solid #ddd;
}
.controls li:last-child {
border-bottom: none;
}
.controls li:hover {
background: #f5f5f5;
}
</style>
<style>
rect.active {
stroke: #3388fc !important;
stroke-width: 2px !important;
}
</style>