安装D3
npm install d3 --save
首先确认你使用的是vue还是Nuxt
vue
//如果使用的是vue就把代码中的注释解开,再把this.$d3替换成d3
// import * as d3 from "d3";
Nuxt
创建组件mindMap.vue
<template>
<div class="catalogue">
<div class="header">
<p>{{ objs.course_name }}</p>
<p>{{ objs.course_name_en }}</p>
</div>
<div class="d3" :style="{ height: `${d3Height + 48}px` }">
<div :id="id" class="d3-content"></div>
</div>
</div>
</template>
<script>
// import * as d3 from "d3";
export default {
props: {
objs: Object,
data: Object,
nodeWidth: {
type: Number,
default: 340,
},
nodeHeight: {
type: Number,
default: 40,
},
active: {
type: String,
default: "",
},
},
components: {},
data() {
return {
isClient: typeof window !== "undefined",
canvasWidth: 1032,
id: "TreeMap" + randomString(4),
deep: 0,
treeData: null,
show: true,
initWidth: 0,
initHeight: 500,
isPlayBtn: false,
isPlayId: "",
d3Height: 500,
isWidth: 0,
};
},
mounted() {
window.addEventListener("resize", () => {
let el = document.querySelector(".catalogue");
this.canvasWidth = el.offsetWidth;
// if (process.browser) {
// this.$nextTick(() => {
// this.drawMap();
// window.handleCustom = this.handleCustom;
// });
// }
});
if (process.browser) {
this.$nextTick(() => {
let el = document.querySelector(".catalogue");
this.canvasWidth = el.offsetWidth;
this.drawMap();
window.handleCustom = this.handleCustom;
});
}
},
methods: {
drawMap(boo) {
let that = this;
// 源数据
let data = {};
// 判断data是否为空对象
if (this.data && JSON.stringify(this.data) !== "{}") {
data = this.data;
} else {
data = this.demoData;
}
if (!this.treeData) {
this.treeData = data;
} else {
// 清空画布
this.$d3
.select("#" + this.id)
.selectAll("svg")
.remove();
}
let leafList = [];
getTreeLeaf(data, leafList);
let leafNum = leafList.length;
let TreeDeep = getDepth(data);
// 左右内边距
let mapPaddingLR = 10;
let mapPaddingTB = 0;
// 上下内边距
if (!boo) {
this.initWidth = this.nodeWidth * TreeDeep + mapPaddingLR * 2;
this.initHeight = (this.nodeHeight - 4) * leafNum + mapPaddingTB * 2;
}
let mapWidth = boo
? this.initWidth
: this.nodeWidth * TreeDeep + mapPaddingLR * 2;
// let mapWidth = this.canvasWidth;
let mapHeight = (this.nodeHeight - 4) * leafNum + mapPaddingTB * 2;
this.d3Height = mapHeight < 500 ? 500 : mapHeight;
// 定义画布—— 外边距 10px
let svgMap = this.$d3
.select("#" + this.id)
.append("svg")
.attr("width", mapWidth + 100)
.attr("height", mapHeight < 500 ? 500 : mapHeight)
.style("margin", "0px");
// Zoom functionality
const zoom = this.$d3
.zoom()
.scaleExtent([0.1, 3])
.on("zoom", (event) => {
// svgMap.attr('transform', event.transform);
const transform = event.transform;
const [x, y] = [transform.x, transform.y];
const scale = transform.k;
// Calculate the new transform to keep the tree centered
const centerX = (mapWidth / 2) * scale;
const centerY = (mapHeight / 2) * scale;
const newX = centerX - mapWidth / 2;
const newY = centerY - mapHeight / 2;
svgMap.attr(
"transform",
`translate(${newX},${newY}) scale(${scale})`
);
});
this.$d3
.select("#" + this.id)
.select("svg")
.call(zoom);
// 定义树状图画布
let treeMap = svgMap
.append("g")
.attr(
"transform",
"translate(" +
mapPaddingLR +
"," +
(mapHeight / 2 - mapPaddingTB + 50) +
")"
);
// 将源数据转换为可以生成树状图的数据(有节点 nodes 和连线 links )
let treeData = this.$d3
.tree()
// 设置每个节点的尺寸
.nodeSize(
// 节点包含后方的连接线 [节点高度,节点宽度]
[this.nodeHeight, this.nodeWidth]
)
// 设置树状图节点之间的垂直间隔
.separation(function (a, b) {
// debugger
// 样式一:节点间等间距
// return (a.parent == b.parent ? 1: 2) / a.depth;
// 样式二:根据节点子节点的数量,动态调整节点间的间距
let rate =
(a.parent == b.parent
? b.children
? b.children.length / 2
: 1
: 2) / a.depth;
// 间距比例不能小于0.7,避免间距太小而重叠
if (rate < 0.7) {
rate = 0.7;
}
return rate;
})(
// 创建层级布局,对源数据进行数据转换
this.$d3.hierarchy(data).sum(function (node) {
// 函数执行的次数,为树节点的总数,node为每个节点
return node.value;
})
);
// 贝塞尔曲线生成器
let Bézier_curve_generator = this.$d3
.linkHorizontal()
.x(function (d) {
return d.y;
})
.y(function (d) {
return d.x;
});
//绘制边
treeMap
.selectAll("path")
// 节点的关系 links
.data(treeData.links())
.enter()
.append("path")
.attr("d", function (d) {
// 根据name值的长度调整连线的起点
var start = {
x: d.source.x,
// 连线起点的x坐标
// 第1个10为与红圆圈的间距,第2个10为link内文字与边框的间距,第3个10为标签文字与连线起点的间距,60为自定义html
y:
d.source.y +
10 +
(d.source.data.title ? getPXwidth(d.source.data.title) + 10 : 0) +
getPXwidth(d.source.data.label) +
(d.source.data.detail_type == 2 ? 82 : 0),
};
// if (d.source.data.detail_type == 2) {
// that.isWidth = getPXwidth(d.source.data.title) + 200;
// }
// var end = { x: d.target.x, y: d.target.y };
var end = { x: d.target.x, y: d.target.y + that.isWidth };
return Bézier_curve_generator({ source: start, target: end });
})
.attr("fill", "none")
.attr("stroke", "#00AB6B")
// 虚线
// .attr("stroke-dasharray", "8")
.attr("stroke-width", 1);
// 创建分组——节点+文字
let groups = treeMap
.selectAll("g")
// 节点 nodes
.data(treeData.descendants())
.enter()
.append("g")
.attr("transform", function (d) {
var cx = d.x;
var cy = d.y;
// if (d.depth == 3) {
// cy = cy + 200;
// }
return "translate(" + cy + "," + cx + ")";
});
//绘制节点(节点前的圆圈)
groups
.append("circle")
// 树的展开折叠
.on("click", function (event, node) {
let data = node.data;
if (data.children) {
let isChildren = that.findByID(that.data.children, data.id);
if (isChildren.children) {
that.isPlayId = data.id;
}
data.childrenTemp = data.children;
data.children = null;
} else {
that.isPlayId = "";
data.children = data.childrenTemp;
data.childrenTemp = null;
}
that.drawMap(true);
})
.attr("cursor", "pointer")
.attr("r", 4)
.attr("fill", function (d) {
if (d.data.childrenTemp) {
return "#00AB6B";
} else {
return "white";
}
})
.attr("stroke", "#00AB6B")
.attr("stroke-width", 1);
//绘制标注(节点前的矩形)
groups
.append("rect")
.attr("x", 8)
.attr("y", -10)
.attr("width", function (d) {
return d.data.link ? getPXwidth(d.data.link) + 10 : 0;
})
.attr("height", 22)
.attr("fill", "red")
.attr("border", "blue")
// 添加圆角
.attr("rx", 4);
//绘制链接方式
groups
.append("text")
.attr("x", 12)
.attr("y", -5)
.attr("dy", 10)
.attr("fill", "white")
.attr("font-size", 14)
.text(function (d) {
return d.data.link;
});
groups
.append("foreignObject")
.attr("width", (d) => {
return (
getPXwidth(d.data.title) + 12 + (d.data.detail_type == 2 ? 82 : 0)
);
})
.attr("height", 20)
.attr("x", function (d) {
return 12 + (d.data.link ? getPXwidth(d.data.link) + 10 : 0);
})
.on("click", function (event, node) {
if (that.isPlayBtn) {
document.querySelector(".main-content").scrollTop = 0;
that.$store.dispatch("user/setCourseVideoId", node.data.id);
}
})
.attr("y", -10)
.append("xhtml:div")
.style("font", '14px "Helvetica Neue"', "line-height", "20px")
.attr("dy", ".35em")
.style("text-anchor", function(d) { return d.children ? "end" : "start"; })
.html((d) => {
let _html = `
<div class="custom-html">
<div>${d.data.title?.length > 20 ? d.data.title.slice(0, 20) + '...' : d.data.title}</div>
</div>`;
if (d.data.detail_type == 2 && that.isPlayId != d.data.id) {
_html = `
<div class="custom-html">
<div>${d.data.title?.length > 20 ? d.data.title.slice(0, 20) + '...' : d.data.title}</div>
<div class="custom-html-btn" οnclick="handleCustom(${1})"><i class="iconfont"></i>视频课</div>
</div>`;
}
return _html;
});
var tooltip = that.$d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0)
.style("position", "absolute")
groups.on("mouseover", function(event, d) {
if(d.data.title?.length > 20){
tooltip.transition()
.duration(200)
.style("opacity", 1);
tooltip.html(d.data.title)
.style("background", "#fff")
.style("padding", "4px 8px")
.style("border", "1px solid #f0f0f0")
.style("left", (event.pageX) + "px")
.style("top", (event.pageY - 28) + "px");
}
})
.on("mouseout", function(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
tooltip.html("")
});
},
redraw(svgMap) {
svgMap.attr(
"transform",
"translate(" +
this.$d3.event.translate +
")scale(" +
this.$d3.event.scale +
")"
);
},
handleCustom(data) {
this.isPlayBtn = true;
},
findByID(tree, id) {
for (let i = 0; i < tree.length; i++) {
if (tree[i].id == id) {
return tree[i];
} else if (tree[i].children) {
let result = this.findByID(tree[i].children, id);
if (result) return result;
}
}
return null;
},
},
};
// 获取树的深度
function getDepth(json) {
var arr = [];
arr.push(json);
var depth = 0;
while (arr.length > 0) {
var temp = [];
for (var i = 0; i < arr.length; i++) {
temp.push(arr[i]);
}
arr = [];
for (var i = 0; i < temp.length; i++) {
if (temp[i].children && temp[i].children.length > 0) {
for (var j = 0; j < temp[i].children.length; j++) {
arr.push(temp[i].children[j]);
}
}
}
if (arr.length >= 0) {
depth++;
}
}
return depth;
}
// 提取树的子节点,最终所有树的子节点都会存入传入的leafList数组中
function getTreeLeaf(treeData, leafList) {
// 判断是否为数组
if (Array.isArray(treeData)) {
treeData.forEach((item) => {
if (item.children && item.children.length > 0) {
getTreeLeaf(item.children, leafList);
} else {
leafList.push(item);
}
});
} else {
if (treeData.children && treeData.children.length > 0) {
getTreeLeaf(treeData.children, leafList);
} else {
leafList.push(treeData);
}
}
}
// 获取包含汉字的字符串的长度
function getStringSizeLength(string) {
//先把中文替换成两个字节的英文,再计算长度
return string.replace(/[\u0391-\uFFE5]/g, "aa").length;
}
// 生成随机的字符串
function randomString(strLength) {
strLength = strLength || 32;
let strLib = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz";
let n = "";
for (let i = 0; i < strLength; i++) {
n += strLib.charAt(Math.floor(Math.random() * strLib.length));
}
return n;
}
// 获取字符串的像素宽度
function getPXwidth(str, fontSize = "14px", fontFamily = "Microsoft YaHei") {
var span = document.createElement("span");
var result = {};
result.width = span.offsetWidth;
result.height = span.offsetHeight;
span.style.visibility = "hidden";
span.style.fontSize = fontSize;
span.style.fontFamily = fontFamily;
span.style.display = "inline-block";
document.body.appendChild(span);
if (typeof span.textContent != "undefined") {
span.textContent = str;
} else {
span.innerText = str;
}
result.width = parseFloat(window.getComputedStyle(span).width) - result.width;
// 字符串的显示高度
// result.height = parseFloat(window.getComputedStyle(span).height) - result.height;
return result.width;
}
</script>
<style lang="scss" scoped>
.custom-icon {
position: absolute;
width: 20px;
height: 20px;
background-size: cover;
}
.catalogue {
.header {
p {
text-align: center;
&:nth-child(1) {
height: 32px;
font-family: Alibaba-PuHuiTi-B;
font-weight: bold;
font-size: 24px;
color: #000000d9;
line-height: 32px;
margin-top: 56px;
}
&:nth-child(2) {
font-family: AlibabaPuHuiTi-Regular;
font-size: 16px;
color: #191b2973;
line-height: 24px;
margin-bottom: 35px;
margin-top: 8px;
}
}
}
.d3 {
background: #fafafa;
position: relative;
overflow: hidden;
width: calc(100%);
min-height: 500px;
overflow: scroll;
padding: 24px;
.d3-content {
position: relative;
width: max-content;
::v-deep .custom-html {
display: flex;
div {
color: #000000d9;
// width: 200px;
// overflow: hidden; /* 确保超出容器的内容被裁剪 */
// white-space: nowrap; /* 确保文本在一行内显示 */
// text-overflow: ellipsis; /* 超出部分显示省略号 */
i {
font-size: 12px;
margin-right: 4px;
}
&:nth-child(2) {
margin-left: 10px;
background: #f2faf7;
border: 0.5px solid #c3e7da;
border-radius: 4px;
color: #00ab6b;
font-size: 12px;
padding: 0 4px;
height: 20px;
cursor: pointer;
}
}
}
}
}
}
</style>
引用
<MindMap
:data="dataList.course_knowledge_map"
:objs="dataList"
/>
import MindMap from "@/pages/web_site_front/resource_manage/pages/onlineCourses/courseTrain/mindMap.vue";
JSON数据
let dataList={
"title": "满汉全席(金玉满堂)",
"children": [
{
"id": 2116,
"show_id": "MC1811210229176336384",
"detail_type": 1,
"title": "(一)亲藩宴",
"children": [
{
"id": 2117,
"show_id": "MC1811215670698573824",
"detail_type": 1,
"title": "到峰点心",
"children": [
{
"id": 1074,
"show_id": "MC1811221498663006208",
"detail_type": 2,
"title": "1.1白玉奶茶",
"children": null
},
{
"id": 1077,
"show_id": "MC1811216950540107776",
"detail_type": 2,
"title": "1.3香酥苹果",
"children": [
{
"id": 2118,
"show_id": "MC1811218179148218368",
"detail_type": 3,
"title": "苹果",
"children": null
}
]
},
{
"id": 1785,
"show_id": "MC1811217195239997440",
"detail_type": 2,
"title": "1.4茶食刀切",
"children": null
},
{
"id": 1080,
"show_id": "MC1811218269577408512",
"detail_type": 2,
"title": "1.5杏仁佛手",
"children": [
{
"id": 2119,
"show_id": "MC1811218413282656256",
"detail_type": 3,
"title": "佛手",
"children": null
},
{
"id": 2120,
"show_id": "MC1811218443401949184",
"detail_type": 3,
"title": "杏仁",
"children": null
}
]
}
]
},
{
"id": 2121,
"show_id": "MC1813039696538333184",
"detail_type": 1,
"title": "麻婆豆腐(1)",
"children": [
{
"id": 2041,
"show_id": "MC1813039820798779392",
"detail_type": 2,
"title": "麻椒",
"children": null
},
{
"id": 2042,
"show_id": "MC1813039909135015936",
"detail_type": 2,
"title": "辣椒",
"children": null
},
{
"id": 2043,
"show_id": "MC1813039979620294656",
"detail_type": 2,
"title": "香油",
"children": null
}
]
}
]
},
{
"id": 2122,
"show_id": "MC1811214759129509888",
"detail_type": 1,
"title": "(二)延臣宴",
"children": [
{
"id": 2123,
"show_id": "MC1811214988734099456",
"detail_type": 1,
"title": "乾果四品",
"children": [
{
"id": 1085,
"show_id": "MC1811215309669658624",
"detail_type": 2,
"title": "2.1蜂蜜花生",
"children": [
{
"id": 2124,
"show_id": "MC1812661517231460352",
"detail_type": 3,
"title": "蜜蜂",
"children": null
}
]
},
{
"id": 1086,
"show_id": "MC1811219711507820544",
"detail_type": 2,
"title": "2.2 苹果软糖",
"children": null
},
{
"id": 1421,
"show_id": "MC1811572626625839104",
"detail_type": 2,
"title": "2.3乾隆白菜",
"children": null
}
]
}
]
},
{
"id": 2125,
"show_id": "MC1811318349517012992",
"detail_type": 1,
"title": "(三)万寿宴",
"children": [
{
"id": 2126,
"show_id": "MC1811318410309255168",
"detail_type": 1,
"title": "万",
"children": [
{
"id": 2127,
"show_id": "MC1811318467976732672",
"detail_type": 1,
"title": "寿",
"children": [
{
"id": 1476,
"show_id": "MC1811593045005209600",
"detail_type": 2,
"title": "增加时长看进度条",
"children": null
},
{
"id": 1412,
"show_id": "MC1811395246259097600",
"detail_type": 2,
"title": "我测试用",
"children": null
}
]
}
]
}
]
},
{
"id": 2128,
"show_id": "MC1811660317128282112",
"detail_type": 1,
"title": "(四)千叟宴",
"children": [
{
"id": 1758,
"show_id": "MC1812680201266126848",
"detail_type": 2,
"title": "鲟龙鱼汤",
"children": null
}
]
},
{
"id": 2129,
"show_id": "MC1811660398778798080",
"detail_type": 1,
"title": "(五)节令宴",
"children": [
{
"id": 2130,
"show_id": "MC1812758268982456320",
"detail_type": 1,
"title": "1",
"children": [
{
"id": 2131,
"show_id": "MC1812758643072430080",
"detail_type": 1,
"title": "2",
"children": [
{
"id": 2132,
"show_id": "MC1812758669962108928",
"detail_type": 1,
"title": "3",
"children": [
{
"id": 1856,
"show_id": "MC1812759373250424832",
"detail_type": 2,
"title": "1111",
"children": null
}
]
},
{
"id": 2133,
"show_id": "MC1813050911662628864",
"detail_type": 1,
"title": "3.1",
"children": [
{
"id": 2107,
"show_id": "MC1813051162096136192",
"detail_type": 2,
"title": "333333",
"children": null
}
]
}
]
}
]
}
]
}
]
}