// 嗯,这是最初的数据。
var treeData = {
"name": "心血管中心",
"children": [
{
"name": "浙江CDC",
"children": [
{
"name": "杭州",
"children": [
{
"name": "东湖南路社区服务中心",
"children": [
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
}
]
},
{
"name": "解放北路社区服务中心",
"children": [
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
}
]
},
{
"name": "东湖南路社区服务中心",
"children": [
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
}
]
},
{
"name": "解放北路社区服务中心",
"children": [
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
}
]
},
{
"name": "解放北路社区服务中心",
"children": [
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
}
]
},
{
"name": "东湖南路社区服务中心",
"children": [
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
}
]
},
{
"name": "解放北路社区服务中心",
"children": [
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
}
]
}
]
},
{
"name": "宁波",
"children": [
{
"name": "东湖南路社区服务中心",
"children": [
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
}
]
},
{
"name": "解放北路社区服务中心",
"children": [
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
}
]
},
{
"name": "东湖南路社区服务中心",
"children": [
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
}
]
},
{
"name": "解放北路社区服务中心",
"children": [
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
}
]
},
{
"name": "解放北路社区服务中心",
"children": [
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
}
]
},
{
"name": "东湖南路社区服务中心",
"children": [
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
}
]
},
{
"name": "解放北路社区服务中心",
"children": [
{
"name": "张医生"
},
{
"name": "李医生"
},
{
"name": "李医生"
}
]
}
]
}
]
}
]
}
;;
/********************* 1. 初始化D3基本能力(设置宽高等) *********************/
// 设置图表的宽高和Margin
var margin = {
top: 20,
right: 90,
bottom: 30,
left: 90
},
width = 1220 - margin.left - margin.right,
height = 900 - margin.top - margin.bottom;
// 这里我们将svg元素,和子group元素拆分
var svg = d3
.select("body")
// 在页面的body里添加svg对象
.append("svg")
// 设置svg宽高
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom);
// 子group元素存为view变量
var view = svg
// 在svg里添加group元素
.append("g")
// 将group放置在左上方
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// 创建zoom操作
var zoom = d3
.zoom()
// 设置缩放区域为0.1-100倍
.scaleExtent([0.1, 100])
.on("zoom", () => {
// 子group元素将响应zoom事件,并更新transform状态
view.attr(
"transform",
"translate(" +
(d3.event.transform.x + margin.left) +
"," +
(d3.event.transform.y + margin.top) +
") scale(" +
d3.event.transform.k +
")"
);
});
// svg层绑定zoom事件,同时释放zoom双击事件
svg.call(zoom).on("dblclick.zoom", () => {});
var i = 0,
duration = 750,
root;
// 定义菜单选项
var userMenuData = [
[{
text: "菜单1",
func: function () {
// id为节点id
var id = Number($(this).attr("id"))
alert("菜单1", id)
}
},
{
text: "菜单2",
func: function () {
var id = Number($(this).attr("id"))
alert("菜单1", id)
}
}
]
];
// 事件监听方式添加事件绑定
// $("body").smartMenu(userMenuData, {
// name: "chatRightControl",
// container: "g.node"
// });
initData();
/********************* 2. 数据初始化绑定(包括数据更新) *********************/
function initData() {
// 计算父节点、字节点、高度和深度(parent, children, height, depth)
root = d3.hierarchy(treeData, function (d) {
return d.children;
});
// 设置第一个元素的初始位置
root.x0 = height / 2;
root.y0 = 0;
// 第二层以上元素收起
//root.children.forEach(collapse);
// 更新节点状态
updateChart(root);
}
/********************* 3. 数据更新绑定 *********************/
function updateChart(source) {
let levelWidth = [1];
let childCount = function (level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function (d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
let newHeight = d3.max(levelWidth) * 25; // 25 pixels per line
// 大致计算需要放大的倍数
var scale =
(getDepth(root) / 8 || 0.5) +
(getMax(root) / 12 || 0.5);
// 定义Tree层级,并设置宽高
var treemap = d3.tree().size([height * scale, width]);
// 设置节点的x、y位置信息
var treeData = treemap(root);
// 计算新的Tree层级
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// 设置每个同级节点间的y间距为180
nodes.forEach(function (d) {
d.y = d.depth * 180;
});
// node交互和绘制
updateNodes(source, nodes);
// link交互和绘制
updateLinks(source, links);
// 为动画过渡保存旧的位置
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
/********************* 4. node交互和绘制 *********************/
function updateNodes(source, nodes) {
// 给节点添加id,用于选择集索引
var node = view.selectAll("g.node").data(nodes, function (d) {
return d.id || (d.id = ++i);
});
// 添加enter操作,添加类名为node的group元素
var nodeEnter = node
.enter()
.append("g")
.attr("class", "node")
.attr("id", d => d.id)
// 默认位置为当前父节点的位置
.attr("transform", function (d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
// 添加mouseover事件
.on("mouseover", d => {
// 从d3.event获取鼠标的位置
var transform = d3.event;
var yPosition = transform.offsetY + 20;
var xPosition = transform.offsetX + 20;
// 将浮层位置设置为鼠标位置
var chartTooltip = d3
.select(".chartTooltip")
.style("left", xPosition + "px")
.style("top", yPosition + "px");
// 更新浮层内容
chartTooltip.select(".name").text(d.data.name);
// 移除浮层hidden样式,展示浮层
chartTooltip.classed("hidden", false);
})
// 添加mouseover事件
.on("mouseout", () => {
// 添加浮层hidden样式,隐藏浮层
d3.select(".chartTooltip").classed("hidden", true);
})
// 给每个新加的节点绑定click事件
.on("click", click)
// 给每个新加的节点绑定dbclick事件
.on("dblclick", dblclick);
// 给每个新加的group元素添加cycle元素
nodeEnter
.append("circle")
.attr("class", "node")
.attr("r", 1e-6)
// 如果元素有子节点,且为收起状态,则填充浅蓝色
.style("fill", function (d) {
return d._children ? "lightsteelblue" : d.data.children ? "#fff" : "#aaa";
});
// 给每个新加的group元素添加文字说明
nodeEnter
.append("text")
.attr("dy", ".35em")
.attr("x", function (d) {
return d.children || d._children ? -13 : 13;
})
.attr("text-anchor", function (d) {
return d.children || d._children ? "end" : "start";
})
.text(function (d) {
return d.data.name;
});
// 获取update集
var nodeUpdate = nodeEnter.merge(node);
// 设置节点的位置变化,添加过渡动画效果
nodeUpdate
.transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + d.y + "," + d.x + ")";
});
// 更新节点的属性和样式
nodeUpdate
.select("circle.node")
.attr("r", 10)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : d.data.children ? "#fff" : "#aaa";
})
.attr("cursor", "pointer");
// 获取exit操作
var nodeExit = node
.exit()
// 添加过渡动画
.transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + source.y + "," + source.x + ")";
})
// 移除元素
.remove();
// exit集中节点的cycle元素尺寸变为0
nodeExit.select("circle").attr("r", 1e-6);
// exit集中节点的text元素可见度降为0
nodeExit.select("text").style("fill-opacity", 1e-6);
}
/********************* 5. link交互和绘制 *********************/
function updateLinks(source, links) {
// 更新数据
var link = view.selectAll("path.link").data(links, function (d) {
return d.id;
});
// 添加enter操作,添加类名为link的path元素
var linkEnter = link
.enter()
.insert("path", "g")
.attr("class", "link")
// 默认位置为当前父节点的位置
.attr("d", function (d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal(o, o);
});
// 获取update集
var linkUpdate = linkEnter.merge(link);
// 更新添加过渡动画
linkUpdate
.transition()
.duration(duration)
.attr("d", function (d) {
return diagonal(d, d.parent);
});
// 获取exit集
var linkExit = link
.exit()
// 设置过渡动画
.transition()
.duration(duration)
.attr("d", function (d) {
var o = {
x: source.x,
y: source.y
};
return diagonal(o, o);
})
// 移除link
.remove();
}
/********************* 6. 单击节点事件处理 *********************/
// 当点击时,切换children,同时用_children来保存原子节点信息
function click(d) {
if (d._clickid) {
// 若在200ms里面点击第二次,则不做任何操作,清空定时器
clearTimeout(d._clickid);
d._clickid = null;
} else {
// 首次点击,添加定时器,350ms后进行toggle
d._clickid = setTimeout(() => {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
updateChart(d);
d._clickid = null;
}, 350);
}
}
/********************* 7. 双击获取子节点事件处理 *********************/
// 将获取到的节点,添加进data对象中,同时若已获取过不再获取
function dblclick(d) {
// 若无d.data.children,则视为未获取
if (!(d.data && d.data.children)) {
// 这里模拟请求,1.json - 5.json 随机获取数据
var randomNum = Math.floor(Math.random() * 5) + 1;
// 给子节点绑定父节点
var children = {
"name": "Test 1",
"children": [
{
"name": "解放北路社区服务中心A" + randomNum
},
{
"name": "解放北路社区服务中心B" + randomNum
},
{
"name": "解放北路社区服务中心C" + randomNum
},
{
"name": "解放北路社区服务中心D" + randomNum
},
{
"name": "解放北路社区服务中心E" + randomNum
},
{
"name": "解放北路社区服务中心F" + randomNum
}
]
}
.children.map(x => {
return {
name: x.name,
parent: d,
depth: d.depth + 1,
data: {
...x
}
}
});
// 将子节点数据绑定在d节点上
if (children.length) d.children = children;
// 同时也绑到data上
d.data.children = children;
updateChart(d);
}
}
// collapse方法,切换子节点展开收起
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
// 添加贝塞尔曲线的path,衔接与父节点和子节点间
function diagonal(s, d) {
path =
`M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x},
${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`;
return path;
}
// 获取最多的子节点数
function getMax(obj) {
let max = 0;
if (obj.children) {
max = obj.children.length;
obj.children.forEach(d => {
const tmpMax = this.getMax(d);
if (tmpMax > max) {
max = tmpMax;
}
});
}
return max;
}
// 获取最深层级数
function getDepth(obj) {
var depth = 0;
if (obj.children) {
obj.children.forEach(d => {
var tmpDepth = this.getDepth(d);
if (tmpDepth > depth) {
depth = tmpDepth;
}
});
}
return 1 + depth;
}
<div class="chartTooltip hidden">
<p>
<strong class="name"></strong>
</p>
</div>
<script src="https://d3js.org/d3.v5.min.js"></script>
@charset "utf-8";
/* smartMenu.css by zhangxinxu */
.smart_menu_box{display:none; width:300px; position:absolute; z-index:201105;}
.smart_menu_body{padding:1px; border:1px solid #B8CBCB; background-color:#fff; -moz-box-shadow:2px 2px 5px #666; -webkit-box-shadow:2px 2px 5px #666; box-shadow:2px 2px 5px #666;}
.smart_menu_ul{margin:0; padding:0; list-style-type:none;}
.smart_menu_li{position:relative;}
.smart_menu_a{display:block; height:25px; line-height:24px; padding:0 5px 0 25px; color:#000; font-size:12px; text-decoration:none; overflow:hidden;}
.smart_menu_a:hover, .smart_menu_a_hover{background-color:#348CCC; color:#fff; text-decoration:none;}
.smart_menu_li_separate{line-height:0; margin:3px; border-bottom:1px solid #B8CBCB; font-size:0;}
.smart_menu_triangle{width:0; height:0; border:5px dashed transparent; border-left:5px solid #666; overflow:hidden; position:absolute; top:7px; right:5px;}
.smart_menu_a:hover .smart_menu_triangle, .smart_menu_a_hover .smart_menu_triangle{border-left-color:#fff;}
.smart_menu_li_hover .smart_menu_box{top:-1px; left:130px;}
.smart_menu_li:first-child a, .smart_menu_li:nth-child(2) a, .smart_menu_li:nth-child(3) a, .smart_menu_li:nth-child(4) a{
cursor: default;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font: 12px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
.chartTooltip {
position: absolute;
width: 200px;
height: auto;
padding: 10px;
box-sizing: border-box;
background-color: white;
border-radius: 5px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.4);
pointer-events: none;
}
.chartTooltip.hidden {
display: none;
}
.chartTooltip p {
margin: 0;
font-size: 14px;
line-height: 20px;
word-wrap: break-word;
}