MENU

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

前言

本文来自 http://www.d3noob.org/2014/01/tree-diagrams-in-d3js_11.html,最近在使用 D3 画树状图,看到这个教程写得很详细,于是就在此翻译成中文,本译文分成了两部分,有兴趣可以看看原文。

目录

  1. 什么是树状图
  2. 一个简单的树状图介绍
  3. 给树状图的节点增加样式
  4. 画一个垂直的树状图
  5. 从“扁平化”的数据生成树状图
  6. 从外部数据生成树状图
  7. 带交互的树状图

什么是树状图

“树状图”不是特有的图表类型。相反,它是 D3 层次结构图系列的代表。
它旨在生成一种“节点-连接”的结构来表示出节点间连接,该结构通过父子节点的方式展示出一个节点到另一个节点的关系。
例如,下面的图展示了一个标有“顶节点”的根节点(开始的位置),“顶节点”有两个子节点(Bob 和 Sally)。同时,Bob 有两个附属的子节点“Son of Bob”和“daughter of Bob”。
1418441347953541.png
这种图的优点很明显:用文字描述很困难,但是用图形的方式很容易表示判断节点间的关系。
生成这种类型图的数据需要描述节点的关系,但这并不困难。例如,下面(JSON 格式)就是上面树状图的的数据,它展示了生成正确层次结构所必须的最少的信息。
{

"name": "Top Node",
"children": [
  {
    "name": "Bob: Child of Top Node",
    "parent": "Top Node",
    "children": [
      {
        "name": "Son of Bob",
        "parent": "Bob: Child of Top Node"
      },
      {
        "name": "Daughter of Bob",
        "parent": "Bob: Child of Top Node"
      }
    ]
  },
  {
    "name": "Sally: Child of Top Node",
    "parent": "Top Node"
  }
]

}
它显示每个节点有一个 name 属性用于在树状图上表示节点本身,并在适当的情况下,表示出它的子节点(数组的方式)和父节点。
以上展示的数据被组织成有层次的,但所有的元数据并不都是这种很完美的形式。我们将通过这种类型的图的例子来说明:导入“扁平化”的数据并将其转换成一种有层次的形式。
网上有大量的树状图的例子,但是我推荐访问由 Christophe Viau 维护的 D3.js gallery 作为各种想法的起点。
在这一章,我们将首先学习生成树状图的一段简单的代码,接着用各种方法修改它。包括:旋转使它成为垂直的图形,给节点添加一些动态的样式,从“扁平化”的结构导入数据和从外部数据源导入数据。最后,我们看到一个更为复杂但是更在网上常用的例子:允许用户交互地展开和收缩节点。

一个简单的树状图介绍

我们先解释一段画树状图代码的例子,这个例子更多的是为了理解画图的过程而不是因为它是一个画树状图的好例子。这并不是一个好例子,因为它没有任何交互,而交互是 d3.js 的强项之一。然而,我们在研究了想要的一些可能的配置选项之后,在本章的最后将做出一个有交互操作的版本。
我们想要做的图形是以下这样:
1418441347292487.png
代码如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">

    <title>Collapsible Tree Example</title>

    <style>

 .node circle {
   fill: #fff;
   stroke: steelblue;
   stroke-width: 3px;
 }

 .node text { font: 12px sans-serif; }

 .link {
   fill: none;
   stroke: #ccc;
   stroke-width: 2px;
 }
 
    </style>

  </head>

  <body>

<!-- load the d3.js library --> 
<script src="http://d3js.org/d3.v3.min.js"></script>
 
<script>

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"
      }
    ]
  }
];

// ************** Generate the tree diagram  *****************
var margin = {top: 20, right: 120, bottom: 20, left: 120},
 width = 960 - margin.right - margin.left,
 height = 500 - margin.top - margin.bottom;
 
var i = 0;

var tree = d3.layout.tree()
 .size([height, width]);

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

var svg = d3.select("body").append("svg")
 .attr("width", width + margin.right + margin.left)
 .attr("height", height + margin.top + margin.bottom)
  .append("g")
 .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

