<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
body, html {
margin: 0;
height: 100%;
width: 100%;
}
.node {
font: 12px sans-serif;
}
.link {
fill: none;
stroke: #cccccc;
stroke-width: 1.5px;
}
.textShadow {
text-shadow: 5px 5px 3px #000000;
}
div {
position: absolute;
top: 10px;
left: 10px;
}
</style>
</head>
<body>
<div data-index="test">
<button onclick="changeTheLine('C')">曲线</button>
<button onclick="changeTheLine('L')">直线</button>
<button onclick="changeLayout('left')">左布局</button>
<button onclick="changeLayout('right')">右布局</button>
<button onclick="changeLayout('top')">上布局</button>
<span>点击树节点文字可标注父级路径</span>
</div>
<script src="d3.v6.js"></script>
<script>
let doc = document
let w = doc.documentElement.clientWidth / 2;
let h = doc.documentElement.clientHeight;
let sourceY = 0;
let text = null;
let node = null;
let link = null;
let line = null;
let firstText = null; // 第一级文字
let textWidthOrHeight = 0;
let rememberShape = 'C'; // C = 曲线/L = 直线
let rememberLayout = 'right'; // left = 左布局/right = 右布局/top = 上布局
let paddingY = 50;
let strokeLength = 150; // 线条长度
let path = null
/**
* 改变线条及文字坐标
**/
function updateLink() {
link.data(line)
.enter()
.append('path')
.attr('class', 'link')
.merge(link)
.attr('d', function (d, i) {
// 生成标注线
if (d.target.taggingArr) {
for (let i of d.target.taggingArr) {
// !link._groups[0][i].line 避免重复克隆dom
if (!link._groups[0][i].line) {
link._groups[0][i].line = true
path = doc.querySelector('g').appendChild(link._groups[0][i].cloneNode(true));
path.style.stroke = '#000000';
path.setAttribute('data-index', `index${i}`);
}
}
}
d.target.widthOrHeight = text._groups[0][i + 1].getBBox()[rememberLayout === 'top' ? 'height' : 'width'];
return lineShape(d, node._groups[0][i + 1]);
});
}
/**
* 改变线条
* @param {String} shape - C = 曲线/L = 直线
* @see removePath
* @see updateLink
**/
function changeTheLine(shape = rememberShape) {
rememberShape = shape; // 新赋值曲线
removePath({removeAll: true});
updateLink();
}
/**
* 改变布局
* @param {String} layout - left = 左布局/right = 右布局/top = 上布局
* @see removePath
* @see updateLink
* @see firstTextTranslate
**/
function changeLayout(layout = rememberLayout) {
removePath({removeAll: true});
rememberLayout = layout; // 新赋值布局方向
updateLink();
firstTextTranslate(layout)
}
/**
* 改变线条及布局,方法调用
* @see topLayout
* @see leftLayout
* @see rightLayout
* @param {Object} d - d3返回的树形数据
* @param {HTMLElement} g - 文字的g标签
**/
function lineShape(d, g) {
textWidthOrHeight = d.target.parent.widthOrHeight ? d.target.parent.widthOrHeight : d.target.widthOrHeight;
return this[`${rememberLayout}Layout`](d, g, textWidthOrHeight);
}
/**
* 上布局
* @param {Object} d - d3返回的树形数据
* @param {HTMLElement} g - 文字的g标签
* @param {Number} textHeight - 文字高度
**/
function topLayout(d, g, textHeight) {
sourceY = d.source.y + paddingY;
if (d.source.depth === 0) {
d.target.M0 = sourceY;
} else {
d.target.M0 = d.target.parent.C4 + textHeight;
}
d.target.C4 = d.target.M0 + strokeLength;
d.target.C0 = d.target.M0 + strokeLength / 2;
d.target.C2 = d.target.M0 + strokeLength / 2;
g.setAttribute('transform', `translate(${d.target.x - (g.getBBox().width / 2)},${(d.target.C4 + textHeight / 2) + 2})`);
return `M${d.source.x},${d.target.M0} ${rememberShape}${d.source.x},${d.target.C0} ${d.target.x},${d.target.C2} ${d.target.x},${d.target.C4}`;
}
/**
* 左布局
* @param {Object} d - d3返回的树形数据
* @param {HTMLElement} g - 文字的g标签
* @param {Number} textWidth - 文字宽度
**/
function leftLayout(d, g, textWidth) {
sourceY = d.source.y + paddingY;
if (d.source.depth === 0) {
d.target.M0 = sourceY;
} else {
d.target.M0 = d.target.parent.L4 + textWidth;
}
d.target.L4 = d.target.M0 + strokeLength;
d.target.L0 = d.target.M0 + strokeLength / 2;
d.target.L2 = d.target.M0 + strokeLength / 2;
g.setAttribute('transform', `translate(${d.target.L4},${d.target.x})`);
return `M${d.target.M0},${d.source.x} ${rememberShape}${d.target.L0},${d.source.x} ${d.target.L2},${d.target.x} ${d.target.L4},${d.target.x}`;
}
/**
* 右布局
* @param {Object} d - d3返回的树形数据
* @param {HTMLElement} g - 文字的g标签
* @param {Number} textWidth - 文字宽度
**/
function rightLayout(d, g, textWidth) {
sourceY = (w - d.source.depth) - paddingY;
if (d.source.depth === 0) {
d.target.M0 = sourceY;
} else {
d.target.M0 = d.target.parent.L4 - textWidth;
}
d.target.L4 = d.target.M0 - strokeLength;
d.target.L0 = d.target.M0 - strokeLength / 2;
d.target.L2 = d.target.M0 - strokeLength / 2;
d.target.translateX = d.target.L4 - d.target.widthOrHeight;
g.setAttribute('transform', `translate(${d.target.translateX},${d.target.x})`);
return `M${d.target.M0},${d.source.x} ${rememberShape}${d.target.L0},${d.source.x} ${d.target.L2},${d.target.x} ${d.target.L4},${d.target.x}`;
}
let count = 1;
/**
* 只设置第一级文字坐标
* @param {String} layout - left = 左布局/right = 右布局/top = 上布局
**/
function firstTextTranslate(layout = rememberLayout) {
firstText.attr('transform', function (d) {
if (layout === 'right') {
return `translate(${w - d.depth * 100 - paddingY},${d.x})`
} else if (layout === 'left') {
return `translate(${d.y + paddingY - firstText.node().getBBox().width - 1},${d.x})`
} else if (layout === 'top') {
return `translate(${d.x - firstText.node().getBBox().width / 2},${d.y + paddingY - firstText.node().getBBox().height / 2})`
}
});
}
/**
* 递归父级
**/
function find(data) {
let arr = [];
return new Promise(resolve => {
function f(val) {
if (!val.parent) {
return resolve(arr)
}
arr.push(val.index);
if (val.parent) {
f(val.parent)
}
}
return f(data)
})
}
/**
* 搜集当前点击节点下的子节点的所有 taggingArr
* @param {Array} data - 子节点
**/
function changeTagging(data) {
let childs = []
return new Promise(resolve => {
function f(data) {
for (let item of data) {
if (item.taggingArr) {
childs.push(...item.taggingArr)
item.taggingArr = null
}
if (item.children) {
f(item.children)
}
}
}
f(data)
return resolve(childs)
})
}
/**
* 删除标注线dom
* @param {Array} indexArr - 要删除的标注线data-index
* @param {Boolean} removeAll - 删除所有的标注线
**/
function removePath({removeArr, removeAll = false}) {
let lineDom = null
let textDom = null
if (typeof removeAll === 'boolean' && removeAll) {
// 删除标注线
lineDom = doc.querySelectorAll(`path[data-index]`)
for (let item of lineDom) {
item.remove()
}
// 删除text选中的class
textDom = doc.querySelectorAll('.textShadow')
for (let item of textDom) {
item.classList.remove('textShadow')
}
} else if (removeArr.length > 0) {
for (let i of removeArr) {
link._groups[0][i].line = false
lineDom = doc.querySelector(`path[data-index=index${i}]`)
if (lineDom) {
lineDom.remove()
}
}
}
}
// 集群
let cluster = d3.cluster()
.size([w, h])
.separation(function (a, b) {
return (a.parent === b.parent ? 1 : 2)
});
// 树形
let tree = d3.tree()
.size([w, h])
.separation((a, b) => {
return (a.parent === b.parent ? 1 : rememberLayout === 'top' ? 1 : 2)
});
d3.json('city.json').then(root => {
let hierarchyData = d3.hierarchy(root);
let svg = d3.select("body")
.append("svg")
.attr("width", '100%')
.attr("height", '100%')
.attr('viewBox', `0 0 ${w} ${h}`);
let g = svg.append('g');
let zoom = d3.zoom()
// .scaleExtent([0, 1])
.on('zoom', function ({transform}) {
g.attr("transform", transform);
});
svg.call(zoom);
// 初始放大比例
// zoom.scaleTo(svg,0.9);
let treeData = tree(hierarchyData);
let nodes = treeData.descendants();
line = treeData.links();
node = g.selectAll('.node')
.data(nodes, function (d, i) {
d.index = i - 1;
return i
})
.enter()
.append('g')
.classed('node', true)
.on('click', async function (ev, current) {
// 重复点击选中的节点
if (current.taggingArr) {
this.querySelector('text').classList.remove('textShadow')
removePath({removeArr: current.taggingArr})
current.taggingArr = null
} else {
// xx.line为true,表示被克隆过
if (link._groups[0][current.index].line) {
let val = [...new Set(await changeTagging(current.children))]
if (val.length) {
let textDom = null
let arr = []
current.taggingArr = []
for (let item of val) {
// 比当前index大的用于删除dom,比当前index小的保留
if (item > current.index) {
// 删除text选中的class
textDom = node._groups[0][item + 1].querySelector('.textShadow')
if (textDom) {
textDom.classList.remove('textShadow')
}
arr.push(item)
} else {
// 保留的节点
current.taggingArr.push(item)
}
}
removePath({removeArr: arr})
}
}
// xx.line为false,表示为新节点
else {
current.taggingArr = await find(current)
}
}
// 更新
updateLink()
});
text = node.append('text')
.attr('dy', 3)
.text(function (d, i) {
return d.data.name
})
.on('click', function () {
this.classList.add('textShadow')
});
firstText = g.select('.node');
firstTextTranslate();
link = g.selectAll('.link')
.data(line)
.enter()
.append('path')
.attr('class', 'link')
.attr('d', function (d, i) {
d.target.widthOrHeight = text._groups[0][i + 1].getBBox()[rememberLayout === 'top' ? 'height' : 'width'];
return lineShape(d, node._groups[0][i + 1]);
})
})
</script>
</body>
</html>
Copy the code
Data format: {“name”: “region “, “children”: [” {}”]