[翻译]d3.js 的树状图(二)

本文是翻译的第二部分

目录

  1. 给树状图的节点增加样式

  2. 画一个垂直的树状图

  3. 从“扁平化”的数据生成树状图

  4. 从外部数据生成树状图

  5. 带交互的树状图

给树状图的节点增加样式

树状图的节点是用来表示数据结构的对象,但是在树状图上,他们也应该被当做附加底层数据的额外信息的入口。
在本章开始的第一个简单的例子中,我们在已经附加了一定量的信息了。每个节点的文本的相对位置是由这个节点是父节点(如果是,文本就在节点的左边)或者是树的叶子节点(如果是,文本就在节点的右边)。
1418441347292487.png
现在,这样很好,但是我们仅限于此吗?(答案是否定的)
这个例子很简单,这是一个将不同的样式添加到节点上以表示不同信息的例子。现在我需要申明,我并不是让你把你的树状图变得很花哨,因为这简直是糟蹋样式。所以不要重复我接下来的这个例子,仅让其中的某些特征作为激发你自己的完美的图表的一个点子。
下面,集中精力看一下我们将要生成的树状图。对这个有些疲劳的人可以略过看下几页。
tree-07.png
这样的结果是由于在 JSON 数组添加了额外的数据字段,在代码中,这些额外的字段用于生成不同样式。
我们改变的样式是:节点的直径,节点的填充和描边的颜色,根据连接的目标节点的不同改变连接的颜色。
我们来看看新的 JSON 数据:

{
    "name": "Top Level",
    "parent": "null",
    "value": 10,
    "type": "black",
    "level": "red",
    "children": [
      {
        "name": "Level 2: A",
        "parent": "Top Level",
        "value": 15,
        "type": "grey",
        "level": "red",
        "children": [
          {
            "name": "Son of A",
            "parent": "Level 2: A",
            "value": 5,
            "type": "steelblue",
            "level": "orange"
          },
          {
            "name": "Daughter of A",
            "parent": "Level 2: A",
            "value": 8,
            "type": "steelblue",
            "level": "red"
          }
        ]
      },
      {
        "name": "Level 2: B",
        "parent": "Top Level",
        "value": 10,
        "type": "grey",
        "level": "green"
      }
    ]
  }

每个节点有一个 value 属性,用于表示他们的重要程度(我们根据这个来改变节点的半径)。一个 tpye 属性,可能用于表示类型的不同(它们可能是活跃,非活跃或者是待定的状态),一个 level 属性,可能用于表示决定问题预警的级别(红色=坏的,橙色=注意,绿色=正常)。
如果不看我们人为的根据属性设置的样式选项,它们是通过很类似但略有不同的方式应用到我们的树状图上了。
首先要改变的是节点的半径,描边颜色和填充颜色。
我们简单的将添加圆圈的代码从:

 nodeEnter.append("circle")
   .attr("r", 10)
   .style("fill", "#fff");

替换成:

 nodeEnter.append("circle")
   .attr("r", function(d) { return d.value; })
   .style("stroke", function(d) { return d.type; })
   .style("fill", function(d) { return d.level; });

替换后将半径属性作为调用了 value 的函数返回,描边颜色通过调用 type 返回, 填充颜色通过 level 返回。这样简单而完美,但是我们需要对代码做一些的微调,设置节点和文本的间距,这样当半径增大或变小的时候,文本距离节点边缘的距离依然可以自适应。
要做这个,我们需要将调整文本在 x 方向的代码:

.attr("x", function(d) { 
    return d.children || d._children ? -13 : 13; })

我们添加一个动态改变的 value 字段:

.attr("x", function(d) { 
    return d.children || d._children ? 
    (d.value + 4) * -1 : d.value + 4 })

最后我们要做的是根据不同的目标节点颜色改变连接的颜色。我们在之前插入连接的代码:

link.enter().insert("path", "g")
   .attr("class", "link")
   .attr("d", diagonal);

增加一行根据 d.link.level 的目标节点的 level 的颜色设置连接颜色(描边):

link.enter().insert("path", "g")
   .attr("class", "link")
   .style("stroke", function(d) { return d.target.level; })
   .attr("d", diagonal);

这些需要灵活地运用各种想法。我不想某天在网上看到一个花里胡哨的树状图的边上标着一行文字“感谢 D3 Tips and Tricks 的帮助”,应该审慎而全面的考虑一些问题。:-)

画一个垂直的树状图

把树状图从水平方向变到垂直方向很简单,只要在我们最开始的简单的树状图的例子上改变三个地方。
首先要对调 x 和 y 坐标来改变节点的方向。
这也就是,将如下添加节点的代码:

var nodeEnter = node.enter().append("g")
   .attr("class", "node")
   .attr("transform", function(d) { 
    return "translate(" + d.y + "," + d.x + ")"; });

