G6 生态树布局(Dendrogram)实战指南:配置详解与源码级原理
G6 生态树布局(Dendrogram)实战指南:配置详解与源码级原理
生态树(Dendrogram)是 G6 内置的树图布局之一,专为层次聚类数据的可视化设计,其核心特点是所有子节点被布局在同一层级,且布局过程不考虑节点尺寸(每个节点按 1px 参与计算)。本文以 packages/site/examples/layout/dendrogram 目录下的三个官方示例为主线,完整讲解生态树布局的配置方式、六种布局方向、辐射状模式与垂直布局实战技巧,并结合 packages/g6/src 源码剖析该布局在 G6 内部的注册、执行与数据转换原理。读完本文,你将能够在 G6 中快速搭建可交互的生态树、脑图式分类树与径向辐射树。
生态树布局的特点与适用场景
生态树布局适用于层次聚类数据的可视化。与 compact-box 紧凑盒、mindmap 脑图等树布局不同,它的两个关键特性是:
- 所有子节点布局在同一层级:同一父节点下的所有子节点会均匀排布在同一层(同一 rank),整体呈典型的"生态树"扇形或瀑布形态;
- 布局不考虑节点大小:布局算法将每个节点视为 1px 的点参与计算,因此当节点尺寸差异较大时,节点间距可能需要通过
nodeSep手动调大,避免视觉重叠。
从源码看,G6 将 dendrogram 与 compact-box、mindmap、indented 一起归类为"树图布局"(见 packages/g6/src/utils/layout.ts 中的 isTreeLayout 判断),这类布局在数据初始化前即会参与位置计算。
快速开始:最小可运行示例
官方示例 basic.js 给出了一个完整的左右方向(LR)生态树,完整代码如下:
import { Graph, treeToGraphData } from '@antv/g6';
/**
* If the node is a leaf node
* @param {*} d - node data
* @returns {boolean} - whether the node is a leaf node
*/
function isLeafNode(d) {
return !d.children || d.children.length === 0;
}
fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')
.then((res) => res.json())
.then((data) => {
const graph = new Graph({
container: 'container',
autoFit: 'view',
data: treeToGraphData(data),
node: {
style: {
labelText: (d) => d.id,
labelPlacement: (d) => (isLeafNode(d) ? 'right' : 'left'),
labelBackground: true,
ports: [{ placement: 'right' }, { placement: 'left' }],
},
animation: {
enter: false,
},
},
edge: {
type: 'cubic-horizontal',
animation: {
enter: false,
},
},
layout: {
type: 'dendrogram',
direction: 'LR', // H / V / LR / RL / TB / BT
nodeSep: 36,
rankSep: 250,
},
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],
});
graph.render();
});
这个示例蕴含了几个可复用的实战要点:
- 数据转换:布局消费的是树形数据,通过
treeToGraphData(data)将带children的树数据转换为 G6 的{ nodes, edges }图数据(转换细节见下文"数据准备"一节); - 标签位置差异化:通过
labelPlacement回调实现"叶子节点标签在右侧、非叶子节点标签在左侧",与 LR 方向(根在左、向右生长)的阅读习惯保持一致; - 连接桩与边类型:为节点配置
right/left两个ports,边使用水平三次曲线cubic-horizontal,保证边从节点左右两侧自然引出; - 关闭入场动画:
animation.enter: false关闭节点与边的入场动画,让布局结果立即呈现,适合大数据量的树; - 交互行为:
behaviors中同时启用画布拖拽(drag-canvas)、缩放(zoom-canvas)、节点拖拽(drag-element)与折叠/展开(collapse-expand),后者支持点击节点收起或展开其子树。
示例使用的数据为 algorithm-category.json 算法分类树,仓库测试数据集中同样提供了该数据集(见 packages/g6/tests/dataset/algorithm-category.json),可直接用于本地复现。
配置方式与配置项全解
生态树布局在 Graph 的 layout 字段中以 type: 'dendrogram' 启用,完整配置方式如下(与 DendrogramLayout.zh.md 手册一致):
const graph = new Graph({
layout: {
type: 'dendrogram',
direction: 'LR',
nodeSep: 30,
rankSep: 250,
radial: false,
},
});
各配置项的含义、类型、默认值与是否必选整理如下:
| 属性 | 描述 | 类型 | 默认值 | 必选 |
|---|---|---|---|---|
| type | 布局类型 | dendrogram |
- | ✓ |
| direction | 布局方向,可选值 | LR | RL | TB | BT | H | V |
LR |
|
| nodeSep | 节点间距,即同一层级节点之间的距离,单位为像素 | number | 20 | |
| rankSep | 层级间距,即不同层级之间的距离,单位为像素 | number | 200 | |
| radial | 是否启用辐射状布局 | boolean | false |
- nodeSep(节点间距):控制同一层级内相邻节点的间隔。由于生态树"每个节点按 1px 计算",当节点实际渲染尺寸较大或标签较长时,应适当调大该值(示例中
basic.js设为 36、vertical.js设为 50),防止同层节点与标签相互重叠; - rankSep(层级间距):控制相邻层级之间的距离。层级越深、树越宽时,过小的
rankSep会让边过于拥挤;示例中左右方向树取 250,垂直树取 120,辐射树取 140,可根据画布尺寸动态调整; - radial(辐射模式):见下文专节说明。
需要说明的是:布局实现来自 @antv/hierarchy 库并包装进 G6 内置注册表(见 packages/g6/src/layouts/index.ts 与 packages/g6/src/registry/build-in.ts),因此 nodeSep、rankSep 等参数在底层对应 @antv/hierarchy 树布局算法的同名字段。
direction:六种布局方向
direction 决定树的生长方向,可选值共 6 种:
TB:根节点在上,向下逐层布局;BT:根节点在下,向上逐层布局;LR:根节点在左,向右逐层布局(默认值,也是最常用的阅读方向);RL:根节点在右,向左逐层布局;H:根节点在中间,左右对称水平布局(类似镜像的脑图风格);V:根节点在中间,上下对称垂直布局。
选择方向的实践建议:
- 展示"根 → 叶"的继承或分类关系时,优先使用
LR/RL横向布局,横向画布更利于放置较长的标签文本; - 展示层级深浅、深度优先结构时,
TB/BT纵向布局更直观; - 当树的左右两侧子树重要性相当、希望突出"中心根节点"时,选用对称的
H/V。
方向改变后,建议同步调整边的类型与连接桩位置以保持视觉一致:横向方向(LR/RL/H)搭配 cubic-horizontal 与左右 ports,纵向方向(TB/BT/V)搭配 cubic-vertical 与上下 ports,详见下方垂直布局示例。
radial:辐射状布局
将 radial 设为 true 后,节点将以根节点为中心呈辐射状分布,适合表现"围绕核心向外扩散"的层级结构。官方示例 radial.js 的完整代码如下:
import { Graph, treeToGraphData } from '@antv/g6';
fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')
.then((res) => res.json())
.then((data) => {
const graph = new Graph({
container: 'container',
autoFit: 'view',
data: treeToGraphData(data),
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],
node: {
style: {
labelText: (d) => d.id,
labelBackground: true,
},
animation: {
enter: false,
},
},
layout: {
type: 'dendrogram',
radial: true,
nodeSep: 40,
rankSep: 140,
},
});
graph.render();
});
使用辐射模式时有两点值得注意:
- 方向搭配建议:将
radial设为true时,建议将direction保持为'LR'或'RL'以获得最佳效果(这是官方手册明确给出的推荐组合); - 间距参数:辐射模式下节点沿圆周排布,
nodeSep决定同环上的节点间隔、rankSep决定环与环之间的半径差,示例中nodeSep: 40、rankSep: 140是一个兼顾密度与可读性的取值。
仓库的场景示例 radial-dendrogram.js 还展示了辐射树在完整业务场景(含更多交互配置)中的应用,可作为进阶参考。
垂直布局实战:标签旋转与方向适配
官方示例 vertical.js 演示了 TB(自上而下)方向的垂直生态树,其亮点在于通过 labelTransform 旋转叶子节点标签,解决纵向布局下长文本标签互相遮挡的问题:
import { Graph, treeToGraphData } from '@antv/g6';
/**
* If the node is a leaf node
* @param {*} d - node data
* @returns {boolean} - whether the node is a leaf node
*/
function isLeafNode(d) {
return !d.children || d.children.length === 0;
}
fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')
.then((res) => res.json())
.then((data) => {
const graph = new Graph({
container: 'container',
autoFit: 'view',
data: treeToGraphData(data),
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],
node: {
style: (d) => {
const style = {
labelText: d.id,
labelPlacement: 'right',
labelOffsetX: 2,
labelBackground: true,
ports: [{ placement: 'top' }, { placement: 'bottom' }],
};
if (isLeafNode(d)) {
Object.assign(style, {
labelTransform: [
['rotate', 90],
['translate', 18],
],
labelBaseline: 'center',
labelTextAlign: 'left',
});
}
return style;
},
animation: {
enter: false,
},
},
edge: {
type: 'cubic-vertical',
animation: {
enter: false,
},
},
layout: {
type: 'dendrogram',
direction: 'TB', // H / V / LR / RL / TB / BT
nodeSep: 50,
rankSep: 120,
},
});
graph.render();
});
这段代码中值得细读的三个技巧:
- 方向联动:
direction: 'TB'时,节点连接桩切换为top/bottom,边类型切换为cubic-vertical,使父子边沿垂直方向平滑引出; - 叶子标签旋转:
isLeafNode判断叶子节点后,通过labelTransform: [['rotate', 90], ['translate', 18]]将标签顺时针旋转 90 度并向下平移 18px,同时配合labelBaseline: 'center'、labelTextAlign: 'left'校准对齐方式——这样底层叶子节点的标签横向排布,避免了纵向树中标签叠压; - 非叶子节点样式分离:节点
style以函数形式返回,普通节点与叶子节点共享基础样式、差异部分按条件合并,是树图节点样式按层级区分的通用写法。
数据准备:treeToGraphData 与树数据模型
生态树(以及其他树布局)消费的数据结构是嵌套的树数据(TreeData),每个节点可携带 children 子节点数组;而 G6 的 Graph 需要扁平的 { nodes, edges } 图数据。官方示例统一使用 treeToGraphData 完成这一转换,其实现位于 packages/g6/src/utils/tree.ts:
export function treeToGraphData(treeData: TreeData, getter?: TreeDataGetter): GraphData {
const {
getNodeData = (datum: TreeData, depth: number) => {
datum.depth = depth;
if (!datum.children) return datum as NodeData;
const { children, ...restDatum } = datum;
return { ...restDatum, children: children.map((child) => child.id) } as NodeData;
},
getEdgeData = (source: TreeData, target: TreeData) => ({ source: source.id, target: target.id }),
getChildren = (datum: TreeData) => datum.children || [],
} = getter || {};
const nodes: NodeData[] = [];
const edges: EdgeData[] = [];
dfs(
treeData,
(node, depth) => {
nodes.push(getNodeData(node, depth));
const children = getChildren(node);
for (const child of children) {
edges.push(getEdgeData(node, child));
}
},
(node) => getChildren(node),
'TB',
);
return { nodes, edges };
}
从源码可以确认以下几点:
- 深度记录:默认的
getNodeData会给每个节点写入depth字段,这对树布局计算层级至关重要; - children 改写:默认转换会把节点数据中的
children由"子节点对象数组"改写为"子节点 id 数组"(引用关系保留在图数据的edges中),这也是 G6 图数据节点可携带children字段引用子节点 id 的约定(可参考 数据文档); - 可定制性:该函数接受第二个参数
getter,可分别覆写getNodeData、getEdgeData、getChildren,例如自定义节点属性注入或按条件过滤子节点,适用于树数据中嵌套非标准字段名的场景。
布局在 G6 内部的注册与执行原理
从源码视角梳理 dendrogram 布局的完整接入链路,有助于理解其运行时机:
- 布局实现来源:
dendrogram的实现由@antv/hierarchy库提供,G6 在 packages/g6/src/layouts/index.ts 中统一导出{ compactBox, dendrogram, indented, mindmap },并在 packages/g6/src/exports.ts 中以DendrogramLayout命名对外导出; - 内置注册:packages/g6/src/registry/build-in.ts 将
dendrogram注册进 G6 内置布局注册表,因此在layout.type: 'dendrogram'时无需额外register即可直接使用; - 树布局识别:packages/g6/src/utils/layout.ts 的
isTreeLayout将dendrogram与其他三种树布局归为一类,用于运行时识别树图数据并触发对应的树布局处理逻辑; - 前置布局(preLayout):packages/g6/src/runtime/options.ts 中,
dendrogram与mindmap会被默认设置为preLayout: true,即在元素初始化前完成位置计算。源码注释同时指出:"下列布局的标签位置待适配,需要手动配置 preLayout false"——如果你的场景中对dendrogram的标签位置做了精细自定义(如labelTransform),且出现标签位置与布局预期不符的情况,可以显式设置preLayout: false让布局在元素初始化后进行二次计算。
与交互、边类型的配套建议
生态树常用于可探索的分类/聚类场景,官方三个示例的配套配置可总结为以下模式:
- 交互行为:
drag-canvas(画布平移)、zoom-canvas(滚轮缩放)、drag-element(拖拽节点)是通用三件套;当树层级较深时,务必加入collapse-expand行为,支持点击非叶子节点折叠/展开子树,这也是树图场景中控制可视范围的核心交互; - 边的类型选择:横向方向配
cubic-horizontal、纵向方向配cubic-vertical,曲线边比直线边更贴合"从父节点引出、汇入子节点"的视觉语义; - 数据量考量:示例均设置
animation.enter: false关闭入场动画,当节点数量较多时能显著缩短首帧渲染时间。
更多参考
- 生态树布局完整手册(含配置图解):DendrogramLayout.zh.md
- 本文对应的三个官方示例:basic.js、vertical.js、radial.js,示例元信息见 meta.json
- 场景级辐射树案例:radial-dendrogram.js
- 数据转换工具源码:packages/g6/src/utils/tree.ts
- 布局注册与树布局识别源码:packages/g6/src/registry/build-in.ts、packages/g6/src/utils/layout.ts