G6 生态树布局(Dendrogram)实战指南:配置详解与源码级原理

原创2026-09-23 13:23:02279 阅读
文章标签:数据可视化前端图表库

G6 生态树布局(Dendrogram)实战指南:配置详解与源码级原理

生态树(Dendrogram)是 G6 内置的树图布局之一,专为层次聚类数据的可视化设计,其核心特点是所有子节点被布局在同一层级,且布局过程不考虑节点尺寸(每个节点按 1px 参与计算)。本文以 packages/site/examples/layout/dendrogram 目录下的三个官方示例为主线,完整讲解生态树布局的配置方式、六种布局方向、辐射状模式与垂直布局实战技巧,并结合 packages/g6/src 源码剖析该布局在 G6 内部的注册、执行与数据转换原理。读完本文,你将能够在 G6 中快速搭建可交互的生态树、脑图式分类树与径向辐射树。

生态树布局的特点与适用场景

生态树布局适用于层次聚类数据的可视化。与 compact-box 紧凑盒、mindmap 脑图等树布局不同,它的两个关键特性是:

  1. 所有子节点布局在同一层级:同一父节点下的所有子节点会均匀排布在同一层(同一 rank),整体呈典型的"生态树"扇形或瀑布形态;
  2. 布局不考虑节点大小:布局算法将每个节点视为 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();
  });

使用辐射模式时有两点值得注意:

  1. 方向搭配建议:将 radial 设为 true 时,建议将 direction 保持为 'LR' 或 'RL' 以获得最佳效果(这是官方手册明确给出的推荐组合);
  2. 间距参数:辐射模式下节点沿圆周排布,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 };
}

从源码可以确认以下几点:

  1. 深度记录:默认的 getNodeData 会给每个节点写入 depth 字段,这对树布局计算层级至关重要;
  2. children 改写:默认转换会把节点数据中的 children 由"子节点对象数组"改写为"子节点 id 数组"(引用关系保留在图数据的 edges 中),这也是 G6 图数据节点可携带 children 字段引用子节点 id 的约定(可参考 数据文档);
  3. 可定制性:该函数接受第二个参数 getter,可分别覆写 getNodeData、getEdgeData、getChildren,例如自定义节点属性注入或按条件过滤子节点,适用于树数据中嵌套非标准字段名的场景。

布局在 G6 内部的注册与执行原理

从源码视角梳理 dendrogram 布局的完整接入链路,有助于理解其运行时机:

  1. 布局实现来源:dendrogram 的实现由 @antv/hierarchy 库提供,G6 在 packages/g6/src/layouts/index.ts 中统一导出 { compactBox, dendrogram, indented, mindmap },并在 packages/g6/src/exports.ts 中以 DendrogramLayout 命名对外导出;
  2. 内置注册:packages/g6/src/registry/build-in.ts 将 dendrogram 注册进 G6 内置布局注册表,因此在 layout.type: 'dendrogram' 时无需额外 register 即可直接使用;
  3. 树布局识别:packages/g6/src/utils/layout.ts 的 isTreeLayout 将 dendrogram 与其他三种树布局归为一类,用于运行时识别树图数据并触发对应的树布局处理逻辑;
  4. 前置布局(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 关闭入场动画,当节点数量较多时能显著缩短首帧渲染时间。

更多参考

登录后查看全文
G6