交换 d.x 和 d.y 的指示符成下面这样:

 var nodeEnter = node.enter().append("g")
   .attr("class", "node")
   .attr("transform", function(d) { 
    return "translate(" + d.x + "," + d.y + ")"; });

因为垂直的树状图要更紧凑一些,我们可以将深度调整到一个更合理的值。在我们的例子中,我们可以通过如下的代码把间距从 180 调到 100 :

nodes.forEach(function(d) { d.y = d.depth * 100; });

其次要做的是对连接也做相同的调整,我们生成曲线的 diagonal 路径的代码是这样:

var diagonal = d3.svg.diagonal()
 .projection(function(d) { return [d.y, d.x]; });

交换 d.x 和 d.y 的指示符成下面这样:

var diagonal = d3.svg.diagonal()
 .projection(function(d) { return [d.x, d.y]; });

现在,我们里最终的样式只差一小步。
tree-08.png
文本仍然是相对于节点的左边或者右边对齐。在这个例子中,这看起来很好。但是如果我们添加一些更多的节点的时候,那么树状图就显得很局促。所以,我们可以根据节点是父节点(上面)或者是叶子节点(下面)将文本放置在节点的上面或者是下面。
我们的之前添加文本的代码如下:

nodeEnter.append("text")
   .attr("x", function(d) { 
    return d.children || d._children ? -13 : 13; })
   .attr("dy", ".35em")
   .attr("text-anchor", function(d) { 
    return d.children || d._children ? "end" : "start"; })
   .text(function(d) { return d.name; })
   .style("fill-opacity", 1);

将 x 属性改成 y 属性,将文本居中对齐(实际上很简单),使节点和起始点的距离增加到 18 (-18) 像素。

nodeEnter.append("text")
   .attr("y", function(d) { 
    return d.children || d._children ? -18 : 18; })
   .attr("dy", ".35em")
   .attr("text-anchor", "middle")
   .text(function(d) { return d.name; })
   .style("fill-opacity", 1);

这样,我们就有了一个垂直的树状图。
tree-09.png
这个例子的完整代码在 github 上面,在 D3 Tips and Tricks 的附录,或者是 D3 Tips and Tricks 的代码样例集里面 (simple-tree-vertical.html)。示例在 bl.ocks.org 上面也能找到。

从“扁平化”的数据生成树状图

树状图是一个展示信息的很好的途径,但是它有一个缺点(至少从我们目前的例子来看)就是需要我们的数据组织成层次化的接头。但是大多数原始的数据是扁平的。也就是说,他们不会组织成“父-子”关系的数组。相反,他们将会是一系列的可能用于描述他们相互关系的对象(我们要转换为的节点)。例如,下面就是我们的样例数据的扁平化表示。

{ "name" : "Level 2: A", "parent":"Top Level" },
{ "name" : "Top Level", "parent":"null" },
{ "name" : "Son of A", "parent":"Level 2: A" },
{ "name" : "Daughter of A", "parent":"Level 2: A" },
{ "name" : "Level 2: B", "parent":"Top Level" }

这样相当简单并且只有节点的名字和它父节点的名字。很显然,这种数据很容易能够写成具有层次结构的数据,但是需要花费一点时间,而对于大一点的数据集,就有些无聊了。
幸运的是,电脑就是专门为了数据有关的重组而生的,在 nrabinowitz 的 Stack Overflow 的问题(Prateek Tandon 的提问)的指导下,Jesus Ruiz 和 AmeliaBR 指引着正确的方向,下面是我们将扁平化的数据转化成我们树状图用到的格式。
我们用到本章开头的简单的例子,首相就是替换我们原来的数据:

var treeData = [
  {
    "name": "Top Level",
    "parent": "null",
    "children": [
      {
        "name": "Level 2: A",
        "parent": "Top Level",
        "children": [
          {
            "name": "Son of A",
            "parent": "Level 2: A"
          },
          {
            "name": "Daughter of A",
            "parent": "Level 2: A"
          }
        ]
      },
      {
        "name": "Level 2: B",
        "parent": "Top Level"
      }
    ]
  }];

替换成扁平化的数据数组:

var data = [
    { "name" : "Level 2: A", "parent":"Top Level" },
    { "name" : "Top Level", "parent":"null" },
    { "name" : "Son of A", "parent":"Level 2: A" },
    { "name" : "Daughter of A", "parent":"Level 2: A" },
    { "name" : "Level 2: B", "parent":"Top Level" }
    ];

这里值得注意的是,由于我们要转换,我们也改变了数组的名字(变成 data),接着我们用我们之前的变量名 treeData 声明了我们新组的数据,这样我们接下来的代码就不需要做改动了。
接下来,我们创建了一个基于 name 属性的 Map,在 Stack Overflow 上,nrabinowitz 使用了 reduce 方法,这个方法以一个空的对象作为其实,在数据数组里做迭代,给每个节点增加一个项。