root = treeData[0];
  
update(root);

function update(source) {

  // Compute the new tree layout.
  var nodes = tree.nodes(root).reverse(),
   links = tree.links(nodes);

  // Normalize for fixed-depth.
  nodes.forEach(function(d) { d.y = d.depth * 180; });

  // Declare the nodes
  var node = svg.selectAll("g.node")
   .data(nodes, function(d) { return d.id || (d.id = ++i); });

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

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

  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);

  // Declare the links
  var link = svg.selectAll("path.link")
   .data(links, function(d) { return d.target.id; });

  // Enter the links.
  link.enter().insert("path", "g")
   .attr("class", "link")
   .attr("d", diagonal);

}

</script>
 
  </body>
</html>

完整版的代码在 github 上可以找到,在 D3 Tips and Tricks 的附录或者是 D3 Tips and Tricks 的代码集里也有(simple-tree-diagram.html)。运行的代码在 bl.ocks.org 能找到。
在描述文件的操作中,我将略过 HTML 文件的结构方面,这些在 D3 Tips and Tricks 的开始已经提到过了。同样,前面已经提到过的 JavaScript 函数将仅作简略介绍。
文件的开始设置了 HTML head 和 body 用于加载 d3.js 脚本,并在 <script> 部分定义了 css 样式。
css 部分设置了代表节点的圆圈,节点旁边的文本和节点间连接的样式。

.node circle {
   fill: #fff;
   stroke: steelblue;
   stroke-width: 3px;
 }

 .node text { font: 12px sans-serif; }

 .link {
   fill: none;
   stroke: #ccc;
   stroke-width: 2px;
 }

接着,JavaScript 部分首先需要声明将要用到的数据数组:

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"
      }
    ]
  }
];

在本章的开头已经提到过了,这种数据采用了层次化的 JSON 数据结构。每个节点必须有一个 name 属性和一个 parent 或(和) child 属性。有这种方式表示层次化结构数据的例子。从传统的父母-后代节点到硬盘或者复杂物体材料的解构。任何一个表示从多个源导致单个结果:比如一次选举或一个依靠多个触发点的预警编码系统,都是这样编制的。
接下来的部分声明了一些标准的图表特征,比如包含间距的 svg 容器的尺寸和形状。

var margin = {top: 20, right: 120, bottom: 20, left: 120},
 width = 960 - margin.right - margin.left,
 height = 500 - margin.top - margin.bottom;
 
var i = 0;

var tree = d3.layout.tree()
 .size([height, width]);

同时,将变量/函数 tree 赋值为 d3.js 的函数。这将用于赋值和计算节点和图表节点所需的数据。我们后面将调用这个变量。
下面的一部分代码将用于画出节点间的连接。这不是具体画连接的代码,仅仅是声明要用到的变量/函数。

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

这里用到了 d3.js diagonal 函数用于画出两点之间的路径,这样,使用优美的流线(Bezier 曲线)表示连接。
下面一部分代码将我们的 SVG 工作区添加到网页的 body 元素上,并创建了一个 group 元素(<g>)用于包含 svg 对象(节点,文本和连接)。

var svg = d3.select("body").append("svg")
 .attr("width", width + margin.right + margin.left)
 .attr("height", height + margin.top + margin.bottom)
  .append("g")
 .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

下面一行代码让我困扰了一会儿,也是我认为代码里面能优化的部分(关于困扰我的原因,可以看一下 Stack Overflow 的这个问题)。

root = treeData[0];

