上一篇: 可缩放的封闭图
代码结构和初始化画布的Chart对象介绍,请先看 这里
本图完整的源码地址: 这里
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){
...
数据转换,这个与普通矩形树状图不同的地方在于,要处理每个子节点的布局信息,将其x
、y
坐标都转化为相对于其父节点左上角坐标的布局,而不是整个画布。除此之外,还需要一个栈用来存储放缩下钻的层级
/* ----------------------------数据转换------------------------ */
const root = d3.hierarchy(data)
.sum((d) => d.house)
.sort((a,b) => a.value - b.value);
const generateTreeMap = d3.treemap()
.tile(function(node, x0, y0, x1, y1){
d3.treemapBinary(node, 0, 0, width, height);
for (const child of node.children) {
child.x0 = x0 + child.x0 / width * (x1 - x0);
child.x1 = x0 + child.x1 / width * (x1 - x0);
child.y0 = y0 + child.y0 / height * (y1 - y0);
child.y1 = y0 + child.y1 / height * (y1 - y0);
}
})
generateTreeMap(root);
const scaleX = d3.scaleLinear().rangeRound([0, width]);
const scaleY = d3.scaleLinear().rangeRound([0, height]);
const stack = [root.data.name];
渲染矩形, 绑定click事件,点击子节点下钻层级,点击上方的白色横条用于返回上一层级。当下钻层级时,在当前点击节点的下面绘制其子节点,随后将其放大充满视图,并同时逐渐显示位于其下面的子节点,产生放大的特效。当返回上一层级时,绘制当前点击节点的兄弟节点,随后将当前节点缩小至原本大小,并逐渐显示兄弟节点,产生缩小的特效。我们可以看到,zoomOut
和zoomIn
代码非常相似,因为放大和缩小都是通过改变scale
函数的domain
属性来巧妙实现的
/* ----------------------------渲染矩形------------------------ */
chart.renderRect = function(group, currentRoot){
const cells = group.selectAll('.cell')
.data(currentRoot.children.concat(currentRoot))
.join('g');
cells.filter(d => d === currentRoot ? d.parent : d.children) //可以点击的节点包括两类:有孩子的非当前根节点、有父节点的当前根节点
.attr('cursor', 'pointer')
.on('click', d => d === currentRoot ? zoomOut(currentRoot) : zoomIn(d))
cells.attr('class', (d, i) => 'cell cell-' + i)
.append('rect')
.attr('fill', (d,i) => d === currentRoot ? 'white' : chart._colors(i % 10));
cells.append('text')
.attr('class', 'cell-text')
.text((d) => d.data.name)
.attr("x", 10)
.attr("y", 20)
.attr('stroke', config.textColor)
.attr('fill', config.textColor)
.text((d) => d === currentRoot ? stack.join(' -> ') : d.data.name);
position(group, currentRoot);
function position(group, currentRoot){
group.selectAll('.cell')
.attr('transform', d => d === currentRoot ? `translate(0, -30)` : `translate(${scaleX(d.x0)},${scaleY(d.y0)})`)
.select('rect')
.attr('width', d => d === currentRoot ? width : scaleX(d.x1) - scaleX(d.x0))
.attr('height', d => d === currentRoot ? 30 : scaleY(d.y1) - scaleY(d.y0));
}
function zoomIn(d){
stack.push(d.data.name);
const oldGroup = group.attr('pointer-events', 'none');
const newGroup = group = chart.body().append('g').attr('transform', 'translate(0, 15)').call(chart.renderRect, d); //绘制当前点击的节点的子节点盖在当前节点上,子节点均在当前节点围成的矩形区域内
scaleX.domain([d.x0, d.x1]);
scaleY.domain([d.y0, d.y1]);
chart.body().transition()
.duration(config.animateDuration)
.call(
t => oldGroup.transition(t)
.call(position, d.parent) //将当前点击的节点放大充满整个绘图区
.remove() //移除当前点击的节点
)
.call(
t => newGroup.transition(t)
.attrTween('opacity', () => d3.interpolate(0, 1)) //子节点逐渐显现
.call(position, d) //将子节点放大充满整个绘图区
);
}
function zoomOut(d){
stack.pop();
const oldGroup = group.attr('pointer-events', 'none');
const newGroup = group = chart.body().append('g').attr('transform', 'translate(0, 15)').call(chart.renderRect, d.parent); //绘制当前点击的节点的兄弟节点,兄弟节点均在绘图区外
scaleX.domain([d.parent.x0, d.parent.x1]);
scaleY.domain([d.parent.y0, d.parent.y1]);
chart.body().transition()
.duration(config.animateDuration)
.call(
t => oldGroup.transition(t)
.call(position, d) //将当前点击的节点的子节点缩小至原本点击节点的大小
.remove()
)
.call(
t => newGroup.transition(t)
.attrTween('opacity', () => d3.interpolate(0, 1)) //点击节点和兄弟节点逐渐显示
.call(position, d.parent) //将点击节点和兄弟节点缩小充满整个绘图区
);
}
}