var dataMap = data.reduce(function(map, node) {
 map[node.name] = node;
 return map;}, {});

不要因为不懂它怎么运行的而郁闷,我不懂内燃机的原理,但是我开车还是行的。道理类似。
接着,我们迭代地将子节点增加到他们得父节点或者是根(如果没有父节点的话)。

var treeData = [];data.forEach(function(node) {
 // add to parent
 var parent = dataMap[node.parent];
 if (parent) {
  // create child array if it doesn't exist
  (parent.children || (parent.children = []))
   // add node to child array
   .push(node);
 } else {
  // parent is null or missing
  treeData.push(node);
 }});

这段代码遍历了数组里的每个节点,如果节点有子节点,那么将其添加到 children 的子数组里,如果有必要将新建这个数组。同理,如果节点没有父节点,就直接将其变成根节点。
就是这样!
代码的简洁性并不因为他的优雅打折扣。这个做法的确很聪明。最后的结果跟我们之前的树状图没有什么不同。
1418441347292487.png
但是,它增加了新的数据格式的支持。

从外部数据生成树状图

到现在为止,我们的所有的例子中用到的数据都是我们在文件本身声明的,能从外部文件中导入数据是我们要知道如何实施的一个重要的特性。
从本章开始的之前的简单的树状图开始,首先需要改变的是,我们需要移除掉声明数据部分的代码。但是我并没有把他扔掉,因为我们将创建一个单独的文件,名为 treeData.json。内容如下:

[
  {
    "name": "Top Level",
    "parent": "null",
    "children": [
      {
        "name": "Level 2: A",
        "parent": "Top Level",
        "children": [
          {
            "name": "Son of A",
            "parent": "Level 2: A"
          },
          {
            "name": "Daughter of A",
            "parent": "Level 2: A"
          }
        ]
      },
      {
        "name": "Level 2: B",
        "parent": "Top Level"
      }
    ]
  }
]

不要包括进 treeData = 部分,或者是末尾的分号(你可以删除这些)。
接下来,我们要做的是,将声明 root 变量和更新树状图部分的代码
``
root = treeData[0];
update(root);

改成一段用 d3.json 的 accessor 加载 treeData.json 的代码。记住要正确处理这个文件的名字,也就是说,treeData,json 文件也可能存在于我们的 html 文件的文件夹,不要弄混了。

d3.json("treeData.json", function(error, treeData) {
root = treeData[0];
update(root);});

接着,声明 root 变量和调用 update 函数画树状图的部分是一样的。
##带交互的树状图

到现在为止,我们展示的所有的例子感觉在网页上展示的信息都是静止的,放到哪里他们就静止在哪里。网页上内容的强项就是它能让用户在更大的意义上参与其中。因此,下面的包含了交互的树状图里面,用户能点击任何一个父节点,它们将会收缩自身,为别的节点腾出更大的空间或者是看起来更加简洁。另外,任何收缩了的父节点被点击的时候,它们将会变成它们之前的样子。
下面展示的例子是 Mike Bostock 的例子的衍生出来的,我不会对这个文件怎么做到得作全面讲解,我会仅仅就对我们感兴趣得部分做一些探讨。
这个例子的完整代码在 github 上面,在 D3 Tips and Tricks 的附录,或者是 D3 Tips and Tricks 的代码样例集里面 (interactive-tree.html)。示例在 bl.ocks.org 上面也能找到。
简明地介绍下这个动作,树状图初始的时候将会展示整棵树:
1418441347292487.png
当点击 "Level 2: A" 节点的时候,树状图将部分收缩为:

1418468259193643.png
我们可以点击根节点("Top Level"),能够将整棵树都收缩起来:
1418468259123755.png
接着点击这些节点,树状图将恢复到原来的状态。
与前面很重要的不同之一就有为了让每个节点对鼠标有响应,我们需要在 <style> 部分增加如下内容:

.node {
cursor: pointer;
}

接下来代码中用到了 d3.js 模型中 enter-update-exit 流程来完成节点操作的合理的变换。
节点如果收缩了,那么它将带有颜色(“钢青色”),在代码的最后,我们有一个用户用到了 d._childeren 的引用,这个我们在大部分的例子中用到了。

function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);}
```
这段代码可以在点击节点的时候,更新与它相关的数据,接下来的一些动作是改变它的一些属性,这个都是基于 if 判断语句的(例如:"fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }),如果 d._children 存在,这段代码将填充节点为淡钢青色,如果不存在,那么就填充为白色。
我们在本章之前看到的例子都能应用到这个具有交互的版本中。所以,这将让你拥有生成更有趣的可视化的图表的能力。
Enjoy it !

以上的所有文字都是 D3 Tips and Tricks 里面的,这本书能被免费地下载(如果你想捐赠一些,也是可以的 :-))。

标签: d3, tree, svg
返回文章列表 文章二维码 打赏
本页链接的二维码
打赏二维码