D3.js学习笔记:绘制树形图

目录

业务运行系统按如下方式组织:

我只关心结构,不关心颜色等其它信息。上图是一个明显的树形结构,可以利用 D3 的 tree 模块在网页中现实。
D3 Tree Layout 参考:

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:父节点,根结点为null
  • children:子节点,叶子节点为null
  • depth:节点深度,根结点从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();

效果