d3-hierarchy
许多数据集从从本质上是嵌套结构的。考虑在 geographic entities 应用场景中,比如人口普查,人口结构以及国家和州;企业和政府的组织结构;文件系统和软件包。甚至非层级的数据也可以被组合成层级数据结构,比如 k-means clustering(k - means 聚类) or phylogenetic trees (生态系统树)。
这个模块实现了几种经典的对层次结构数据的可视化技术:
Node-link diagrams(节点-链接图) 对节点和边使用离散的标记来展示拓扑结构,比如节点展示为圆并且父子节点之间使用线连接。Installing
NPM
安装:npm install d3-hierarchy
. 此外还可以下载 latest release. 可以直接从 d3js.org 以 standalone library 或作为 D3 4.0 的一部分直接载入. 支持 AMD
, CommonJS
和基础的标签引入形式,如果使用标签引入则会暴露 d3
全局变量:
<script src="https://d3js.org/d3-hierarchy.v1.min.js"></script>
<script>
var treemap = d3.treemap();
</script>
API Reference
- Hierarchy
在计算层次布局之前,你需要一个根节点。如果你的数据已经是层次结构,比如
JSON
。你可以直接将其传递给 d3.hierarchy; 此外,你可以重新排列扁平数据,比如将CSV
使用 d3.stratify 重组为层次结构数据。d3.hierarchy(data[, children]) <源码>
根据指定的层次结构数据构造一个根节点。指定的数据 data 必须为一个表示根节点的对象。比如:
{ "name": "Eve", "children": [ { "name": "Cain" }, { "name": "Seth", "children": [ { "name": "Enos" }, { "name": "Noam" } ] }, { "name": "Abel" }, { "name": "Awan", "children": [ { "name": "Enoch" } ] }, { "name": "Azura" } ] }
指定的 children 访问器会为每个数据进行调用,从根 data 开始,并且必须返回一个数组用以表示当前数据的子节点,返回
null
表示当前数据没有子节点。如果没有指定 children 则默认为:function children(d) { return d.children; }
返回的节点和每一个后代会被附加如下属性:
- node.data - 关联的数据,由 constructor 指定.
- node.depth - 当前节点的深度, 根节点为
0
. - node.height - 当前节点的高度, 叶节点为
0
. - node.parent - 当前节点的父节点, 根节点为
null
. - node.children - 当前节点的孩子节点(如果有的话); 叶节点为
undefined
. - node.value - 当前节点以及 descendants(后代节点) 的总计值; 可以通过 node.sum 和 node.count 计算.
这个方法也可以用来测试一个节点是否是
instanceof d3.hierarchy
并且可以用来扩展节点原型链。node.ancestors() <源码>
返回祖先节点数组,第一个节点为自身,然后依次为从自身到根节点的所有节点。
node.descendants() <源码>
返回后代节点数组,第一个节点为自身,然后依次为所有子节点的拓扑排序。
node.leaves() <源码>
返回叶节点数组,叶节点是没有孩子节点的节点。
node.path(target) <源码>
返回从当前 node 到指定 target 节点的最短路径。路径从当前节点开始,遍历到当前 node 和 target 节点共同最近祖先,然后到 target 节点。这个方法对 hierarchical edge bundling(分层边捆绑) 很有用。
node.links() <源码>
返回当前 node 的
links
数组, 其中每个 link 是一个定义了 source 和 target 属性的对象。每个link
的source
为父节点,target
为子节点。node.sum(value) <源码>
从当前 node 开始以 post-order traversal 的次序为当前节点以及每个后代节点调用指定的 value 函数,并返回当前 node。这个过程会为每个节点附加 node.value 数值属性,属性值是当前节点的
value
值和所有后代的value
的合计,函数的返回值必须为非负数值类型。value 访问器会为当前节点和每个后代节点进行评估,包括内部结点;如果你仅仅想让叶节点拥有内部值,则可以在遍历到叶节点时返回0
。例如 这个例子,使用如下设置等价于 node.count:root.sum(function(d) { return d.value ? 1 : 0; });
在进行层次布局之前必须调用 node.sum 或 node.count,因为布局需要 node.value 属性,比如 d3.treemap。
API
支持方法的 method chaining (链式调用), 因此你可以在计算布局之前调用 node.sum 和 node.sort, 随后生成 descendant nodes 数组, 比如:var treemap = d3.treemap() .size([width, height]) .padding(2); var nodes = treemap(root .sum(function(d) { return d.value; }) .sort(function(a, b) { return b.height - a.height || b.value - a.value; })) .descendants();
这个例子假设
node
数据包含value
字段.node.count() <源码>
计算当前 node 下所有叶节点的数量,并将其分配到 node.value 属性, 同时该节点的所有后代节点也会被自动计算其所属下的所有叶节点数量。如果 node 为叶节点则
count
为1
。该操作返回当前 node。对比 node.sum。node.sort(compare) <源码>
以 pre-order traversal 的次序对当前 node 以及其所有的后代节点的子节点进行排序,指定的 compare 函数以 a 和 b 两个节点为参数。返回当前 node。如果 a 在 b 前面则应该返回一个比
0
小的值,如果 b 应该在 a 前面则返回一个比0
大的值,否则不改变 a 和 b 的相对位置。参考 array.sort 获取更多信息。与 node.sum 不同,compare 函数传递两个 nodes 实例而不是两个节点的数据。例如,如果数据包含
value
属性,则根据节点以及此节点所有的后续节点的聚合值进行降序排序,就像 circle-packing 一样:root .sum(function(d) { return d.value; }) .sort(function(a, b) { return b.value - a.value; });
类似的,按高度降序排列 (与任何后代的距离最远) 然后按值降序排列,是绘制 treemaps 和 icicles 的推荐排序方式:
root .sum(function(d) { return d.value; }) .sort(function(a, b) { return b.height - a.height || b.value - a.value; });
先对高度进行降序,然后按照
id
进行升序排列,是绘制 trees 和 dendrograms 的推荐排序方式:root .sum(function(d) { return d.value; }) .sort(function(a, b) { return b.height - a.height || a.id.localeCompare(b.id); });
如果想在可视化布局中使用排序,则在调用层次布局之前,必须先调用 node.sort;参考 Stratify
考虑如下关系的列表:
Name Parent Eve Cain Eve Seth Eve Enos Seth Noam Seth Abel Eve Awan Eve Enoch Awan Azura Eve 这些名称时独一无二的,因此我们可以将上述表示层级数据的列表表示为
CSV
文件:name,parent Eve, Cain,Eve Seth,Eve Enos,Seth Noam,Seth Abel,Eve Awan,Eve Enoch,Awan Azura,Eve
解析
CSV
使用 d3.csvParse:var table = d3.csvParse(text);
然后返回:
[ {"name": "Eve", "parent": ""}, {"name": "Cain", "parent": "Eve"}, {"name": "Seth", "parent": "Eve"}, {"name": "Enos", "parent": "Seth"}, {"name": "Noam", "parent": "Seth"}, {"name": "Abel", "parent": "Eve"}, {"name": "Awan", "parent": "Eve"}, {"name": "Enoch", "parent": "Awan"}, {"name": "Azura", "parent": "Eve"} ]
转为层次结构数据:
var root = d3.stratify() .id(function(d) { return d.name; }) .parentId(function(d) { return d.parent; }) (table);
返回:
此时的层次数据才可以被传递给层次布局来可视化,比如 d3.tree.
d3.stratify() <源码>
使用默认的配置构造一个新的
stratify
(分层) 操作。stratify(data) <源码>
根据执行的扁平 data 生成一个新的层次结构数据.
stratify.id([id]) <源码>
如果指定了 id, 则将
id
访问器设置为指定的函数并返回分层操作。否则返回当前的id
访问器, 默认为:function id(d) { return d.id; }
id
访问器会为每个输入到 stratify operator 的数据元素进行调用,并传递当前数据 d 以及当前索引 i. 返回的字符串会被用来识别节点与 parent id(父节点id
) 之间的关系。对于叶节点,id
可能是undefined
; 此外id
必须是唯一的。(Null
和空字符串等价于undefined
).stratify.parentId([parentId]) <源码>
如果指定了 parentId,则将当前父节点
id
访问器设置为给定的函数,并返回分层操作。否则返回当前父节点id
访问器, 默认为:function parentId(d) { return d.parentId; }
父节点
id
访问器会为每个输入的元素进行调用,并传递当前元素 d 以及当前索引 i. 返回的字符串用来识别节点与 Clustercluster layout 布局用来生成 dendrograms(系统树):节点-连接图中所有的叶节点都放在相同的深度上。
Dendograms
通常不如 tidy trees 紧凑,但是当所有的叶子都在同一水平时是有用的,比如分层聚类或 phylogenetic tree diagrams(系统树图)。d3.cluster() <源码>
使用默认的配置创建一个新的系统树布局.
cluster(root) <源码>
对指定的 root hierarchy 进行布局,并为 root 以及它的每一个后代附加两个属性:
- node.x - 节点的 x- 坐标
- node.y - 节点的 y- 坐标
节点的 x 和 y 坐标可以是任意的坐标系统;例如你可以将 x 视为角度而将 y 视为半径来生成一个 radial layout(径向布局)。你也可以在布局之前使用 root.sort 进行排序操作。
cluster.size([size]) <源码>
如果指定了 size 则设置当前系统树布局的尺寸为一个指定的二元数值类型数组,表示 [width, height] 并返回当前系统树布局。如果 size 没有指定则返回当前系统树布局的尺寸,默认为 [1, 1]。如果返回的布局尺寸为
null
时则表示实际的尺寸根据 node size 确定。坐标 x 和 y 可以是任意的坐标系统;例如如果要生成一个 radial layout(径向布局) 则可以将其设置为 [360, radius] 用以表示角度范围为360°
并且半径范围为 radius。cluster.nodeSize([size]) <源码>
如果指定了 size 则设置系统树布局的节点尺寸为指定的数值二元数组,表示为 [width, height] 并返回当前系统树布局。如果没有指定 size 则返回当前节点尺寸,默认为
null
。如果返回的尺寸为null
则表示使用 layout size 来自动计算节点大小。当指定了节点尺寸时,根节点的位置总是位于 ⟨0, 0⟩。cluster.separation([separation]) <源码>
如果指定了 seperation, 则设置间隔访问器为指定的函数并返回当前系统树布局。如果没有指定 seperation 则返回当前的间隔访问器,默认为:
function separation(a, b) { return a.parent == b.parent ? 1 : 2; }
间隔访问器用来设置相邻的两个叶节点之间的间隔。指定的间隔访问器会传递两个叶节点 a 和 b,并且必须返回一个期望的间隔值。这些节点通常是兄弟节点,如果布局将两个节点放置到相邻的位置,则可以通过配置间隔访问器设置相邻节点之间的间隔控制其期望的距离。
Tree
tree 布局基于 Reingold–Tilford “tidy” algorithm 用来生成节点-链接树,由 Buchheim et al. 等人进行了时间上的优化,紧凑树布局比 dendograms 空间上看来更紧凑。
d3.tree() <源码>
使用默认的设置创建一个新的树布局。
tree(root) <源码>
对指定的 root hierarchy 进行布局,并为 root 以及它的每一个后代附加两个属性:
- node.x - 节点的 x- 坐标
- node.y - 节点的 y- 坐标
节点的 x 和 y 坐标可以是任意的坐标系统;例如你可以将 x 视为角度而将 y 视为半径来生成一个 radial layout(径向布局)。你也可以在布局之前使用 root.sort 进行排序操作。
tree.size([size]) <源码>
如果指定了 size 则设置当前系统树布局的尺寸为一个指定的二元数值类型数组,表示 [width, height] 并返回当前树布局。如果 size 没有指定则返回当前系统树布局的尺寸,默认为 [1, 1]。如果返回的布局尺寸为
null
时则表示实际的尺寸根据 node size 确定。坐标 x 和 y 可以是任意的坐标系统;例如如果要生成一个 radial layout(径向布局) 则可以将其设置为 [360, radius] 用以表示角度范围为360°
并且半径范围为 radius。tree.nodeSize([size]) <源码>
如果指定了 size 则设置系统树布局的节点尺寸为指定的数值二元数组,表示为 [width, height] 并返回当前树布局。如果没有指定 size 则返回当前节点尺寸,默认为
null
。如果返回的尺寸为null
则表示使用 layout size 来自动计算节点大小。当指定了节点尺寸时,根节点的位置总是位于 ⟨0, 0⟩。tree.separation([separation]) <源码>
如果指定了 seperation, 则设置间隔访问器为指定的函数并返回当前树布局。如果没有指定 seperation 则返回当前的间隔访问器,默认为:
function separation(a, b) { return a.parent == b.parent ? 1 : 2; }
一种更适合于径向布局的变体,可以按比例缩小半径差距:
function separation(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; }
间隔访问器用来设置相邻的两个节点之间的间隔。指定的间隔访问器会传递两个节点 a 和 b,并且必须返回一个期望的间隔值。这些节点通常是兄弟节点,如果布局将两个节点放置到相邻的位置,则可以通过配置间隔访问器设置相邻节点之间的间隔控制其期望的距离。
Treemap
参考 1991年 Ben Shneiderman 的介绍,treemap 根据每个节点的值递归的将区域划分为矩形。
D3
的treemap
的实现支持可扩展的 Treemap Tiling几种内置的铺设方法,用来供 Partition
partition layout 用来生成邻接图:一个节点链接树图的空间填充变体。与使用连线链接节点与父节点不同,在这个布局中节点会被绘制为一个区域(可以是弧也可以是矩形),并且其位置反应了其在层次结构中的相对位置。节点的尺寸被编码为一个可度量的维度,这个在节点-链接图中很难表示。
d3.partition()
使用默认的设置创建一个分区图布局。
partition(root) <源码>
对指定的 root Pack
Enclosure(打包)
图使用嵌套来表示层次结构。最里层表示叶节点的圆的大小用来编码定量的维度值。每个圆都表示当前节点的近似累计大小,因为有空间浪费以及变形;仅仅只有叶节点可以准确的比较。尽管 circle packing 与 treemap 相比不能高效的利用空间,但是能更突出的表示层次结构。d3.pack()
使用默认的设置创建一个打包布局。
pack(root) <源码>
对 root hierarchy 进行布局,root 节点以及每个后代节点会被附加以下属性:
- node.x - 节点中心的 x- 坐标
- node.y - 节点中心的 y- 坐标
- node.r - 圆的半径
在传入布局之前必须调用 root.sum。可能还需要调用 root.sort 对节点进行排序。
pack.radius([radius]) <源码>
如果指定了 radius 则将半径访问器设置为指定的函数并返回
pack
布局。如果没有指定 radius 则返回当前半径访问器,默认为null
, 表示叶节点的圆的半径由叶节点的 node.value(通过 node.sum 计算) 得到;然后按照比例缩放以适应 layout size。如果半径访问器不为null
则叶节点的半径由函数精确指定。pack.size([size]) <源码>
如果指定了 size 则将当前
pack
布局的尺寸设置为指定的二元数值类型数组: [width, height] 并返回当前pack
布局。如果没有指定 size 则返回当前的尺寸,默认为 [1, 1]。pack.padding([padding]) <源码>
如果指定了 padding 则设置布局的间隔访问器为指定的数值或函数并返回
pack
布局。如果没有指定 padding 则返回当前的间隔访问器,默认为常量0
。当兄弟节点被打包时,节点之间的间隔会被设置为指定的间隔;外层包裹圆与字节点之间的间隔也会被设置为指定的间隔。如果没有指定 explicit radius(明确的半径),则间隔是近似的,因为需要一个双通道算法来适应 layout size:这些圆首先没有间隙;一个用于计算间隔的比例因子会被计算;最后,这些圆被填充了。d3.packSiblings(circles) <源码>
打包指定的一组 circles,每个圆必须包含 circle.r 属性用来表示圆的半径。每个圆会被附加以下属性:
- circle.x - 圆中心的 x- 坐标
- circle.y - 圆中心的 y- 坐标
这些圆是根据 Wang et al. 等人的算法来定位。
d3.packEnclose(circles) <源码>
计算能包裹一组 circles 的 smallest circle(最小圆),每个圆必须包含 circle.r 属性表示半径,以及 circle.x 以及 circle.y 属性表示圆的中心,最小包裹圆的实现基于 Matoušek-Sharir-Welzl algorithm。(也可以参考 Apollonius’ Problem)