相对于那些对 JavaScript 很熟的人来说,这可能想都不用想,这行代码做的是定义了我们的数据将用到的“树”。因为我们的数据是一个数组,数组的第一层是 treeData。treeData 的第一层的第一个对象的 name 属性是“Top Level”。这(第一个对象)是对象 0。因此我们的的起点是 treeData[0]。我们能确定的是,如果将声明改成这样:
root = treeData[0].children[0];
这意味着将 treeData 的第一层的第一个子节点 (child[0]) 作为根节点。结果,我们的树状图将是这样:
1418441347101572.png
由于“Level 2: A”是 “Top Level”的第一个子节点。
接下来,我们调用了如下的函数画出我们的树状图:
update(root);
这调用了 update 函数并使用 root 数据创建我们的树状图。
最后的部分显然就是 update 函数,这也是聚合我们已经声明的和画树状图的函数和数据。
这个过程的第一步是赋值我们的节点和连接:
var nodes = tree.nodes(root),
links = tree.links(nodes);
这里用到了我们之前声明的 tree 函数,并将其 d3.js magic 使用到我们的数据 (root) 上并确定了节点的详细信息,通过节点的详细信息,我们能确定连接的详细信息。
如果你想知道这一切是怎么做的,我恐怕就帮不上什么忙了,但是这一切的基础就是这个过程的结果是产生了一些节点的集合。每个节点具有一些特征,这些特征分别是:
.children:节点的子节点组成的数组;
.depth:深度(之前的一些篇幅讲过);
.id:标示每个节点的唯一数字;
.name:从数据获取到的 name 属性;
.parent:父节点的 name 值;
.x 和 .y:分别是节点在屏幕的 x 和 y 坐标;
通过这些节点数据就创建了一些联系节点的连接。每个连接由 .source 和 .target 组成,它们分别代表一个节点。
我们接下来确定了节点间的水平间距:
nodes.forEach(function(d) { d.y = d.depth * 180; });
这里用到了 node(每个节点通过 nodes = tree.nodes(root)已经确定) 的 depth 属性计算屏幕上 y 轴的位置。
这里的 depth 指的是树状图上相对于左边的根节点的位置。下面这张图展示了 depth 是怎样和树状图上的节点位置相关联的。
1418441348321060.png
所以,通过调整我们的“膨胀系数”(当前设定到 180 ),我们可以调整节点间的间距。例如,这是我们调整到 80 时候的情况:
1418441348526795.png
我们接下来定义了变量/函数 node ,这样我们才能在后面调用的时候,知道怎样通过正确的 id 选择到正确的对象。

var node = svg.selectAll("g.node")
   .data(nodes, function(d) { return d.id || (d.id = ++i); });

下面一段代码将变量/函数 nodeEnter 赋值为将一个节点添加到特定的位置的动作。

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

接着,我们用一段代码添加包含节点的圆(用 nodeEnter)。

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

使用 10 像素的半径和白色的填充。
接着,我们给每个节点添加文本:

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);

这一块整齐的代码将文本放置在有子节点的节点(d.children)的左边或者是放在没有子节点(d._children)的节点的右边。这对于做树状图也是一段显得冗余的代码,但是这个在本章结尾具有交互的版本中更有用。同时,这段代码也让文本对齐并确保是可见的。
接下来,我们声明了 link 变量/函数,并告诉他在具有唯一的 target id 的所有连接的基础上建立一个连接。

var link = svg.selectAll("path.link")
   .data(links, function(d) { return d.target.id; });

乍一看,这可能并不明显,因为我们只想画出节点和他的父节点间连接。因为根节点 (Top Level) 没有父节点,所以总的连接数应该比总节点数小 1 。因此,数据中只有具有与唯一的 target id 的节点的连接才需要生成。如果我们将上面的 .target 替换成 .source ,我们将只有两个唯一的 .source id。那么我们会得到下面这样:
blob.png
我们代码的最后一段将我们的连接添加为 diagnonal 路径(之前声明过):

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

剩下的就只有一些 HTML 标签需要闭合,这样我们就得到了树状图!
1418441347292487.png
别忘了,这个例子的完整代码在 github 上面,在 D3 Tips and Tricks 的附录,或者是 D3 Tips and Tricks 的代码样例集里面 (simple-tree-diagram.html)。示例在 bl.ocks.org 上面也能找到。

Tags: d3, tree, 树状图, svg
Archives QR Code Tip
QR Code for this page
Tipping QR Code