D3.js学习笔记:绘制树形图
业务运行系统按如下方式组织:
我只关心结构,不关心颜色等其它信息。上图是一个明显的树形结构,可以利用 D3 的 tree 模块在网页中现实。
D3 Tree Layout 参考:
数据准备
需要json表示的树对象,形如:
{
"name": "flare",
"children": [
{
"name": "analytics",
"children": [
{
"name": "cluster",
"children": [
{"name": "AgglomerativeCluster", "size": 3938},
{"name": "CommunityStructure", "size": 3812},
{"name": "MergeEdge", "size": 743}
]
},
{
"name": "graph",
"children": [
{"name": "BetweennessCentrality", "size": 3534},
{"name": "LinkDistance", "size": 5731}
]
}
]
}
]
}
递归定义结构,其中 name 表示节点名,children 表示子节点数组,其它字段任意。
D3 的 Tree Layout 文档中给出一些常用的字段:
parent
:父节点,根结点为nullchildren
:子节点,叶子节点为nulldepth
:节点深度,根结点从0开始x
:节点的 x 坐标y
:节点的 y 坐标
之前已经准备好节点树对象的 json 数据,但需要修改。D3示例中叶子节点没有 children 属性,但我生成的 json 数据中叶子节点有值为空数组的 children 属性。需要在调用api获取数据后,删掉叶子节点中的 children 属性。
function del_empty_children(node){
if(node.children.length == 0)
delete node.children;
else {
node.children.forEach(del_empty_children);
}
}
del_empty_children(result);
可展开的树
参考D3官网示例《Collapsible Tree》
点击节点,可以展开或折叠子节点。整个树都由json数据对象生成,但如何才能实现展开或折叠呢?
展开折叠的实现
D3中数据与svg图像一一对应,数据不同,绘出的图形也不一样。展开折叠可以从另一个方面理解为后面的json树数据不一样了。当然,我们可以在每次点击时重新获取数据,但这样重复获取数据不是好方法。上面的示例中采用一种巧妙的方法实现数据变化。
之前提到,children 属性表示子节点,从 api 中返回的数据包含完整的树信息,但初次显示只关心第1层子节点(根结点为第0层),其余节点默认折叠。可以删掉将第1层节点的 children 属性,这样就不会显示子节点,但为了保存信息,将 children 属性移动到另一个属性 _children 中。这样再点击节点时,从 _children 中将原来的子节点信息拷贝回 children 节点,重新绘图,就可以实现展开节点。同理,通过将 children 移动到 _children 实现折叠节点。
定义一个将某节点折叠的函数:
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
初始只保留第1层节点:
root.children.forEach(collapse);
update(root);
点击某个节点时判断是否折叠,将 children 和 _children 互换:
function circle_click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
每次改变数据后,都要重新绘制图形,并实现过渡动画。下面简单介绍下整体流程。
整体流程
// 监视变量 treeGraphData ,改变时重新绘图。
scope.$watch("treeGraphData", function (new_value, old_value) {
// ...
root.children.forEach(collapse);
// 更新图形
update(root);
// 上面提到过,该函数更新数据并重绘图形
function update(){
// ...
}
// ...
// 点击某节点时执行的操作,需要更新整幅图像。
function circle_click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
}
下面就分析如何实现图形绘制。
绘图
整个图形共有如下组件和功能:
- 名称
- 连接点
- 连接线
- 点击响应
- 过渡动画
D3将数据映射为图像,数据变化时图像也应该随之变化。D3可以在变化的数据中识别出保留的数据集、新增加的数据集(enter 数据集)、删除的数据集(exit 数据集)。对于本例来说:
- 保留数据集需要更改名称、连接点、连接线位置,更改连接点填充色。
- 新增数据集需要绘制连接点、名称、连接线。
- 删除数据集需要移除连接点、名称、连接线。
以上改变图形都使用过渡动画。详细代码参看源文件。简要分析如下:
获取节点集合和连线集合
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
先处理节点集合。设置y坐标点,每层占180px
nodes.forEach(function (d) {
d.y = d.depth * 180;
});
每个node对应一个group
var node = g_svg.selectAll("g.node")
.data(nodes, function (d) {
return d.id || (d.id = ++i);
});
新增节点数据集,设置位置
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function (d) {
// 从 source 开始的偏移。
// 因为在父节点点击展开,调用update(父节点)函数,子节点从父节点开始排异
return "translate(" + source.y0 + "," + source.x0 + ")";
});
添加连接点
nodeEnter.append("circle")
.attr("r", 1e-6)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
})
.on("click", circle_click);
添加标签
nodeEnter.append("text")
.attr("x", function (d) {
return d.children || d._children ? -10 : 10;
})
.attr("dy", ".35em")
.attr("text-anchor", function (d) {
return d.children || d._children ? "end" : "start";
})
.text(function (d) {
return d.name;
})
.style("fill-opacity", 1e-6);
node就是保留的数据集,为原来数据的图形添加过渡动画。首先是整个组的位置。
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + d.y + "," + d.x + ")";
});
更新连接点的填充色
nodeUpdate.select("circle")
.attr("r", 4.5)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
});
最后处理消失的数据,添加消失动画
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
再处理连线集合。
var link = g_svg.selectAll("path.link")
.data(links, function (d) {
return d.target.id;
});
添加新的连线
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function (d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
});
保留的连线添加过渡动画
link.transition()
.duration(duration)
.attr("d", diagonal);
消失的连线添加过渡动画
link.exit().transition()
.duration(duration)
.attr("d", function (d) {
var o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
})
.remove();