上一篇: 矩形树图 https://blog.csdn.net/zjw_python/article/details/98489369
下一篇: 漏斗图 https://blog.csdn.net/zjw_python/article/details/98497967
代码结构和初始化画布的Chart对象介绍,请先看 https://blog.csdn.net/zjw_python/article/details/98182540
本图完整的源码地址: https://github.com/zjw666/D3_demo/tree/master/src/treeChart/basicTreeChart
1 图表效果
2 数据
{
"name": "grandfather",
"children": [
{
"name": "father",
"children": [
{
"name": "son",
"children": [
{"name": "grandson1", "house": 2},
{"name": "grandson2", "house": 3},
{"name": "grandson3", "house": 4}
]
}
]
},
{
"name": "mother1",
"children": [
{
"name": "daughter1",
"children": [
{"name": "granddaughter1", "house": 4},
{"name": "granddaughter2", "house": 2}
]
},
{
"name": "daughter2",
"children": [
{"name": "granddaughter3", "house": 4}
]
}
]
},
{
"name": "mother2",
"children": [
{
"name": "son1",
"children": [
{"name": "grandson4", "house": 6},
{"name": "granddaughter4", "house": 1}
]
},
{
"name": "son2",
"children": [
{"name": "granddaughter5", "house": 2},
{"name": "grandson5", "house": 3},
{"name": "granddaughter5", "house": 2}
]
}
]
}
]
}
3 关键代码
导入数据
d3.json('./data.json').then(function(data){
....
一些样式参数配置,例如节点,线条的颜色等
const config = {
margins: {top: 80, left: 50, bottom: 50, right: 50},
textColor: 'black',
title: '基础树图',
hoverColor: 'gray',
animateDuration: 1000,
pointSize: 5,
pointFill: 'white',
pointStroke: 'red',
paddingLeft: 20,
lineStroke: 'gray'
}
数据转换,树图与矩阵树图类似,都是树形层次性数据结构,因此需要将数据转化为一系列节点,对于树图,其布局算法是d3.tree
,传入处理后的数据后,将自动为每个节点添加布局信息
/* ----------------------------数据转换------------------------ */
chart._nodeId = 0; //用于标识数据唯一性
const root = d3.hierarchy(data);
const generateTree = d3.tree()
.size([chart.getBodyHeight(), chart.getBodyWidth()*0.8]);
generateTree(root);
渲染树的节点,其实渲染出树的节点很简单,直接使用circle
元素就可以了,难点在于过渡的动画效果,我们想做出类似于Echart树图的效果,点击节点时会放缩子树,并重新布局。要做到这种效果就必须对节点的各个阶段,enter
,update
和exit
添加对应的动画效果。
/* ----------------------------渲染节点------------------------ */
chart.renderNode = function(){
const groups = chart.body().selectAll('.g')
.data(root.descendants(), (d) => d.id || (d.id = ++chart._nodeId));
const groupsEnter = groups.enter()
.append('g')
.attr('class', (d) => 'g g-' + d.id)
.attr('transform-origin', (d) => { //子树从点击位置逐渐放大
if (d.parent){
return chart.oldY + config.paddingLeft + ' ' + chart.oldX;
}
return d.y + config.paddingLeft + ' ' + d.x;
})
.attr('transform', (d) => { //首次渲染进入不放缩
if (d.parent && chart.first) return 'scale(0.01)' + 'translate(' + (chart.oldY + config.paddingLeft) + ',' + chart.oldX + ')';
return 'scale(1)' + 'translate(' + (d.y + config.paddingLeft) + ',' + d.x + ')';
})
groupsEnter.append('circle')
.attr('r', config.pointSize)
.attr('cx', 0)
.attr('cy', 0)
.attr('fill', config.pointFill)
.attr('stroke', config.pointStroke);
groupsEnter.merge(groups)
.transition().duration(config.animateDuration)
.attr('transform', (d) => 'translate(' + (d.y + config.paddingLeft) + ',' + d.x + ')')
.select('circle')
.attr('fill', (d) => d._children ? config.hoverColor : config.pointFill);
groups.exit()
.attr('transform-origin', (d) => (chart.targetNode.y + config.paddingLeft) + ' ' + chart.targetNode.x) //子树逐渐缩小到新位置
.transition().duration(config.animateDuration)
.attr('transform', 'scale(0.01)')
.remove();
}
树的节点渲染好了,那么节点的文本标签位置也就定了下来
/* ----------------------------渲染文本标签------------------------ */
chart.renderText = function(){
d3.selectAll('.text').remove();
const groups = d3.selectAll('.g');
groups.append('text')
.attr('class', 'text')
.text((d) => d.data.name.length<5?d.data.name:d.data.name.slice(0,3) + '...')
.attr('dy', function(){
return chart.textDy || (chart.textDy = this.getBBox().height/4);
})
.attr('text-anchor', (d) =>{
return d.children ? 'end' : 'start';
})
.attr('dx', (d) =>{
return d.children ? -config.pointSize*1.5 : config.pointSize*1.5;
});
}
接下来渲染节点之间的连线,这里使用d3.path
绘制贝塞尔曲线,并选取两节点的中间点作为控制点,过渡动画效果与节点类似,通过scale
实现
/* ----------------------------渲染连线------------------------ */
chart.renderLines = function(){
const nodesExceptRoot = root.descendants().slice(1);
const links = chart.body().selectAll('.link')
.data(nodesExceptRoot, (d) => d.id || (d.id = ++chart._nodeId));
links.enter()
.insert('path', '.g')
.attr('class', 'link')
.attr('transform-origin', (d) => {
if (d.parent){ //连线从点击位置逐渐放大
return chart.oldY + config.paddingLeft + ' ' + chart.oldX;
}
return d.y + config.paddingLeft + ' ' + d.x;
})
.attr('transform', (d) => { //首次渲染进入不放缩
if (d.parent && chart.first) return 'scale(0.01)';
return 'scale(1)';
})
.merge(links)
.transition().duration(config.animateDuration)
.attr('d', (d) => {
return generatePath(d, d.parent);
})
.attr('transform', 'scale(1)')
.attr('fill', 'none')
.attr('stroke', config.lineStroke)
links.exit()
.attr('transform-origin', (d) => { //连线逐渐缩小到新位置
return chart.targetNode.y + config.paddingLeft + ' ' + chart.targetNode.x;
})
.transition().duration(config.animateDuration)
.attr('transform', 'scale(0.01)')
.remove();
function generatePath(node1, node2){
const path = d3.path();
path.moveTo(node1.y + config.paddingLeft, node1.x);
path.bezierCurveTo(
(node1.y + node2.y)/2 + config.paddingLeft, node1.x,
(node1.y + node2.y)/2 + config.paddingLeft, node2.x,
node2.y + config.paddingLeft, node2.x
);
return path.toString();
}
}
最后绑定鼠标交互事件,当点击某个节点隐藏子树时,将其children
属性设置为null
,并暂存其子树数据,重新触发布局。当点击某各节点显现子树时,将暂存的子树数据拿出并重新赋值children
属性,并重新布局,如此达到子树切换的效果。
/* ----------------------------绑定鼠标交互事件------------------------ */
chart.addMouseOn = function(){
d3.selectAll('.g circle')
.on('click', function(d){
toggle(d);
generateTree(root);
chart.renderNode();
chart.renderLines();
chart.renderText();
chart.addMouseOn();
});
function toggle(d){
chart.first = true;
if (d.children){
d._children = d.children;
d.children = null;
}else{
d.children = d._children;
d._children = null;
}
chart.oldX = d.x; //点击位置x坐标
chart.oldY = d.y; //点击位置y坐标
chart.targetNode = d; //被点击的节点
}
}