上一篇: 关系图-弦图
代码结构和初始化画布的Chart对象介绍,请先看 这里
本图完整的源码地址: 这里
1 图表效果
2 数据
{
"name": "letter",
"children": [
{
"name": "a",
"children": [
{
"name": "a1",
"children": [
{"name": "a11", "value": 2},
{"name": "a12", "value": 3},
{"name": "a13", "value": 4},
{"name": "a14", "value": 5},
{"name": "a15", "value": 5},
{"name": "a16", "value": 7}
]
}
]
},
{
"name": "b",
"children": [
{
"name": "b1",
"children": [
{"name": "b11", "value": 4},
{"name": "b12", "value": 2},
{"name": "b13", "value": 4},
{"name": "b14", "value": 4},
{"name": "b15", "value": 4},
{"name": "b16", "value": 4}
]
},
{
"name": "b2",
"children": [
{"name": "b21", "value": 4},
{"name": "b22", "value": 5},
{"name": "b23", "value": 4},
{"name": "b24", "value": 4}
]
}
]
},
{
"name": "c",
"children": [
{
"name": "c1",
"children": [
{"name": "c11", "value": 6},
{"name": "c12", "value": 1}
]
},
{
"name": "c2",
"children": [
{"name": "c21", "value": 2},
{"name": "c22", "value": 3},
{"name": "c23", "value": 2},
{"name": "c24", "value": 3},
{"name": "c25", "value": 3},
{"name": "c26", "value": 3},
{"name": "c27", "value": 3},
{"name": "c28", "value": 3},
{"name": "c29", "value": 3}
]
}
]
}
]
}
3 关键代码
导入数据
d3.json('./data.json').then(function(data){
....
一些样式配置参数,与基础树图类似
const config = {
margins: {top: 80, left: 50, bottom: 20, right: 50},
textColor: 'black',
title: '径向树图',
hoverColor: 'gray',
animateDuration: 1000,
pointSize: 5,
pointFill: 'white',
pointStroke: 'red',
lineStroke: 'gray'
}
首先运用d3.hierarchy
将数据转换为节点树,接着调用d3.cluster
定义树图的布局信息,注意径向树图的size
参数不再是长宽,而是弧度和半径
/* ----------------------------数据转换------------------------ */
chart._nodeId = 0; //用于标识数据唯一性
const root = d3.hierarchy(data);
const generateTree = d3.cluster()
.nodeSize([10, 10])
.separation((a,b) => a.parent === b.parent ? 1 : 3)
.size([2 * Math.PI, d3.min([chart.getBodyWidth(), chart.getBodyHeight()]) / 2 * 0.8]);
generateTree(root);
接着渲染节点,节点由circle
元素表示,与基础树图的动画效果类似,要达到点击伸缩子树的功能,则需要对元素的enter
和exit
阶段添加过渡效果,enter
时子树要从点击位置逐渐放大,而exit
时子树要逐渐缩小到父节点的新位置
/* ----------------------------渲染节点------------------------ */
chart.renderNode = function(){
let nodes = d3.select('.groups');
if (nodes.empty()){
nodes = chart.body()
.append('g')
.attr('class', 'groups')
.attr('transform', 'translate(' + chart.getBodyWidth()/2 + ',' + chart.getBodyHeight()/2 + ')');
}
const groups = nodes
.selectAll('.g')
.data(root.descendants(), (d) => d.id || (d.id = ++chart._nodeId));
chart.groupsEnter = groups.enter()
.append('g')
.attr('class', (d) => 'g ' + d.data.name)
.attr('transform', (d) => {
if (chart.first) return 'translate(' + chart.oldY * Math.cos(chart.oldX - Math.PI/2) + ',' + chart.oldY * Math.sin(chart.oldX - Math.PI/2) + ')'; //首次渲染,子树从(0,0)点开始放缩,否则,从点击位置开始放缩
})
chart.groupsEnter.append('circle')
.attr('r', config.pointSize)
.attr('cx', 0)
.attr('cy', 0)
.attr('stroke', config.pointStroke);
chart.groupsEnter.merge(groups)
.transition().duration(config.animateDuration)
.attr('transform', (d) => {
return 'translate(' + d.y * Math.cos(d.x - Math.PI/2) + ',' + d.y * Math.sin(d.x - Math.PI/2) + ')';
})
.selectAll('circle')
.attr('fill', (d) => d._children ? config.hoverColor: config.pointFill);
groups.exit()
.attr('transform-origin', () => chart.targetNode.y * Math.cos(chart.targetNode.x - Math.PI/2) + ' ' + chart.targetNode.y * Math.sin(chart.targetNode.x - Math.PI/2)) //子树逐渐缩小到新位置
.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('x', 0)
.attr('y', 0)
.attr('class', 'text')
.attr('text-anchor', 'middle')
.attr('dy', 3)
.text((d) => d.data.name)
.attr('transform', function(d){
const offsetRadius = d.children? -(this.getBBox().width + config.pointSize) : this.getBBox().width + config.pointSize;
const translate = d.depth === 0 ? 'translate(' + this.getBBox().width/2 + ',' + -this.getBBox().height/2 + ')' : 'translate(' + offsetRadius * Math.cos(d.x - Math.PI/2) + ',' + offsetRadius * Math.sin(d.x - Math.PI/2) + ')';
let angle = (180 * d.x / Math.PI) % 360;
if (angle > 90 && angle < 270) {
angle = angle + 180;
}
const rotate = d.depth === 0 ? 'rotate(0)': 'rotate(' + angle + ')';
return translate + ' ' + rotate;
});
}
渲染节点之间的连线,这里直接使用d3.linkRadial
进行生成path,注意连线与节点类似,也需要相同的放缩过渡效果
/* ----------------------------渲染连线------------------------ */
chart.renderLines = function(){
let link = d3.select('.links');
if (link.empty()){
link = chart.body()
.insert('g', '.groups')
.attr('class', 'links')
.attr('transform', 'translate(' + chart.getBodyWidth()/2 + ',' + chart.getBodyHeight()/2 + ')')
}
const links = link
.selectAll('.link')
.data(root.links().map((item) => {
item.id = item.source.id + '-' + item.target.id; // 为链接添加id
return item;
}), (d) => d.id );
links.enter()
.append('path')
.attr('class', 'link')
.attr('fill', 'none')
.attr('stroke', config.lineStroke)
.attr('transform-origin', () => chart.oldY * Math.cos(chart.oldX - Math.PI/2) + ' ' + chart.oldY * Math.sin(chart.oldX - Math.PI/2))
.attr('transform', 'scale(0.01)')
.merge(links)
.transition().duration(config.animateDuration)
.attr('transform', 'scale(1)')
.attr('d', d3.linkRadial()
.angle((d) => d.x)
.radius((d) => d.y)
);
links.exit()
.attr('transform-origin', () => chart.targetNode.y * Math.cos(chart.targetNode.x - Math.PI/2) + ' ' + chart.targetNode.y * Math.sin(chart.targetNode.x - Math.PI/2)) //子树逐渐缩小到新位置
.transition().duration(config.animateDuration)
.attr('transform', 'scale(0.01)')
.remove();
}
最后绑定鼠标交互事件,当点击某个节点隐藏子树时,将其children
属性设置为null
,并暂存其子树数据,重新触发布局。当点击某各节点显现子树时,将暂存的子树数据拿出并重新赋值children
属性,并重新布局,如此达到子树切换的效果。
/* ----------------------------绑定鼠标交互事件------------------------ */
chart.addMouseOn = function(){
d3.selectAll('.g')
.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; //被点击的节点,该节点的x和y坐标随后将被更新
}
}