
Antv G6 Graph
- 680 installs
- 454 repo stars
- Updated July 31, 2026
- antvis/chart-visualization-skills
antv-g6-graph is an AntV agent skill that implements interactive graph and network visualizations with G6 nodes, edges, layouts, and events for developers building dashboards, knowledge graphs, and agent tooling UIs.
About
antv-g6-graph is a chart-visualization skill from AntV for building interactive network diagrams with the G6 graph engine. The skill guides developers through configuring nodes, edges, layout algorithms, interaction events, and rendering patterns suited to dashboards, dependency maps, knowledge graphs, and agent workflow UIs. Developers reach for antv-g6-graph when they need force-directed or hierarchical graph views, draggable nodes, edge styling, zoom and pan handlers, or embeddable graph widgets in React or vanilla JavaScript frontends. It complements other AntV chart-visualization skills by focusing specifically on topology and relationship data instead of time-series or cartesian charts. Outputs are G6 graph configurations, component scaffolding, and event-handler patterns ready to integrate into web applications.
- G6 graph setup and theming
- Layout algorithms for networks
- Node-edge interaction handlers
- Performance tuning for large graphs
- Export and embedding patterns
Antv G6 Graph by the numbers
- 680 all-time installs (skills.sh)
- Ranked #510 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/antvis/chart-visualization-skills --skill antv-g6-graphAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 680 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 31, 2026 |
| Repository | antvis/chart-visualization-skills ↗ |
How do you build interactive network graphs with AntV G6?
Implement interactive graph and network visualizations with AntV G6—nodes, edges, layouts, and events—for dashboards, knowledge graphs, and agent tooling UIs.
Who is it for?
Frontend developers building relationship visualizations, knowledge graphs, or agent workflow maps who standardize on AntV G6.
Skip if: Skip antv-g6-graph when you need time-series or bar charts—use other AntV chart skills—or when a static diagram without interaction suffices.
When should I use this skill?
Trigger when a developer asks for AntV G6 graphs, network visualizations, node-edge layouts, knowledge graph UI, or interactive topology charts.
What you get
G6 graph configurations, node-edge data models, layout setups, and event-driven interactive network UI components.
- g6 graph config
- node-edge data model
- interactive chart component
Files
G6 v5 图可视化代码生成技能
核心约束(必须遵守)
初始化规范
container参数必填,传入 DOM 元素 ID 字符串或 DOM 元素对象- 使用
new Graph({...})构造函数,不得使用new G6.Graph()(v4 写法) - 所有配置在构造函数中一次性完成,不得事后多次调用配置方法覆盖
graph.render()返回 Promise,异步渲染;若需等待完成请await graph.render()
数据结构规范
- 数据格式:
{ nodes: [...], edges: [...], combos?: [...] } - 每个节点必须有唯一
id(字符串);业务数据放在data字段 - 边必须有
source和target,值为节点id - 禁止使用 v4 的
graph.data()方法传数据
节点/边样式规范
- 样式通过
node.style/edge.style配置,支持静态值和回调函数 - 回调函数签名:
(datum: NodeData | EdgeData) => value - 标签文本通过
style.labelText设置(不是label或labelCfg) - 节点大小通过
style.size设置(单个数值或 [width, height] 数组)
布局规范
layout配置放在 Graph 选项中:{ type: 'force', ... }force布局不支持preventOverlap/nodeSize(G6 v4 参数,v5 静默忽略);防重叠请改用d3-force+collide- 树形布局(mindmap, compact-box, dendrogram, indented)需要树形数据或
treeToGraphData()转换 - 力导向布局异步运行,
graph.render()后会持续迭代 - `nodeStrength` 必须为非负数(≥ 0),负值会导致布局计算异常或节点行为不可预测
交互行为规范
behaviors为字符串数组或配置对象数组- 常用行为字符串简写:
'drag-canvas','zoom-canvas','drag-element','click-select' - G6 v5 移除了 Mode(模式)概念,所有 behavior 直接在数组中配置
- 复杂配置使用对象形式:
{ type: 'click-select', multiple: true }
插件规范
plugins为数组,与behaviors类似- 简写:
'minimap','grid-line','tooltip' - 复杂配置:
{ type: 'tooltip', getContent: (e, items) => '...' }
---
禁止的错误模式
❌ 使用 v4 API
// 错误:v4 chainable API
const graph = new G6.Graph({ ... });
graph.data(data);
graph.render();
graph.node((node) => ({ ... })); // v4 回调
// 正确:v5 构造函数
const graph = new Graph({
container: 'container',
data: { nodes: [...], edges: [...] },
node: { style: { ... } },
});
graph.render();❌ 错误的节点 data 结构
// 错误:直接在顶层放业务属性
{ id: 'node1', label: 'Node 1', value: 100 }
// 正确:业务属性放在 data 字段
{ id: 'node1', data: { label: 'Node 1', value: 100 } }❌ 错误的标签配置
// 错误:v4 labelCfg
node: {
labelCfg: { style: { fill: '#333' } }
}
// 正确:v5 style.labelText
node: {
style: {
labelText: (d) => d.data.label,
labelFill: '#333',
labelFontSize: 14,
}
}❌ behaviors 使用 Mode 概念
// 错误:v4 modes
modes: {
default: ['drag-canvas', 'zoom-canvas'],
edit: ['create-edge'],
}
// 正确:v5 直接 behaviors 数组
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],❌ 自定义节点 render() 中读取 attributes.data → 白屏
// 错误:attributes 是计算后的样式对象,不含节点 data,访问 data.color 抛 TypeError
render(attributes, container) {
const { data } = attributes; // undefined
const fill = data.color; // TypeError → 白屏
}
// 正确:通过 node.style 回调把 data 字段映射为自定义样式属性
// ① Graph 配置
node: {
type: 'my-node',
style: { color: (d) => d.data.color },
},
// ② render() 中直接从 attributes 读取
render(attributes, container) {
const { color = '#1783FF' } = attributes; // ✅
}❌ 使用 extend 注册自定义节点
// 错误:extend 已从 G6 v5 正式版移除,导入后调用会报 "extend is not a function"
import { Graph, extend } from '@antv/g6';
const extendedGraph = extend(Graph, {
nodes: { 'my-node': MyNodeFn },
});
// 错误:v4 的 group.addShape() API
const MyNode = (node) => (model) => {
const group = node.group();
group.addShape('circle', { attrs: { r: 20 } });
};
// 正确:BaseNode 类 + register()
import { BaseNode, Circle, ExtensionCategory, Graph, register } from '@antv/g6';
class MyNode extends BaseNode {
render(attributes, container) {
super.render(attributes, container);
this.upsert('key', Circle, { cx: 0, cy: 0, r: 20, fill: '#1783FF' }, container);
}
}
register(ExtensionCategory.NODE, 'my-node', MyNode);
const graph = new Graph({ node: { type: 'my-node' } });❌ 缺少 container
// 错误:遗漏 container
const graph = new Graph({ });
// 正确:container 必填,值为字符串 ID 或 DOM 元素
const graph = new Graph({ container: 'container' });
// 或传入 DOM 元素
const graph = new Graph({ container: document.getElementById('container') });常见变体错误:container: container(把字符串 ID 当变量名使用,变量未定义 → ReferenceError → 白屏)❌ autoFit: 'view' 配合异步力导向布局导致白屏
// 错误:combo-combined / force / d3-force 等布局是异步迭代的
// autoFit 在布局迭代开始前执行,节点全堆在原点,包围盒为零 → 缩放异常 → 白屏
const graph = new Graph({
autoFit: 'view', // ❌ 异步布局下不能在此设置
layout: { type: 'combo-combined' },
});
graph.render();
// 正确:不设置 autoFit,在 AFTER_LAYOUT 事件后调用 fitView
import { Graph, GraphEvent } from '@antv/g6';
const graph = new Graph({
layout: { type: 'combo-combined' },
});
graph.on(GraphEvent.AFTER_LAYOUT, () => graph.fitView({ padding: 20 }));
graph.render();同步布局(dagre、grid、circular等)不受此影响,可以直接用autoFit: 'view'。
---
基础结构模板
import { Graph } from '@antv/g6';
const graph = new Graph({
// 1. 容器
container: 'container', // DOM id 或 HTMLElement
autoFit: 'view', // 可选:'center' | 'view' | false
// 2. 数据
data: {
nodes: [
{ id: 'n1', data: { label: '节点1' } },
{ id: 'n2', data: { label: '节点2' } },
],
edges: [
{ source: 'n1', target: 'n2' },
],
},
// 3. 节点样式
node: {
type: 'circle', // 节点类型
style: {
size: 40,
fill: '#1783FF',
stroke: '#fff',
lineWidth: 2,
labelText: (d) => d.data.label,
labelPlacement: 'bottom',
},
},
// 4. 边样式
edge: {
type: 'line',
style: {
stroke: '#aaa',
lineWidth: 1,
endArrow: true,
},
},
// 5. 布局
layout: {
type: 'force',
preventOverlap: true,
nodeSize: 40,
},
// 6. 交互
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],
// 7. 插件(可选)
plugins: ['grid-line'],
// 8. 主题(可选)
theme: 'light', // 'light' | 'dark'
});
graph.render();---
图类型选择指南
| 图类型 | 推荐布局 | 典型场景 |
|---|---|---|
| 网络图/关系图 | force / fruchterman | 社交网络、知识图谱 |
| 层次/流程图 | dagre / antv-dagre | 组织架构、工作流 |
| 树形图 | compact-box / mindmap | 文件树、思维导图 |
| 环形图 | circular | 循环依赖、环形关系 |
| 网格图 | grid | 棋盘布局、矩阵关系 |
| 同心圆 | concentric | 中心辐射关系 |
| 辐射布局 | radial | 以某节点为中心的辐射 |
---
内置节点类型
| 类型名 | 形状 | 适用场景 |
|---|---|---|
circle | 圆形 | 通用节点,网络图 |
rect | 矩形 | 流程图、UML |
ellipse | 椭圆 | 通用,强调纵向 |
diamond | 菱形 | 决策节点 |
hexagon | 六边形 | 蜂窝布局 |
triangle | 三角形 | 特殊标记 |
star | 五角星 | 特殊标记、评分 |
donut | 环形 | 带进度的节点 |
image | 图片 | 头像、图标节点 |
html | HTML | 富文本自定义节点 |
---
内置边类型
| 类型名 | 形状 | 适用场景 |
|---|---|---|
line | 直线 | 简单图、拓扑图 |
cubic | 三次贝塞尔曲线 | 通用,弧形效果 |
cubic-horizontal | 水平三次曲线 | 水平流程图 |
cubic-vertical | 垂直三次曲线 | 垂直流程图 |
quadratic | 二次贝塞尔曲线 | 轻量弧形边 |
polyline | 折线 | 正交布局 |
loop | 自环 | 节点自身的循环 |
---
内置布局算法
| 布局名 | 类型 | 特点 |
|---|---|---|
force | 力导向 | 物理模拟,自然分布 |
d3-force | 力导向 | 基于 D3,可配置力类型 |
fruchterman | 力导向 | 快速,支持 GPU 加速 |
force-atlas2 | 力导向 | 大规模图,聚类效果好 |
dagre | 层次 | DAG,自动分层 |
antv-dagre | 层次 | AntV 优化版 Dagre |
circular | 环形 | 节点排列为圆形 |
concentric | 同心圆 | 按属性值分环 |
grid | 网格 | 规则网格排列 |
radial | 辐射 | 以某节点为中心辐射 |
mds | 降维 | 保持节点相对距离 |
random | 随机 | 调试用 |
compact-box | 树形 | 紧凑树,节省空间 |
mindmap | 树形 | 思维导图风格 |
dendrogram | 树形 | 树状图 |
indented | 树形 | 缩进树 |
---
内置交互行为
| 行为名 | 描述 |
|---|---|
drag-canvas | 拖拽画布 |
zoom-canvas | 滚轮缩放画布 |
scroll-canvas | 滚轮平移画布 |
drag-element | 拖拽节点/边/combo |
drag-element-force | 力导向图中拖拽节点 |
click-select | 点击选中元素 |
brush-select | 框选元素 |
lasso-select | 套索选择 |
hover-activate | 悬停激活元素 |
collapse-expand | 折叠/展开节点(树图) |
create-edge | 交互式创建边 |
focus-element | 聚焦元素(缩放到指定元素) |
fix-element-size | 缩放时保持元素大小不变 |
auto-adapt-label | 自动显示/隐藏标签(防重叠) |
optimize-viewport-transform | 大规模图视口优化 |
---
内置插件
| 插件名 | 描述 |
|---|---|
grid-line | 网格背景线 |
background | 背景颜色/图片 |
watermark | 水印 |
minimap | 缩略图导航 |
legend | 图例 |
tooltip | 元素提示框 |
toolbar | 工具栏(缩放、撤销等) |
contextmenu | 右键菜单 |
history | 撤销/重做 |
timebar | 时间轴过滤 |
fisheye | 鱼眼放大效果 |
edge-bundling | 边捆绑 |
edge-filter-lens | 边过滤镜头 |
hull | 元素轮廓包围 |
bubble-sets | 气泡集合 |
snapline | 对齐辅助线 |
fullscreen | 全屏 |
---
元素状态(States)
G6 v5 内置 5 种状态:selected、active、highlight、inactive、disabled
// 在 Graph 配置中为状态设置样式
node: {
style: {
fill: '#1783FF',
},
state: {
selected: {
fill: '#ff6b6b',
stroke: '#ff4d4d',
lineWidth: 3,
},
hover: {
fill: '#40a9ff',
},
},
},
// 动态设置状态
graph.setElementState('node1', 'selected');
graph.setElementState('node1', ['selected', 'highlight']);
graph.setElementState('node1', []); // 清除所有状态---
主题系统
// 内置主题
const graph = new Graph({
theme: 'light', // 默认
// theme: 'dark',
});
// 动态切换主题
graph.setTheme('dark');
graph.render();---
数据操作 API
// 添加元素
graph.addNodeData([{ id: 'n3', data: { label: '新节点' } }]);
graph.addEdgeData([{ source: 'n1', target: 'n3' }]);
// 更新元素
graph.updateNodeData([{ id: 'n1', style: { fill: 'red' } }]);
// 删除元素
graph.removeNodeData(['n3']);
// 更新数据后需要重新渲染
graph.draw();---
常见使用模式
数据驱动样式(推荐)
node: {
style: {
size: (d) => d.data.size || 30,
fill: (d) => {
const colorMap = { type1: '#1783FF', type2: '#FF6B6B', type3: '#52C41A' };
return colorMap[d.data.type] || '#ccc';
},
labelText: (d) => d.data.name,
},
},调色板(Palette)映射
node: {
palette: {
type: 'group', // 按分类映射颜色
field: 'category', // 数据中的分类字段
color: 'tableau10', // 内置色板名
},
},连续数值映射节点大小
transforms: [
{
type: 'map-node-size',
field: 'value',
range: [16, 60],
},
],平行边处理
transforms: [
{
type: 'process-parallel-edges',
offset: 15,
},
],
edge: {
type: 'quadratic',
},---
数据操作 API 速查
// 增
graph.addNodeData([{ id: 'n3', data: { label: '新节点' } }]);
graph.addEdgeData([{ source: 'n1', target: 'n3' }]);
graph.draw();
// 删
graph.removeNodeData(['n3']); // 关联边自动删除
graph.draw();
// 改
graph.updateNodeData([{ id: 'n1', data: { label: '更新' } }]);
graph.draw();
// 查
const node = graph.getNodeData('n1');
const selected = graph.getElementDataByState('node', 'selected');
const zoom = graph.getZoom();
// 视口
await graph.fitView({ padding: 20 });
await graph.focusElement('n1', { duration: 500 });
await graph.zoomTo(1.5);
// 状态
graph.setElementState('n1', 'selected');
graph.setElementState('n1', []); // 清除
// 销毁
graph.destroy();---
事件监听速查
// 元素事件(node/edge/combo + 事件类型)
graph.on('node:click', (e) => console.log(e.target.id));
graph.on('edge:pointerover', (e) => graph.setElementState(e.target.id, 'active'));
graph.on('canvas:click', () => { /* 点击空白 */ });
// 生命周期事件
import { GraphEvent } from '@antv/g6';
graph.on(GraphEvent.AFTER_RENDER, () => console.log('渲染完成'));
graph.on(GraphEvent.AFTER_LAYOUT, () => console.log('布局完成'));---
Reference 文档索引
核心
- `g6-core-graph-init`:Graph 初始化完整配置
- `g6-core-data-structure`:数据结构规范
- `g6-core-graph-api`:Graph 实例 API(增删改查、视口、状态)
- `g6-core-events`:事件系统(元素事件、画布事件、生命周期)
- `g6-core-custom-element`:自定义节点/边(register + BaseNode/BaseEdge)
- `g6-core-transforms-animation`:数据变换(map-node-size)与动画配置
节点类型
- `g6-node-circle`:圆形(通用)
- `g6-node-rect`:矩形(流程图)
- `g6-node-image`:图片节点
- `g6-node-diamond-ellipse-hexagon`:菱形/椭圆/六边形
- `g6-node-star-triangle-donut`:五角星/三角形/环形进度
- `g6-node-html`:HTML 富文本节点
- `g6-node-react`:React/Vue 自定义节点(@antv/g6-extension-react)
Combo
- `g6-combo-overview`:Combo 分组(circle/rect,折叠展开)
边类型
- `g6-edge-line`:直线边
- `g6-edge-cubic`:三次贝塞尔曲线边
- `g6-edge-cubic-directional`:有向三次曲线(cubic-horizontal 水平 / cubic-vertical 垂直)
- `g6-edge-polyline`:折线边
- `g6-edge-quadratic-loop`:二次曲线与自环边
布局
- `g6-layout-force`:力导向(force/d3-force)
- `g6-layout-dagre`:层次/流程图(dagre)
- `g6-layout-circular`:环形
- `g6-layout-grid`:网格
- `g6-layout-mindmap`:思维导图
- `g6-layout-advanced`:同心圆/辐射/mds/fruchterman
- `g6-layout-combo-fishbone`:复合布局(combo-combined)+ 鱼骨布局(fishbone)
数据变换
- `g6-core-transforms-animation`:map-node-size 与动画配置
- `g6-transform-parallel-edges-radial`:平行边处理(process-parallel-edges)+ 径向标签(place-radial-labels)
交互行为
- `g6-behavior-click-select`:点击选中
- `g6-behavior-drag-element`:拖拽节点
- `g6-behavior-canvas-nav`:画布拖拽+缩放
- `g6-behavior-hover-activate`:悬停激活
- `g6-behavior-lasso-collapse`:套索选择 + 折叠展开
- `g6-behavior-create-edge-focus`:创建边 + 聚焦元素
- `g6-behavior-advanced`:fix-element-size / auto-adapt-label / drag-element-force
插件
- `g6-plugin-tooltip`:悬停提示框
- `g6-plugin-minimap`:缩略图
- `g6-plugin-contextmenu-toolbar`:右键菜单 + 工具栏
- `g6-plugin-history-legend`:撤销重做 + 图例
- `g6-plugin-fisheye-hull-watermark`:鱼眼放大 + 轮廓包围 + 水印
- `g6-plugin-timebar-gridline`:时间轴 + 网格线
- `g6-plugin-background-snapline`:画布背景(background)+ 对齐线(snapline)
- `g6-plugin-edge-bundling-bubble`:边绑定(edge-bundling)+ 气泡集(bubble-sets)
- `g6-plugin-fullscreen-title`:全屏(fullscreen)+ 图标题(title)
状态与主题
- `g6-state-overview`:元素状态系统
- `g6-theme-overview`:主题系统
场景模板
- `g6-pattern-network-graph`:网络关系图
- `g6-pattern-tree-graph`:树形图/组织架构
- `g6-pattern-flow-chart`:流程图
缩放时固定元素尺寸(fix-element-size)
当用户缩小画布时,保持标签、边框等关键视觉元素的绝对像素尺寸,防止字体变得过小难以阅读。
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
data: {
nodes: Array.from({ length: 20 }, (_, i) => ({
id: `n${i}`,
{ label: `节点${i}` },
})),
edges: Array.from({ length: 15 }, (_, i) => ({
source: `n${i % 10}`,
target: `n${(i + 5) % 20}`,
})),
},
node: {
type: 'circle',
style: {
size: 36,
fill: '#1783FF',
stroke: '#fff',
lineWidth: 2,
labelText: (d) => d.data.label,
labelPlacement: 'bottom',
labelFontSize: 12,
},
},
layout: { type: 'force', preventOverlap: true },
behaviors: [
'drag-canvas',
'zoom-canvas',
'drag-element',
{
type: 'fix-element-size',
// 只在缩小时启用(zoom < 1)
enable: (event) => event.data.scale < 1,
// 固定节点的标签尺寸
node: [
{ shape: 'label' }, // 固定标签(字号、位置不随缩放变化)
{ shape: 'key', fields: ['lineWidth'] }, // 固定节点边框宽度
],
// 固定边的标签和线宽
edge: [
{ shape: 'label' },
{ shape: 'key', fields: ['lineWidth'] },
{ shape: 'halo', fields: ['lineWidth'] },
],
},
],
});
graph.render();fix-element-size 配置参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enable | `boolean \ | ((event) => boolean)` | (e) => e.data.scale < 1 |
node | FixShapeConfig[] | — | 节点中要固定的形状列表 |
edge | FixShapeConfig[] | — | 边中要固定的形状列表 |
combo | FixShapeConfig[] | — | combo 中要固定的形状列表 |
reset | boolean | false | 是否在重绘时恢复原始样式 |
FixShapeConfig:
interface FixShapeConfig {
shape: string; // 形状名:'key' | 'label' | 'halo' | 'icon' | ...
fields?: string[]; // 只固定特定属性(如 lineWidth),不指定则固定所有
}---
自动隐藏重叠标签(auto-adapt-label)
当视口空间不足时,根据节点重要性(中心度)自动隐藏低优先级标签,避免文字重叠。
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
data: {
nodes: Array.from({ length: 50 }, (_, i) => ({
id: `n${i}`,
{ label: `节点${i}`, degree: Math.floor(Math.random() * 10) },
})),
edges: Array.from({ length: 60 }, (_, i) => ({
source: `n${i % 25}`,
target: `n${(i * 3 + 7) % 50}`,
})),
},
node: {
type: 'circle',
style: {
size: 20,
fill: '#1783FF',
stroke: '#fff',
labelText: (d) => d.data.label,
labelPlacement: 'bottom',
labelFontSize: 11,
},
},
layout: { type: 'force', preventOverlap: true, nodeSize: 20 },
behaviors: [
'drag-canvas',
'zoom-canvas',
{
type: 'auto-adapt-label',
// 标签间距检测 padding(px)
padding: 4,
// 节点重要性排序:使用中心度,度数高的节点标签优先显示
sortNode: {
type: 'degree', // 'degree' | 'betweenness' | 'closeness' | 'eigenvector'
direction: 'both', // 'in' | 'out' | 'both'
},
// 防抖延迟(ms)
throttle: 100,
},
],
});
graph.render();auto-adapt-label 配置参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
padding | number | 0 | 标签碰撞检测额外间距 |
sortNode | `NodeCentralityOptions \ | SortFn` | { type: 'degree' } |
sortEdge | SortFn | — | 边排序函数 |
sortCombo | SortFn | — | combo 排序函数 |
throttle | number | 100 | 防抖延迟(ms) |
---
力导向布局中拖拽节点(drag-element-force)
在 d3-force 布局运行时,拖拽节点同时更新布局力场,实现真实的物理效果。
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
data: {
nodes: Array.from({ length: 20 }, (_, i) => ({
id: `n${i}`,
{ label: `N${i}` },
})),
edges: Array.from({ length: 25 }, (_, i) => ({
source: `n${i % 15}`,
target: `n${(i * 2 + 3) % 20}`,
})),
},
node: {
type: 'circle',
style: {
size: 30,
fill: '#1783FF',
stroke: '#fff',
labelText: (d) => d.data.label,
labelPlacement: 'center',
labelFill: '#fff',
},
},
layout: {
type: 'd3-force', // 必须使用 d3-force 或 d3-force-3d
link: { distance: 80 },
many: { strength: -200 },
},
behaviors: [
'drag-canvas',
'zoom-canvas',
{
type: 'drag-element-force',
// true:拖拽后节点固定在当前位置(不再参与布局)
// false:松开后继续参与布局力场
fixed: false,
},
],
});
graph.render();drag-element-force 配置参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
fixed | boolean | false | 拖拽松开后节点是否固定 |
注意:drag-element-force只支持d3-force/d3-force-3d布局,与普通force布局不兼容。普通力导向图请使用drag-element。
核心概念
三种画布导航行为:
drag-canvas:鼠标拖拽移动画布zoom-canvas:滚轮缩放画布scroll-canvas:滚轮滚动画布(替代 zoom,适合有滚动条的页面)
最小可运行示例
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
data: {
nodes: [
{ id: 'node1' },
{ id: 'node2' },
{ id: 'node3' },
{ id: 'node4' },
{ id: 'node5' },
],
edges: [
{ id: 'edge1', source: 'node1', target: 'node2' },
{ id: 'edge2', source: 'node1', target: 'node3' },
{ id: 'edge3', source: 'node2', target: 'node4' },
{ id: 'edge4', source: 'node3', target: 'node5' },
],
},
layout: { type: 'grid' },
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],
});
graph.render();常用配置
完整参数配置
behaviors: [
{
type: 'drag-canvas',
// 允许拖拽的方向
direction: 'both', // 'both' | 'x' | 'y'
// 拖拽边界限制
range: Infinity, // 超出边界的距离限制
// 按键触发
trigger: {
up: ['ArrowUp'],
down: ['ArrowDown'],
left: ['ArrowLeft'],
right: ['ArrowRight'],
},
},
{
type: 'zoom-canvas',
// 缩放范围
range: [0.1, 10], // [最小缩放, 最大缩放]
// 动画
animation: { duration: 200 },
},
],防止拖拽画布时误触节点
behaviors: [
{
type: 'drag-canvas',
// 只在画布背景上拖拽(避免与节点拖拽冲突)
enable: (event) => event.targetType === 'canvas',
},
'drag-element',
],键盘方向键移动画布
behaviors: [
{
type: 'drag-canvas',
trigger: {
up: ['ArrowUp'],
down: ['ArrowDown'],
left: ['ArrowLeft'],
right: ['ArrowRight'],
},
},
'zoom-canvas',
],适配有页面滚动条的场景
// 页面有滚动条时,滚轮默认滚动页面而不是缩放图
// 使用 scroll-canvas 替代 zoom-canvas
behaviors: [
'drag-canvas',
'scroll-canvas', // 滚轮滚动画布(上下左右)
// 按住 Ctrl 时缩放
{
type: 'zoom-canvas',
key: 'ctrl', // 按住 Ctrl + 滚轮 才缩放
},
'drag-element',
],程序控制视口
// 缩放到指定比例
graph.zoomTo(1.5);
graph.zoomTo(1.5, true); // 带动画
// 恢复默认缩放
graph.zoomTo(1);
// 平移画布
graph.translateBy(100, 50); // 相对移动
graph.translateTo([400, 300]); // 移动到绝对位置
// 自适应视图
graph.fitView(); // 缩放到全图可见
graph.fitCenter(); // 居中但不缩放
// 聚焦某个节点
graph.focusElement('node1');常见错误与修正
错误1:边数据缺少唯一 id 导致重复边冲突
错误现象:Edge already exists: 12-20
原因分析:G6 5.x 中,如果边数据没有显式指定 id,系统会自动以 ${source}-${target} 作为边的 ID。当通过随机数生成边时,可能产生相同 source-target 组合的重复边,导致 ID 冲突报错。
// ❌ 错误示例:随机生成边,可能产生重复的 source-target 组合
const edges = [];
for (let i = 0; i < 34; i++) {
const target = Math.floor(Math.random() * 34);
if (target !== i) {
edges.push({ source: `${i}`, target: `${target}` });
// 如果同一 source-target 被添加两次,ID "i-target" 重复,报错
}
}修正方案1:为每条边显式指定唯一 id
// ✅ 正确示例:为每条边指定唯一 id
const edges = [];
let edgeIndex = 0;
for (let i = 0; i < 34; i++) {
const target = Math.floor(Math.random() * 34);
if (target !== i) {
edges.push({
id: `edge-${edgeIndex++}`, // 显式指定唯一 id
source: `${i}`,
target: `${target}`,
});
}
}修正方案2:生成边时去重,避免相同 source-target 重复出现
// ✅ 正确示例:用 Set 去重,避免重复边
const edgeSet = new Set();
const edges = [];
for (let i = 0; i < 34; i++) {
const target = Math.floor(Math.random() * 34);
const key = `${i}-${target}`;
if (target !== i && !edgeSet.has(key)) {
edgeSet.add(key);
edges.push({ source: `${i}`, target: `${target}` });
}
}修正方案3(推荐):直接使用明确的静态数据,不依赖随机生成
// ✅ 推荐:使用确定性数据,避免随机带来的不确定性
const data = {
nodes: Array.from({ length: 34 }, (_, i) => ({ id: `${i}` })),
edges: [
{ source: '0', target: '1' },
{ source: '0', target: '2' },
{ source: '1', target: '3' },
// ... 明确指定的边列表,无重复
],
};错误2:最小示例代码语法错误
错误现象:代码中 data 字段缺失或语法不完整导致空白渲染。
原因:Graph 构造函数中 data 字段是必须的,且必须包含 nodes 和 edges 数组。
// ❌ 错误示例:缺少 data 字段
const graph = new Graph({
container: 'container',
{ nodes: [...], edges: [...] }, // 语法错误,缺少 data: 键名
behaviors: ['drag-canvas'],
});
// ✅ 正确示例
const graph = new Graph({
container: 'container',
data: {
nodes: [{ id: 'node1' }, { id: 'node2' }],
edges: [{ source: 'node1', target: 'node2' }],
},
behaviors: ['drag-canvas'],
});错误3:treeToGraphData 未定义
错误现象:treeToGraphData is not defined
原因:treeToGraphData 是 G6 提供的工具函数,用于将树形结构数据转换为图数据,需要从 @antv/g6 中显式导入,不能直接使用。
// ❌ 错误示例:未导入直接使用
const data = treeToGraphData(treeData);
// ✅ 正确示例:先导入再使用
import { Graph, treeToGraphData } from '@antv/g6';
const data = treeToGraphData(treeData);
const graph = new Graph({
container: 'container',
data,
behaviors: ['drag-canvas', 'zoom-canvas'],
});
graph.render();错误4:画布渲染空白
常见原因及修正:
1. 容器尺寸为 0:确保容器 DOM 元素有明确的宽高,或在 Graph 配置中指定 width 和 height。 2. data 为空:确保 data.nodes 数组不为空。 3. 未调用 render():必须显式调用 graph.render() 才会渲染。 4. autoFit 配置:使用 autoFit: 'view' 可自动适配视图,避免图形超出画布范围不可见。
// ✅ 完整可运行示例
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
autoFit: 'view',
data: {
nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }],
edges: [
{ source: 'node1', target: 'node2' },
{ source: 'node2', target: 'node3' },
],
},
layout: { type: 'circular' },
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],
});
graph.render();核心概念
click-select 让用户通过点击选中节点/边,支持:
- 选中状态标记(默认状态名
selected) - 邻居节点/边联动高亮
- 多选(Shift/Ctrl + 点击)
- 点击空白取消选中
最小可运行示例
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
width: 640,
height: 480,
data: {
nodes: [
{ id: 'n1', data: { label: 'A' } },
{ id: 'n2', data: { label: 'B' } },
{ id: 'n3', data: { label: 'C' } },
],
edges: [
{ source: 'n1', target: 'n2' },
{ source: 'n2', target: 'n3' },
],
},
node: {
type: 'circle',
style: {
size: 40,
fill: '#1783FF',
labelText: (d) => d.data.label,
labelPlacement: 'center',
labelFill: '#fff',
},
state: {
selected: {
fill: '#ff4d4f',
stroke: '#cf1322',
lineWidth: 3,
halo: true,
haloFill: '#ff4d4f',
haloOpacity: 0.2,
},
},
},
layout: { type: 'force', preventOverlap: true },
behaviors: [
'drag-canvas',
'zoom-canvas',
'click-select', // 字符串简写
],
});
graph.render();常用变体
完整配置(含邻居高亮)
behaviors: [
'drag-canvas',
'zoom-canvas',
{
type: 'click-select',
// 支持多选(按住 Shift 或 Ctrl 点击)
multiple: true,
// 触发方式
trigger: ['click'], // 'click' | 'dblclick'
// 选中状态名
state: 'selected',
// 邻居状态名
neighborState: 'highlight',
// 未选中元素的状态名
unselectedState: 'inactive',
// 展开几跳的邻居(0=只选自身)
degree: 1,
// 点击回调
onClick: (event) => {
const { targetType, target } = event;
if (targetType === 'node') {
console.log('选中节点:', target.id);
}
},
},
],
// 配套状态样式
node: {
state: {
selected: { fill: '#ff4d4f', lineWidth: 3 },
highlight: { fill: '#ffa940', opacity: 1 },
inactive: { opacity: 0.3 },
},
},
edge: {
state: {
highlight: { stroke: '#ffa940', lineWidth: 2 },
inactive: { opacity: 0.2 },
},
},点击后展示详情面板
// 监听选中事件
graph.on('node:click', (event) => {
const nodeId = event.target.id;
const nodeData = graph.getNodeData(nodeId);
// 更新 UI 面板
document.getElementById('detail-panel').innerHTML = `
<h3>${nodeData.data.name}</h3>
<p>${nodeData.data.description}</p>
`;
});通过 API 设置选中状态
// 选中特定节点
graph.setElementState('n1', 'selected');
// 多状态叠加
graph.setElementState('n1', ['selected', 'highlight']);
// 清除状态
graph.setElementState('n1', []);
// 获取当前选中节点
const selectedNodes = graph.getElementDataByState('node', 'selected');常见错误
错误1:配置了 click-select 但状态样式未定义
// ❌ 只有行为,没有状态样式,节点点击后无视觉反馈
behaviors: ['click-select'],
// ✅ 同时配置状态样式
behaviors: ['click-select'],
node: {
state: {
selected: {
fill: '#ff4d4f',
lineWidth: 3,
},
},
},错误2:point 事件与 click-select 冲突
// click-select 内部会消费 click 事件
// 若需要自定义 click 逻辑,使用 onClick 回调
behaviors: [
{
type: 'click-select',
onClick: (event) => {
// 自定义处理
},
},
],交互式创建边(create-edge)
允许用户通过拖拽或点击在两个节点之间创建新边。
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
width: 640,
height: 480,
data: {
nodes: [
{ id: 'n1', data: { label: 'A' } },
{ id: 'n2', data: { label: 'B' } },
{ id: 'n3', data: { label: 'C' } },
],
edges: [],
},
node: {
type: 'circle',
style: {
size: 40,
fill: '#1783FF',
stroke: '#fff',
lineWidth: 2,
labelText: (d) => d.data.label,
labelPlacement: 'center',
labelFill: '#fff',
// 端口(精确连接点)
ports: [
{ key: 'top', placement: 'top' },
{ key: 'bottom', placement: 'bottom' },
{ key: 'left', placement: 'left' },
{ key: 'right', placement: 'right' },
],
},
},
edge: {
type: 'line',
style: {
stroke: '#aaa',
lineWidth: 1.5,
endArrow: true,
},
},
layout: { type: 'circular' },
behaviors: [
'drag-canvas',
'zoom-canvas',
'drag-element',
{
type: 'create-edge',
trigger: 'drag', // 'drag'(拖拽)| 'click'(点击源→点击目标)
style: {
// 创建过程中临时边的样式
stroke: '#1783FF',
lineWidth: 2,
lineDash: [4, 2],
endArrow: true,
},
// 边创建完成的回调
onFinish: (edgeData) => {
console.log('新建边:', edgeData.source, '->', edgeData.target);
// 可在此为新边追加业务数据
graph.updateEdgeData([{
...edgeData,
{ weight: 1, label: '新连接' },
}]);
graph.draw();
},
// 返回 undefined 取消创建,返回修改后的 data 允许创建
onCreate: (edgeData) => {
if (edgeData.source === edgeData.target) return undefined; // 禁止自环
return edgeData;
},
},
],
});
graph.render();create-edge 配置参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
trigger | `'drag' \ | 'click'` | 'drag' |
style | EdgeStyleProps | — | 创建中的临时边样式 |
onFinish | (edge: EdgeData) => void | — | 边创建完成回调 |
onCreate | `(edge: EdgeData) => EdgeData \ | undefined` | — |
enable | `boolean \ | ((event) => boolean)` | true |
---
聚焦元素(focus-element)
点击元素后平滑动画将视口移动到该元素的中心位置。
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
width: 640,
height: 480,
data: {
nodes: Array.from({ length: 30 }, (_, i) => ({
id: `n${i}`,
{ label: `节点${i}`, x: Math.random() * 2000, y: Math.random() * 2000 },
})),
edges: [],
},
node: {
type: 'circle',
style: {
size: 30,
fill: '#1783FF',
stroke: '#fff',
labelText: (d) => d.data.label,
labelPlacement: 'bottom',
},
},
layout: { type: 'random', width: 2000, height: 2000 },
behaviors: [
'drag-canvas',
'zoom-canvas',
{
type: 'focus-element',
// 动画配置
animation: {
easing: 'ease-in-out',
duration: 600,
},
// 启用条件(默认点击任意元素均触发聚焦)
enable: true,
},
],
});
graph.render();通过 API 聚焦元素
// 以动画方式将视口移动到指定节点
await graph.focusElement('n5', {
easing: 'ease-in-out',
duration: 500,
});
// 搜索后聚焦
document.getElementById('search').addEventListener('input', async (e) => {
const keyword = e.target.value;
const node = graph.getNodeData().find(n => n.data.label.includes(keyword));
if (node) {
await graph.focusElement(node.id, { duration: 500 });
graph.setElementState(node.id, 'selected');
}
});focus-element 配置参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
animation | ViewportAnimationEffectTiming | { easing: 'ease-in', duration: 500 } | 视口动画配置 |
enable | `boolean \ | ((event) => boolean)` | true |
核心概念
drag-element:拖拽节点到指定位置,其他节点不动(适合非力导向布局)drag-element-force:拖拽时物理模拟继续(适合力导向布局)
重要注意事项
边数据不能重复
G6 中每条边必须唯一(相同 source + target 的边不能重复添加),否则会抛出 Edge already exists: {source}-{target} 错误。
生成边数据时必须做去重处理,不能使用随机方式直接 push 边,需要用 Set 或 Map 记录已存在的边。
// ❌ 错误:随机生成边,可能产生重复
const edges = [];
for (let i = 0; i < 34; i++) {
for (let j = 0; j < 3; j++) {
const target = Math.floor(Math.random() * 34);
edges.push({ source: `${i}`, target: `${target}` }); // 可能重复!
}
}
// ✅ 正确:用 Set 去重
const edges = [];
const edgeSet = new Set();
for (let i = 0; i < 34; i++) {
for (let j = 0; j < 3; j++) {
const target = Math.floor(Math.random() * 34);
const key = `${i}-${target}`;
const reverseKey = `${target}-${i}`;
if (target !== i && !edgeSet.has(key) && !edgeSet.has(reverseKey)) {
edgeSet.add(key);
edges.push({ source: `${i}`, target: `${target}` });
}
}
}数据应直接使用题目提供的数据
当题目提供了具体的节点和边数据时,应直接使用,不要自行随机生成,避免重复边等问题。
最小可运行示例
import { Graph } from '@antv/g6';
const data = {
nodes: [
{ id: '0' },
{ id: '1' },
{ id: '2' },
{ id: '3' },
{ id: '4' },
{ id: '5' },
],
edges: [
{ source: '0', target: '1' },
{ source: '0', target: '2' },
{ source: '1', target: '3' },
{ source: '2', target: '4' },
{ source: '3', target: '5' },
],
};
const graph = new Graph({
container: 'container',
autoFit: 'view',
data,
node: {
style: {
labelText: (d) => d.id,
labelFill: '#fff',
labelPlacement: 'center',
},
},
layout: { type: 'circular' },
behaviors: [
'drag-canvas',
'zoom-canvas',
'drag-element',
],
});
graph.render();常用变体
力导向图中的拖拽
behaviors: [
'drag-canvas',
'zoom-canvas',
'drag-element-force', // 力导向布局必须用 force 版
],
layout: { type: 'force', preventOverlap: true },完整配置
behaviors: [
'drag-canvas',
'zoom-canvas',
{
type: 'drag-element',
// 是否启用,默认可拖拽节点和 Combo
enable: (event) => ['node', 'combo'].includes(event.targetType),
// 拖拽动画
animation: true,
// 拖拽结束后的操作效果:'move' | 'link' | 'none'
dropEffect: 'move',
// 拖拽时隐藏关联边(提升性能):'none' | 'out' | 'in' | 'both' | 'all'
hideEdge: 'none',
// 拖拽时显示幽灵节点(影子节点)
shadow: true,
// 拖拽状态名
state: 'selected',
// 自定义鼠标样式
cursor: {
default: 'default',
grab: 'grab',
grabbing: 'grabbing',
},
},
],多选后批量拖拽
// 配合 click-select 实现多选拖拽
behaviors: [
'drag-canvas',
'zoom-canvas',
{
type: 'click-select',
multiple: true,
state: 'selected',
},
{
type: 'drag-element',
// 拖拽时会同时移动所有 selected 状态的节点
state: 'selected',
},
],常见错误与修正
错误1:力导向图用普通 drag-element
// ❌ 力导向图中拖拽后节点不参与物理模拟
layout: { type: 'force' },
behaviors: ['drag-element'], // 错误!
// ✅ 力导向图使用 drag-element-force
layout: { type: 'force' },
behaviors: ['drag-element-force'],错误2:随机生成边导致重复边报错
错误现象:Edge already exists: 12-20
原因:使用随机方式生成边数据时,可能产生相同 source + target 的重复边,G6 不允许重复边存在。
// ❌ 错误:随机生成可能产生重复边
const edges = [];
for (let i = 0; i < 34; i++) {
const numEdges = 2 + Math.floor(Math.random() * 2);
for (let j = 0; j < numEdges; j++) {
const target = Math.floor(Math.random() * 34);
if (target !== i) {
edges.push({ source: `${i}`, target: `${target}` }); // 可能重复!
}
}
}
// ✅ 正确方案1:直接使用题目提供的固定数据
const data = {
nodes: [{ id: '0' }, { id: '1' }, /* ... */ { id: '33' }],
edges: [
{ source: '0', target: '1' },
{ source: '0', target: '2' },
// ... 使用确定的、不重复的边数据
],
};
// ✅ 正确方案2:生成时用 Set 去重
const edges = [];
const edgeSet = new Set();
for (let i = 0; i < 34; i++) {
for (let j = i + 1; j < 34; j++) {
// 按顺序生成,天然不重复
if (Math.random() < 0.1) { // 控制边的密度
edgeSet.add(`${i}-${j}`);
edges.push({ source: `${i}`, target: `${j}` });
}
}
}错误3:节点数据中 label 字段位置错误
G6 5.x 中节点的 label 通过样式配置,不是在 data 字段中:
// ❌ 错误:G6 5.x 不支持直接在 data 中配置 label
nodes: [{ id: 'n1', label: 'A' }]
// ✅ 正确:通过 node.style.labelText 配置
node: {
style: {
labelText: (d) => d.id, // 或 d.data?.label
labelPlacement: 'center',
labelFill: '#fff',
},
},错误4:treeToGraphData 未导入
如果使用树形数据需要转换为图数据,必须从 @antv/g6 中导入 treeToGraphData:
// ❌ 错误:直接使用未导入的函数
data: treeToGraphData(treeData), // ReferenceError: treeToGraphData is not defined
// ✅ 正确:先导入再使用
import { Graph, treeToGraphData } from '@antv/g6';
const graph = new Graph({
data: treeToGraphData(treeData),
// ...
});最小可运行示例
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
width: 640,
height: 480,
data: {
nodes: [
{ id: 'n1', data: { label: 'A' } },
{ id: 'n2', data: { label: 'B' } },
{ id: 'n3', data: { label: 'C' } },
{ id: 'n4', data: { label: 'D' } },
],
edges: [
{ source: 'n1', target: 'n2' },
{ source: 'n1', target: 'n3' },
{ source: 'n2', target: 'n4' },
{ source: 'n3', target: 'n4' },
],
},
node: {
type: 'circle',
style: {
size: 40,
fill: '#1783FF',
labelText: (d) => d.data.label,
labelPlacement: 'center',
labelFill: '#fff',
cursor: 'pointer',
},
state: {
active: {
fill: '#ff7875',
halo: true,
haloFill: '#ff7875',
haloOpacity: 0.25,
haloLineWidth: 12,
},
inactive: {
opacity: 0.3,
},
},
},
edge: {
type: 'line',
style: { stroke: '#ccc', endArrow: true },
state: {
active: {
stroke: '#ff7875',
lineWidth: 3,
},
inactive: {
opacity: 0.2,
},
},
},
layout: { type: 'force' },
behaviors: [
'drag-canvas',
'zoom-canvas',
{
type: 'hover-activate',
degree: 1, // 高亮几跳邻居(1=直接邻居)
state: 'active', // 激活状态名
},
],
});
graph.render();参数参考
interface HoverActivateOptions {
degree?: number; // 邻居跳数,默认 1
state?: string; // 激活元素状态,默认 'active'
inactiveState?: string; // 未激活元素状态,默认 'inactive'
enable?: boolean | ((event) => boolean);
}套索选择(lasso-select)
允许用户绘制自由曲线选区,圈住的元素被标记为 selected 状态。
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
width: 640,
height: 480,
data: {
nodes: Array.from({ length: 20 }, (_, i) => ({
id: `n${i}`,
data: { label: `节点${i}` },
})),
edges: Array.from({ length: 15 }, (_, i) => ({
source: `n${i % 10}`,
target: `n${(i + 3) % 20}`,
})),
},
node: {
type: 'circle',
style: {
size: 30,
fill: '#1783FF',
stroke: '#fff',
labelText: (d) => d.data.label,
labelPlacement: 'bottom',
},
state: {
selected: {
fill: '#ff4d4f',
stroke: '#cf1322',
halo: true,
haloFill: '#ff4d4f',
haloOpacity: 0.2,
},
},
},
layout: { type: 'force', preventOverlap: true },
behaviors: [
'drag-canvas',
'zoom-canvas',
{
type: 'lasso-select',
// 鼠标右键拖拽触发套索(避免与拖拽画布冲突)
trigger: 'pointerdown',
// 套索样式
style: {
fill: 'rgba(99, 149, 255, 0.1)',
stroke: '#6395ff',
lineWidth: 1,
lineDash: [4, 2],
},
// 选中状态名
state: 'selected',
// 实时更新(拖拽过程中动态高亮)
immediately: false,
// 被选范围内的元素类型
itemTypes: ['node'], // 只选节点,不选边
},
],
});
graph.render();
// 获取所有选中节点
graph.on('canvas:pointerup', () => {
const selectedNodes = graph.getElementDataByState('node', 'selected');
console.log('选中节点:', selectedNodes.map(n => n.id));
});lasso-select 配置参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
trigger | string | 'pointerdown' | 触发事件 |
immediately | boolean | false | 拖拽时实时更新选中状态 |
state | string | 'selected' | 选中状态名 |
itemTypes | `('node' \ | 'edge' \ | 'combo')[]` |
style | PathStyleProps | — | 套索路径样式 |
---
折叠展开(collapse-expand)
点击/双击节点(树图)或 combo 折叠/展开子树。
import { Graph, treeToGraphData } from '@antv/g6';
const treeData = {
id: 'root',
data: { label: '根节点' },
children: [
{
id: 'branch1',
data: { label: '分支1' },
children: [
{ id: 'leaf1', data: { label: '叶子1' } },
{ id: 'leaf2', data: { label: '叶子2' } },
],
},
{
id: 'branch2',
data: { label: '分支2' },
children: [
{ id: 'leaf3', data: { label: '叶子3' } },
],
},
],
};
const graph = new Graph({
container: 'container',
width: 640,
height: 480,
data: treeToGraphData(treeData),
node: {
type: 'rect',
style: {
size: [100, 36],
fill: '#1783FF',
stroke: '#fff',
radius: 4,
labelText: (d) => d.data.label,
labelPlacement: 'center',
labelFill: '#fff',
},
},
edge: {
type: 'cubic-horizontal',
style: { stroke: '#aaa' },
},
layout: {
type: 'mindmap',
direction: 'H',
getHeight: () => 36,
getWidth: () => 100,
getVGap: () => 10,
getHGap: () => 60,
},
behaviors: [
'drag-canvas',
'zoom-canvas',
{
type: 'collapse-expand',
trigger: 'click', // 'click' | 'dblclick'
animation: true, // 折叠时带动画
// 折叠/展开回调
onCollapse: (id) => console.log('折叠:', id),
onExpand: (id) => console.log('展开:', id),
},
],
});
graph.render();collapse-expand 配置参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
trigger | `'click' \ | 'dblclick'` | 'dblclick' |
animation | boolean | true | 折叠/展开动画 |
enable | `boolean \ | ((event) => boolean)` | true |
align | boolean | true | 折叠后是否自动居中 |
onCollapse | (id: string) => void | — | 折叠完成回调 |
onExpand | (id: string) => void | — | 展开完成回调 |
通过 API 控制折叠展开
// 折叠节点及其子树
await graph.collapseElement('branch1');
// 展开
await graph.expandElement('branch1');
// 检查状态
console.log(graph.isCollapsed('branch1')); // true/false常见错误
错误:在非树图中使用 collapse-expand
// collapse-expand 只适用于树形数据(每个节点有唯一父节点)
// 在普通网络图中使用会导致意外行为
// ✅ 树图专用,配合 treeToGraphData 使用
import { treeToGraphData } from '@antv/g6';
data: treeToGraphData(treeData),
behaviors: [{ type: 'collapse-expand' }],自定义节点
基础结构
import {
BaseNode,
ExtensionCategory,
Graph,
register,
Rect,
Text,
Circle,
} from '@antv/g6';
class StatusNode extends BaseNode {
/**
* 绘制节点主体
* 重写 render() 获得完全控制权,需自行管理所有子形状
*/
render(attributes, container) {
super.render(attributes, container);
const [width, height] = this.getSize(attributes);
const { status, label } = attributes;
// 使用 upsert 方法创建/更新形状(第一参数为 key,第二参数为构造函数,第三参数为属性)
// 主体矩形(会替代默认的 key 形状)
this.upsert('key', Rect, {
x: -width / 2,
y: -height / 2,
width,
height,
fill: this.getStatusColor(status),
stroke: '#fff',
lineWidth: 2,
radius: 6,
}, container);
// 状态指示点
this.upsert('status-dot', Circle, {
cx: width / 2 - 8,
cy: -height / 2 + 8,
r: 5,
fill: status === 'online' ? '#52c41a' : '#ff4d4f',
}, container);
// 标签(覆盖默认标签行为)
this.upsert('label', Text, {
x: 0,
y: 0,
text: label || attributes.id,
fill: '#fff',
fontSize: 13,
fontWeight: 'bold',
textAlign: 'center',
textBaseline: 'middle',
}, container);
}
getStatusColor(status) {
const colors = { online: '#52c41a', offline: '#ff4d4f', idle: '#faad14' };
return colors[status] || '#1783FF';
}
// 返回节点默认大小
getDefaultStyle() {
return { size: [120, 50] };
}
}
// 注册自定义节点类型
register(ExtensionCategory.NODE, 'status-node', StatusNode);
// 使用
const graph = new Graph({
container: 'container',
data: {
nodes: [
{ id: 'server1', data: { label: 'Web Server', status: 'online' } },
{ id: 'server2', data: { label: 'DB Server', status: 'offline' } },
{ id: 'server3', data: { label: 'Cache', status: 'idle' } },
],
edges: [
{ source: 'server1', target: 'server2' },
{ source: 'server1', target: 'server3' },
],
},
node: {
type: 'status-node',
style: {
size: [130, 50],
// 自定义属性通过 style 回调映射
status: (d) => d.data.status,
label: (d) => d.data.label,
},
},
layout: { type: 'dagre', rankdir: 'LR' },
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],
});
graph.render();关键 API
// upsert(key, Shape, attrs, container) - 创建或更新子形状
this.upsert('shape-key', Rect, { x, y, width, height, fill }, container);
// 获取节点尺寸
const [width, height] = this.getSize(attributes);
// 获取 shapeMap(已渲染的所有形状)
const allShapes = this.shapeMap;
// 节点中心坐标(世界坐标系)
const { x, y } = this.getPosition();---
继承内置节点扩展(推荐)
对于简单的样式扩展(如添加动画、光晕效果),推荐继承内置节点(如 Circle、Rect)而非 BaseNode,可以复用内置节点的绘制逻辑:
import { Circle, ExtensionCategory, Graph, register } from '@antv/g6';
// 继承内置 Circle 节点,添加呼吸动画光晕
class BreathingCircle extends Circle {
// onCreate 在元素完成创建并执行完入场动画后调用
// 适合启动循环动画,避免与入场动画冲突
onCreate() {
const halo = this.shapeMap.halo;
if (halo) {
halo.animate([{ lineWidth: 0 }, { lineWidth: 20 }], {
duration: 1000,
iterations: Infinity,
direction: 'alternate',
});
}
}
}
register(ExtensionCategory.NODE, 'breathing-circle', BreathingCircle);
const graph = new Graph({
container: 'container',
data: {
nodes: [
{ id: 'node-0' },
{ id: 'node-1' },
{ id: 'node-2' },
{ id: 'node-3' },
],
},
node: {
type: 'breathing-circle',
style: {
size: 50,
halo: true, // 开启光晕形状
},
palette: ['#3875f6', '#efb041', '#ec5b56', '#72c240'],
},
layout: {
type: 'grid',
},
behaviors: ['drag-canvas', 'zoom-canvas'],
});
graph.render();生命周期钩子
自定义节点/边支持以下生命周期钩子:
class MyNode extends BaseNode {
/**
* 在元素完成创建并执行完入场动画后调用
* 适合启动循环动画、绑定事件等一次性初始化操作
*/
onCreate() {
const keyShape = this.shapeMap['key'];
// 启动呼吸动画
keyShape.animate(
[{ r: 20 }, { r: 25 }, { r: 20 }],
{ duration: 2000, iterations: Infinity }
);
}
/**
* 在元素更新并执行完过渡动画后调用
*/
onUpdate() {
console.log('Node updated:', this.id);
}
/**
* 在元素完成退场动画并销毁后调用
*/
onDestroy() {
console.log('Node destroyed:', this.id);
}
}---
自定义边
import {
BaseEdge,
ExtensionCategory,
Graph,
register,
Path,
} from '@antv/g6';
class ArrowEdge extends BaseEdge {
/**
* 返回边的 SVG Path 数据(必须实现)
* 使用 this.getEndpoints(attributes) 获取起点和终点坐标
*/
getKeyPath(attributes) {
// 获取起点和终点坐标(已考虑连接桩、节点边界等因素)
const [sourcePoint, targetPoint] = this.getEndpoints(attributes, false);
if (!sourcePoint || !targetPoint) return [['M', 0, 0]];
const [sx, sy] = sourcePoint;
const [tx, ty] = targetPoint;
// 折线路径:水平 -> 垂直 -> 水平
const midX = (sx + tx) / 2;
return [
['M', sx, sy],
['L', midX, sy],
['L', midX, ty],
['L', tx, ty],
];
}
}
register(ExtensionCategory.EDGE, 'arrow-edge', ArrowEdge);
const graph = new Graph({
// ...
edge: {
type: 'arrow-edge',
style: {
stroke: '#aaa',
lineWidth: 1.5,
endArrow: true,
},
},
});自定义边动画(蚂蚁线)
super.render() 后通过 this.shapeMap['key'] 拿到主形状,再调用 Web Animations API:
import { BaseEdge, ExtensionCategory, Graph, register } from '@antv/g6';
class DashEdge extends BaseEdge {
getKeyPath(attributes) {
const [sourcePoint, targetPoint] = this.getEndpoints(attributes);
if (!sourcePoint || !targetPoint) return [['M', 0, 0]];
const [sx, sy] = sourcePoint;
const [tx, ty] = targetPoint;
return [['M', sx, sy], ['L', tx, ty]];
}
render(attributes, container) {
super.render(attributes, container);
const keyShape = this.shapeMap['key'];
if (keyShape) {
keyShape.style.lineDash = [10, 10];
// 蚂蚁线:通过 lineDashOffset 偏移实现流动效果
keyShape.animate(
[{ lineDashOffset: 0 }, { lineDashOffset: -20 }],
{ duration: 1000, iterations: Infinity },
);
}
}
}
register(ExtensionCategory.EDGE, 'line-dash', DashEdge);
const graph = new Graph({
container: 'container',
data: {
nodes: [
{ id: 'n1', data: { label: '开始' } },
{ id: 'n2', data: { label: '结束' } },
],
edges: [{ source: 'n1', target: 'n2' }],
},
edge: {
type: 'line-dash',
style: { stroke: '#999', lineWidth: 2 },
},
behaviors: ['drag-canvas', 'zoom-canvas'],
});
graph.render();---
注册类型汇总
import { ExtensionCategory, register } from '@antv/g6';
// 注册自定义节点
register(ExtensionCategory.NODE, 'my-node', MyNodeClass);
// 注册自定义边
register(ExtensionCategory.EDGE, 'my-edge', MyEdgeClass);
// 注册自定义 combo
register(ExtensionCategory.COMBO, 'my-combo', MyComboClass);
// 注册自定义布局
register(ExtensionCategory.LAYOUT, 'my-layout', MyLayoutClass);
// 注册自定义行为
register(ExtensionCategory.BEHAVIOR, 'my-behavior', MyBehaviorClass);
// 注册自定义插件
register(ExtensionCategory.PLUGIN, 'my-plugin', MyPluginClass);---
常见错误与修正
错误:在 render() 中启动循环动画导致白屏或动画异常
// ❌ render() 在元素创建和更新时都会被调用,在此处启动动画会导致:
// 1. 动画重复启动,性能问题
// 2. 与入场动画冲突,可能导致白屏
// 3. 更新时动画被重置
class BreathingNode extends BaseNode {
render(attributes, container) {
super.render(attributes, container);
const circle = this.upsert('key', Circle, { cx: 0, cy: 0, r: 30 }, container);
// 错误:在 render 中启动动画
circle.animate(
[{ r: 30 }, { r: 40 }, { r: 30 }],
{ duration: 2000, iterations: Infinity }
);
}
}
// ✅ 使用 onCreate 生命周期钩子,在入场动画完成后启动循环动画
class BreathingNode extends BaseNode {
render(attributes, container) {
super.render(attributes, container);
this.upsert('key', Circle, { cx: 0, cy: 0, r: 30 }, container);
}
onCreate() {
const keyShape = this.shapeMap['key'];
keyShape.animate(
[{ r: 30 }, { r: 40 }, { r: 30 }],
{ duration: 2000, iterations: Infinity }
);
}
}
// ✅ 或者继承内置节点,利用内置的 halo 形状实现呼吸效果(推荐)
class BreathingCircle extends Circle {
onCreate() {
const halo = this.shapeMap.halo;
if (halo) {
halo.animate(
[{ lineWidth: 0 }, { lineWidth: 20 }, { lineWidth: 0 }],
{ duration: 2000, iterations: Infinity }
);
}
}
}错误:使用已移除的 extend API
// ❌ extend 已从 G6 v5 正式版移除,调用报 "extend is not a function"
import { Graph, extend } from '@antv/g6';
const ExtGraph = extend(Graph, { nodes: { 'my-node': MyNodeFn } });
// ✅ 使用 BaseNode + register
import { BaseNode, ExtensionCategory, register } from '@antv/g6';
class MyNode extends BaseNode { /* ... */ }
register(ExtensionCategory.NODE, 'my-node', MyNode);错误:忘记调用 register 就使用自定义类型
// ❌ 没有 register,G6 不认识 'my-node'
const graph = new Graph({
node: { type: 'my-node' },
});
// ✅ 先 register,再使用
register(ExtensionCategory.NODE, 'my-node', MyNode);
const graph = new Graph({
node: { type: 'my-node' },
});错误:在 render 中直接操作 DOM(应使用 upsert)
// ❌ 直接操作 DOM 不受 G6 渲染周期管理
render(attributes, container) {
const div = document.createElement('div');
container.appendChild(div);
}
// ✅ 使用 upsert 管理形状生命周期
render(attributes, container) {
this.upsert('my-shape', Rect, { x: 0, y: 0 }, container);
}错误:在 render 中通过 attributes.data 读取节点业务数据 → 白屏
// ❌ attributes 是计算后的样式属性集合,不包含节点的 data 字段
// attributes.data 为 undefined,访问 data.color 抛 TypeError → 白屏
render(attributes, container) {
const { data } = attributes; // undefined!
const color = data.color; // TypeError: Cannot read properties of undefined
}
// ✅ 通过 node.style 回调把 data 映射为样式属性,在 attributes 中直接读取
// 第一步:在 Graph 配置的 node.style 中把数据映射为自定义属性
node: {
type: 'my-node',
style: {
color: (d) => d.data.color, // 映射为 attributes.color
label: (d) => d.data.label, // 映射为 attributes.label
},
},
// 第二步:在 render() 里直接解构 attributes
render(attributes, container) {
const { color = '#1783FF', label } = attributes; // ✅ 正确读取
}错误:upsert key 与默认形状冲突导致双重渲染
// ❌ key 不是 'key',super.render() 已创建默认 'key' 形状,
// 再 upsert('circle', ...) 会叠加一个额外圆形
render(attributes, container) {
super.render(attributes, container);
this.upsert('circle', Circle, { cx: 0, cy: 0, r: 20 }, container); // 双圆!
}
// ✅ 使用 'key' 替换默认主形状
render(attributes, container) {
super.render(attributes, container);
this.upsert('key', Circle, { cx: 0, cy: 0, r: 20 }, container); // 替换默认形状
}错误:动画使用 CSS 属性(scale)而非形状属性
// ❌ scale 是 CSS transform,@antv/g 形状 animate() 使用形状自身的属性名
circle.animate(
[{ scale: 1 }, { scale: 1.1 }, { scale: 1 }], // 静默忽略,无任何效果
{ duration: 2000, iterations: Infinity }
);
// ✅ 动画 Circle 形状时使用 r / fill / stroke 等形状属性
circle.animate(
[{ r: 20 }, { r: 25 }, { r: 20 }],
{ duration: 2000, iterations: Infinity }
);错误:自定义边中直接访问 attributes.sourcePoint → 白屏
// ❌ attributes 中不存在 sourcePoint / targetPoint 属性
// 直接访问返回 undefined,解构赋值后计算会抛出异常导致白屏
class MyEdge extends BaseEdge {
getKeyPath(attributes) {
const { sourcePoint, targetPoint } = attributes; // undefined!
const [sx, sy] = sourcePoint; // TypeError: Cannot read properties of undefined
return [['M', sx, sy], ['L', tx, ty]];
}
}
// ✅ 使用 this.getEndpoints(attributes) 获取起点和终点
class MyEdge extends BaseEdge {
getKeyPath(attributes) {
const [sourcePoint, targetPoint] = this.getEndpoints(attributes, false);
const [sx, sy] = sourcePoint;
const [tx, ty] = targetPoint;
return [['M', sx, sy], ['L', tx, ty]];
}
}</skill>
核心概念
G6 是数据驱动的图可视化引擎,使用标准 JSON 格式描述图结构。
GraphData 基本结构:
interface GraphData {
nodes?: NodeData[];
edges?: EdgeData[];
combos?: ComboData[];
}最小可运行示例
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
data: {
nodes: [
{ id: 'n1', data: { name: '节点A', type: 'user' } },
{ id: 'n2', data: { name: '节点B', type: 'product' } },
],
edges: [
{ id: 'e1', source: 'n1', target: 'n2', data: { weight: 5 } },
],
},
node: {
style: { labelText: (d) => d.data.name },
},
layout: { type: 'force' },
behaviors: ['drag-canvas', 'zoom-canvas'],
});
graph.render();NodeData 完整结构
interface NodeData {
id: string; // 必填,唯一标识符
type?: string; // 节点类型,如 'circle', 'rect', 'image'
data?: Record<string, unknown>; // 业务数据(推荐存放自定义属性)
style?: NodeStyle; // 节点样式(覆盖全局配置)
states?: string[]; // 初始状态列表
combo?: string; // 所属 combo 的 id
children?: string[]; // 树形数据中子节点 id 列表
}
// 示例
const nodes = [
{
id: 'user-001',
type: 'circle', // 覆盖全局节点类型
data: {
name: '张三',
role: 'admin',
score: 95,
},
style: {
fill: '#ff7875', // 覆盖全局样式
size: 60,
},
states: ['selected'], // 初始为选中状态
},
];EdgeData 完整结构
interface EdgeData {
id?: string; // 可选,唯一标识,未指定时自动生成
source: string; // 必填,起点节点 id
target: string; // 必填,终点节点 id
type?: string; // 边类型,如 'line', 'cubic', 'polyline'
data?: Record<string, unknown>; // 业务数据
style?: EdgeStyle; // 边样式(覆盖全局配置)
states?: string[]; // 初始状态列表
}
// 示例
const edges = [
{
id: 'edge-001',
source: 'user-001',
target: 'product-001',
data: {
type: 'purchase',
amount: 299,
date: '2024-01-15',
},
style: {
stroke: '#ff4d4f',
lineWidth: 2,
},
},
];ComboData 完整结构
interface ComboData {
id: string; // 必填,唯一标识符
type?: string; // combo 类型:'circle' | 'rect'
data?: Record<string, unknown>; // 业务数据
style?: ComboStyle; // combo 样式
states?: string[]; // 初始状态
combo?: string; // 父 combo id(嵌套 combo)
}
// 示例:节点分组
const data = {
nodes: [
{ id: 'n1', combo: 'group1', data: { label: '成员1' } },
{ id: 'n2', combo: 'group1', data: { label: '成员2' } },
{ id: 'n3', combo: 'group2', data: { label: '成员3' } },
],
edges: [
{ source: 'n1', target: 'n3' },
],
combos: [
{ id: 'group1', data: { label: '团队A' } },
{ id: 'group2', data: { label: '团队B' } },
],
};树形数据
树形布局(mindmap、compact-box 等)使用 treeToGraphData() 转换,必须从 @antv/g6 中导入:
import { Graph, treeToGraphData } from '@antv/g6';
// 树形结构数据
const treeData = {
id: 'root',
data: { label: '根节点' },
children: [
{
id: 'child1',
data: { label: '子节点1' },
children: [
{ id: 'grandchild1', data: { label: '孙节点1' } },
{ id: 'grandchild2', data: { label: '孙节点2' } },
],
},
{
id: 'child2',
data: { label: '子节点2' },
},
],
};
const graph = new Graph({
container: 'container',
data: treeToGraphData(treeData), // 转换为 GraphData 格式
layout: {
type: 'mindmap',
direction: 'H',
},
behaviors: ['drag-canvas', 'zoom-canvas', 'collapse-expand'],
});
graph.render();远程数据加载
const graph = new Graph({
container: 'container',
data: { nodes: [], edges: [] }, // 初始空数据
layout: { type: 'force' },
behaviors: ['drag-canvas', 'zoom-canvas'],
});
// 异步加载数据
fetch('https://api.example.com/graph-data')
.then((res) => res.json())
.then((data) => {
graph.setData(data); // 或在 render 前设置
graph.render();
});
// 推荐方式:等待 render 后再更新
await graph.render();
const data = await fetch('/api/data').then((r) => r.json());
graph.setData(data);
await graph.draw();数据操作 API
// 读取数据
const allNodes = graph.getNodeData();
const oneNode = graph.getNodeData('n1');
const allEdges = graph.getEdgeData();
const oneEdge = graph.getEdgeData('e1');
// 新增
graph.addNodeData([
{ id: 'n10', data: { label: '新节点' } },
]);
graph.addEdgeData([
{ source: 'n1', target: 'n10' },
]);
await graph.draw();
// 更新
graph.updateNodeData([
{ id: 'n1', data: { label: '更新后' }, style: { fill: 'red' } },
]);
await graph.draw();
// 删除
graph.removeNodeData(['n10']); // 会同时删除关联的边
graph.removeEdgeData(['e1']);
await graph.draw();
// 批量更新数据(替换全量)
graph.setData({ nodes: [...], edges: [...] });
await graph.draw();样式与数据的分离(最佳实践)
// ✅ 推荐:业务数据放 data,样式通过回调函数从 data 计算
const nodes = [
{ id: 'n1', data: { name: '高优先级', priority: 'high', value: 100 } },
{ id: 'n2', data: { name: '低优先级', priority: 'low', value: 30 } },
];
const graph = new Graph({
container: 'container',
data: { nodes, edges: [] },
node: {
style: {
// 通过回调函数将数据映射为样式
fill: (d) => d.data.priority === 'high' ? '#ff4d4f' : '#1783FF',
size: (d) => Math.max(20, d.data.value / 2),
labelText: (d) => d.data.name,
},
},
});常见错误与修正
错误1:业务属性放在节点顶层
// ❌ 错误:label、type 等业务属性直接在节点顶层
{ id: 'n1', label: '节点1', category: 'user', value: 100 }
// ✅ 正确:业务属性放在 data 字段
{ id: 'n1', data: { label: '节点1', category: 'user', value: 100 } }错误2:边缺少 source 或 target
// ❌ 错误:缺少 source 或 target
{ id: 'e1', from: 'n1', to: 'n2' } // v4 写法
// ✅ 正确
{ id: 'e1', source: 'n1', target: 'n2' }错误3:节点 id 重复
// ❌ 错误:id 重复会导致渲染异常
const nodes = [
{ id: 'node1', data: { label: 'A' } },
{ id: 'node1', data: { label: 'B' } }, // 重复 id
];
// ✅ 正确:每个节点 id 必须唯一
const nodes = [
{ id: 'node-a', data: { label: 'A' } },
{ id: 'node-b', data: { label: 'B' } },
];错误4:边的 source/target 引用了不存在的节点
// ❌ 错误:引用了不存在的节点 id
const edges = [
{ source: 'n1', target: 'n999' }, // n999 不存在
];
// ✅ 正确:确保 source 和 target 都存在于 nodes 中错误5:重复边导致 "Edge already exists" 错误
G6 不允许存在重复边(相同 source 和 target 的边)。动态生成边时必须去重,否则会抛出 Edge already exists: xxx-yyy 错误。
// ❌ 错误:随机生成边时可能产生重复边
const edges = [];
for (let i = 0; i < 34; i++) {
for (let j = 0; j < 3; j++) {
const target = Math.floor(Math.random() * 34);
if (target !== i) {
edges.push({ source: `${i}`, target: `${target}` }); // 可能重复!
}
}
}
// ✅ 正确:使用 Set 去重,确保每对 source-target 唯一
const edges = [];
const edgeSet = new Set();
for (let i = 0; i < 34; i++) {
for (let j = 0; j < 3; j++) {
const target = Math.floor(Math.random() * 34);
const key = `${i}-${target}`;
const reverseKey = `${target}-${i}`;
if (target !== i && !edgeSet.has(key) && !edgeSet.has(reverseKey)) {
edgeSet.add(key);
edges.push({ source: `${i}`, target: `${target}` });
}
}
}最佳实践:优先使用明确的静态边数据,避免随机生成边。 如果必须动态生成,务必在添加前检查重复:
// ✅ 推荐:使用明确的边数据,不依赖随机生成
const data = {
nodes: Array.from({ length: 10 }, (_, i) => ({ id: `${i}` })),
edges: [
{ source: '0', target: '1' },
{ source: '0', target: '2' },
{ source: '1', target: '3' },
{ source: '2', target: '3' },
// 每对 source-target 只出现一次
],
};
const graph = new Graph({
container: 'container',
autoFit: 'view',
data,
node: {
style: {
labelText: (d) => d.id,
labelPlacement: 'center',
labelFill: '#fff',
},
},
layout: { type: 'circular' },
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],
});
graph.render();错误6:treeToGraphData 未导入
// ❌ 错误:忘记从 @antv/g6 导入 treeToGraphData
import { Graph } from '@antv/g6';
// ...
data: treeToGraphData(treeData), // ReferenceError: treeToGraphData is not defined
// ✅ 正确:必须显式导入
import { Graph, treeToGraphData } from '@antv/g6';
// ...
data: treeToGraphData(treeData),事件监听基础
// 监听
graph.on('node:click', (event) => {
const { target, targetType } = event;
console.log('点击节点:', target.id);
});
// 取消监听(传入同一函数引用)
const handler = (e) => console.log(e);
graph.on('node:click', handler);
graph.off('node:click', handler);
// 取消该事件的所有监听
graph.off('node:click');---
元素事件
元素事件格式:{元素类型}:{事件类型},如 node:click、edge:pointerover。
| 事件名 | 说明 |
|---|---|
node:click | 点击节点 |
node:dblclick | 双击节点 |
node:pointerover | 鼠标移入节点 |
node:pointerout | 鼠标移出节点 |
node:pointerdown | 鼠标/触摸按下节点 |
node:pointerup | 鼠标/触摸抬起 |
node:contextmenu | 右键节点 |
node:dragstart | 开始拖拽节点 |
node:drag | 拖拽节点中 |
node:dragend | 拖拽节点结束 |
edge:click | 点击边 |
edge:pointerover | 鼠标移入边 |
combo:click | 点击 combo |
combo:dblclick | 双击 combo |
事件对象属性
interface IElementEvent {
target: DisplayObject; // 触发事件的图形对象
targetType: string; // 'node' | 'edge' | 'combo' | 'canvas'
originalEvent: Event; // 原始 DOM 事件
// 坐标(画布坐标系)
canvas: { x: number; y: number };
// 坐标(视口坐标系)
viewport: { x: number; y: number };
// 坐标(客户端坐标系)
client: { x: number; y: number };
}典型用法
// 点击节点获取数据
graph.on('node:click', (event) => {
const nodeId = event.target.id;
const nodeData = graph.getNodeData(nodeId);
console.log(nodeData);
});
// 悬停边高亮
graph.on('edge:pointerover', (event) => {
graph.setElementState(event.target.id, 'active');
});
graph.on('edge:pointerout', (event) => {
graph.setElementState(event.target.id, []);
});
// 右键菜单
graph.on('node:contextmenu', (event) => {
event.originalEvent.preventDefault();
console.log('右键节点:', event.target.id);
});---
画布事件
| 事件名 | 说明 |
|---|---|
canvas:click | 点击画布空白区域 |
canvas:dblclick | 双击画布 |
canvas:pointerdown | 鼠标按下画布 |
canvas:pointerup | 鼠标抬起 |
canvas:pointermove | 鼠标在画布移动 |
canvas:wheel | 画布滚轮事件 |
canvas:contextmenu | 右键画布 |
// 点击空白区域取消选中
graph.on('canvas:click', () => {
const selected = graph.getElementDataByState('node', 'selected');
selected.forEach(n => graph.setElementState(n.id, []));
});---
图生命周期事件
import { GraphEvent } from '@antv/g6';
// 渲染完成
graph.on(GraphEvent.AFTER_RENDER, () => {
console.log('图渲染完成');
});
// 布局完成
graph.on(GraphEvent.AFTER_LAYOUT, () => {
console.log('布局完成');
});
// 元素创建后(批量)
graph.on(GraphEvent.AFTER_ELEMENT_CREATE, (event) => {
console.log('新增元素:', event.data);
});
// 视口变换(缩放/平移)
graph.on(GraphEvent.AFTER_TRANSFORM, (event) => {
const { translate, zoom } = event.data;
console.log('视口变换:', zoom);
});常用生命周期事件
| 事件常量 | 事件名 | 触发时机 |
|---|---|---|
GraphEvent.BEFORE_RENDER | beforerender | render() 开始前 |
GraphEvent.AFTER_RENDER | afterrender | render() 完成后 |
GraphEvent.BEFORE_DRAW | beforedraw | draw() 开始前 |
GraphEvent.AFTER_DRAW | afterdraw | draw() 完成后 |
GraphEvent.AFTER_LAYOUT | afterlayout | 布局计算完成 |
GraphEvent.AFTER_ELEMENT_CREATE | afterelementcreate | 元素新增后 |
GraphEvent.AFTER_ELEMENT_UPDATE | afterelementupdate | 元素更新后 |
GraphEvent.AFTER_ELEMENT_DESTROY | afterelementdestroy | 元素删除后 |
GraphEvent.AFTER_TRANSFORM | aftertransform | 视口变换后 |
GraphEvent.BEFORE_DESTROY | beforedestroy | destroy() 前 |
---
常见模式
节点拖拽后更新坐标
graph.on('node:dragend', (event) => {
const nodeId = event.target.id;
const { x, y } = graph.getNodeData(nodeId);
console.log(`节点 ${nodeId} 新坐标: (${x}, ${y})`);
});动态更新 tooltip 数据
graph.on('node:pointerover', async (event) => {
const nodeId = event.target.id;
const detail = await fetchNodeDetail(nodeId);
graph.updateNodeData([{ id: nodeId, data: { ...detail } }]);
});数据操作 API
读取数据
// 获取所有数据
const allData = graph.getData(); // { nodes, edges, combos }
const nodes = graph.getNodeData(); // NodeData[]
const edges = graph.getEdgeData(); // EdgeData[]
const combos = graph.getComboData(); // ComboData[]
// 按 id 获取单个元素
const node = graph.getNodeData('n1');
const edge = graph.getEdgeData('e1');
const combo = graph.getComboData('c1');
// 按 id 数组批量获取
const someNodes = graph.getNodeData(['n1', 'n2', 'n3']);添加元素
// 添加节点
graph.addNodeData([
{ id: 'n3', data: { label: '新节点', type: 'server' } },
]);
// 添加边
graph.addEdgeData([
{ source: 'n1', target: 'n3', data: { weight: 5 } },
]);
// 添加 combo
graph.addComboData([
{ id: 'c1', data: { label: '新分组' } },
]);
// 添加后需要 draw 生效
graph.draw();更新元素
// 更新节点数据(只传需要更新的字段)
graph.updateNodeData([
{ id: 'n1', data: { label: '更新后的标签', value: 200 } },
]);
// 更新边
graph.updateEdgeData([
{ id: 'e1', data: { weight: 10 } },
]);
graph.draw();删除元素
graph.removeNodeData(['n3']); // 删除节点(关联边自动删除)
graph.removeEdgeData(['e1']); // 删除边
graph.removeComboData(['c1']); // 删除 combo
graph.draw();批量操作(合并为一次历史记录)
// batch 内的操作合并为一次渲染和历史记录
graph.batch(() => {
graph.addNodeData([{ id: 'n10', data: { label: '批量A' } }]);
graph.addNodeData([{ id: 'n11', data: { label: '批量B' } }]);
graph.addEdgeData([{ source: 'n10', target: 'n11' }]);
});
graph.draw();---
视口控制 API
缩放
// 获取当前缩放比例
const zoom = graph.getZoom(); // 返回数字,1.0 = 原始大小
// 缩放到指定比例(带动画)
await graph.zoomTo(1.5, { easing: 'ease-out', duration: 300 });
// 相对缩放(在当前比例基础上)
await graph.zoom(0.8); // 缩小到当前的 80%平移
// 获取当前平移量
const { x, y } = graph.getTranslate();
// 平移到绝对位置
await graph.translateTo({ x: 100, y: 200 });
// 相对平移
await graph.translate({ x: 50, y: 0 });适配视图
// 自动缩放并居中显示所有元素
await graph.fitView({
padding: 20, // 边距
direction: 'both', // 'x' | 'y' | 'both'
when: 'overflow', // 仅内容溢出时适配
});
// 居中(不缩放)
await graph.fitCenter();
// 聚焦到指定元素(平移+缩放到该元素)
await graph.focusElement('n1', {
easing: 'ease-in-out',
duration: 500,
});---
元素状态 API
// 设置单个元素状态
graph.setElementState('n1', 'selected');
graph.setElementState('n1', ['selected', 'highlight']);
graph.setElementState('n1', []); // 清除所有状态
// 批量设置(推荐,性能更好)
graph.setElementState({
'n1': 'selected',
'n2': ['highlight'],
'e1': 'active',
});
// 读取状态
const states = graph.getElementState('n1'); // string[]
// 按状态查询元素
const selectedNodes = graph.getElementDataByState('node', 'selected');
const activeEdges = graph.getElementDataByState('edge', 'active');---
元素可见性 API
// 隐藏/显示(可带动画)
graph.hideElement(['n1', 'n2'], true); // true = 带动画
graph.showElement(['n1', 'n2'], true);
// 调整 Z 轴顺序
graph.frontElement(['n1']); // 置顶
graph.backElement(['n1']); // 置底---
关联查询 API
// 查询节点的所有关联边
const relatedEdges = graph.getRelatedEdgesData('n1');
const incomingEdges = graph.getIncomingEdgesData('n1');
const outgoingEdges = graph.getOutgoingEdgesData('n1');
// 查询元素类型
const type = graph.getElementType('n1'); // 'node' | 'edge' | 'combo' | null---
布局 / 行为 / 插件动态更新
// 动态切换布局
graph.setLayout({ type: 'circular' });
await graph.layout(); // 重新执行布局
// 动态更新行为(不用重新 render)
graph.setBehaviors([
'drag-canvas',
'zoom-canvas',
{ type: 'click-select', multiple: true },
]);
// 局部更新某个行为配置
graph.updateBehavior({
key: 'click-select',
multiple: false,
});
// 动态添加/移除插件
graph.setPlugins(['minimap', { type: 'tooltip', getContent: () => '' }]);
// 获取插件实例(需要给插件设置 key)
// plugins: [{ type: 'history', key: 'h1', stackSize: 20 }]
const history = graph.getPluginInstance('h1');---
图片导出
// 导出为 PNG Data URL
const dataURL = await graph.toDataURL({ type: 'image/png', encoderOptions: 0.9 });
// 下载图片
const link = document.createElement('a');
link.download = 'graph.png';
link.href = dataURL;
link.click();---
销毁
// 销毁图实例,释放内存
graph.destroy();核心概念
Graph 是 G6 的核心容器,管理所有元素(节点、边、Combo)和操作(交互、渲染)。
G6 v5 与 v4 的关键区别:
- 所有配置在
new Graph({...})中一次完成 - 数据在构造函数中通过
data字段传入(不再使用graph.data()) - 节点标签通过
style.labelText回调配置(不再用label或labelCfg) behaviors直接是数组(不再有 Mode 模式概念)
最小可运行示例
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container', // 必填:DOM 元素 id 或 HTMLElement
data: {
nodes: [
{ id: 'node1', data: { label: '节点1' } },
{ id: 'node2', data: { label: '节点2' } },
{ id: 'node3', data: { label: '节点3' } },
],
edges: [
{ id: 'e1', source: 'node1', target: 'node2' },
{ id: 'e2', source: 'node2', target: 'node3' },
],
},
layout: { type: 'force' },
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],
});
graph.render();完整配置说明
容器与尺寸
const graph = new Graph({
container: 'container', // 字符串 id 或 DOM 元素
width: 800, // 画布宽度(px),默认不用配置
height: 600, // 画布高度(px),默认不用配置
autoFit: 'view', // 自动适配:'center' | 'view' | false
padding: [20, 20, 20, 20], // 内边距 [top, right, bottom, left]
devicePixelRatio: 2, // 设备像素比,高清屏设置
});渲染器配置
const graph = new Graph({
container: 'container',
renderer: () => new CanvasRenderer(), // 默认 Canvas 渲染器
// renderer: () => new SVGRenderer(), // SVG 渲染器(需单独引入)
// renderer: () => new WebGLRenderer(), // WebGL 渲染器(需单独引入)
});完整示例(包含所有常用配置)
import { Graph } from '@antv/g6';
const graph = new Graph({
// 容器
container: 'container',
width: 960,
height: 600,
autoFit: 'view',
// 数据
data: {
nodes: [
{ id: 'n1', data: { label: '产品', type: 'product', value: 80 } },
{ id: 'n2', data: { label: '用户', type: 'user', value: 50 } },
{ id: 'n3', data: { label: '订单', type: 'order', value: 30 } },
],
edges: [
{ id: 'e1', source: 'n1', target: 'n2', data: { label: '购买' } },
{ id: 'e2', source: 'n2', target: 'n3', data: { label: '生成' } },
],
},
// 节点配置
node: {
type: 'circle',
style: {
size: 40,
fill: '#1783FF',
stroke: '#fff',
lineWidth: 2,
labelText: (d) => d.data.label,
labelPlacement: 'bottom',
labelFill: '#333',
},
},
// 边配置
edge: {
type: 'line',
style: {
stroke: '#aaa',
lineWidth: 1.5,
endArrow: true,
labelText: (d) => d.data.label,
},
},
// 布局
layout: {
type: 'force',
preventOverlap: true,
nodeSize: 40,
linkDistance: 100,
},
// 主题
theme: 'light',
// 交互行为
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'click-select'],
// 插件
plugins: ['grid-line', 'minimap'],
// 动画
animation: true,
});
await graph.render();边数据的 ID 规则
⚠️ 重要:边的 ID 自动生成规则
当边数据中未指定 id 时,G6 会自动以 ${source}-${target} 格式生成边 ID。
这意味着:如果两条边的 source 和 target 相同(即平行边),它们会生成相同的 ID,导致 `Edge already exists` 错误。
// ❌ 错误:两条边 source/target 相同,自动生成的 id 均为 "A-B",报错
edges: [
{ source: 'A', target: 'B' },
{ source: 'A', target: 'B' }, // 重复!
]
// ✅ 正确:为每条边显式指定唯一 id
edges: [
{ id: 'e1', source: 'A', target: 'B' },
{ id: 'e2', source: 'A', target: 'B' },
]最佳实践:始终为边数据显式指定唯一 `id`,避免自动生成 ID 冲突。
// ✅ 推荐写法:每条边都有唯一 id
const edges = [
{ id: 'e-0-1', source: '0', target: '1' },
{ id: 'e-0-2', source: '0', target: '2' },
{ id: 'e-1-2', source: '1', target: '2' },
];
// ✅ 动态生成边时,使用索引确保 id 唯一
const edges = rawEdges.map((e, i) => ({
id: `edge-${i}`,
source: e.source,
target: e.target,
}));生命周期方法
// 渲染(必须调用)
await graph.render();
// 更新数据后重绘
graph.draw();
// 适配视图
graph.fitView();
graph.fitCenter();
// 销毁
graph.destroy();
// 监听事件
graph.on('node:click', (event) => {
const { target } = event;
console.log('点击节点:', target.id);
});
// 获取渲染状态
console.log(graph.rendered); // boolean
console.log(graph.destroyed); // boolean动态操作
// 添加节点
graph.addNodeData([{ id: 'n4', data: { label: '新节点' } }]);
await graph.draw();
// 删除节点(关联边也会删除)
graph.removeNodeData(['n4']);
await graph.draw();
// 更新元素样式
graph.updateNodeData([{ id: 'n1', style: { fill: 'red' } }]);
await graph.draw();
// 设置元素状态
graph.setElementState('n1', 'selected');
graph.setElementState('n1', []); // 清除状态
// 缩放
graph.zoomTo(1.5);
graph.zoomTo(1, true); // 带动画
// 移动视口
graph.translateTo([400, 300]);
// 定位到某元素
graph.focusElement('n1');树形数据转换
如果数据是树形结构(有父子层级关系),需要使用 treeToGraphData 工具函数将其转换为 G6 标准图数据格式后再传入 data。
import { Graph, treeToGraphData } from '@antv/g6';
const treeData = {
id: 'root',
children: [
{ id: 'child1', children: [{ id: 'leaf1' }] },
{ id: 'child2' },
],
};
const graph = new Graph({
container: 'container',
data: treeToGraphData(treeData), // ✅ 必须转换后传入
layout: { type: 'compact-box' },
behaviors: ['drag-canvas', 'zoom-canvas'],
});
graph.render();⚠️treeToGraphData需从@antv/g6中显式导入,不可直接调用未导入的函数。
常见错误
错误1:缺少 container
// ❌ 错误
const graph = new Graph({ });
// ✅ 正确
const graph = new Graph({ container: 'container' });错误2:使用 v4 的 graph.data() 方式
// ❌ 错误(v4 写法)
const graph = new G6.Graph({ container: 'container' });
graph.data({ nodes: [...], edges: [...] });
graph.render();
// ✅ 正确(v5 写法)
const graph = new Graph({
container: 'container',
data: { nodes: [...], edges: [...] },
});
graph.render();错误3:数据中直接写标签
// ❌ 错误:节点数据直接写 label
{ id: 'node1', label: 'Node 1' }
// ✅ 正确:业务数据放在 data 字段
{ id: 'node1', data: { label: 'Node 1' } }
// 然后在样式中:
node: {
style: {
labelText: (d) => d.data.label,
},
}错误4:使用 v4 的 modes 配置
// ❌ 错误(v4 modes)
modes: { default: ['drag-canvas', 'zoom-canvas'] }
// ✅ 正确(v5 behaviors)
behaviors: ['drag-canvas', 'zoom-canvas']错误5:autoFit 与固定尺寸冲突
// ❌ autoFit: true 同时设置 width/height 会产生不可预期结果
const graph = new Graph({
autoFit: true, // 旧写法
});
// ✅ 正确:使用 'view' 或 'center'
const graph = new Graph({
autoFit: 'view', // 或 'center',或 false(手动控制)
});错误6:边 ID 冲突导致 "Edge already exists"
当动态生成边数据时,若多条边的 source 和 target 相同(平行边),未指定 id 会导致自动生成的 id 重复,抛出 Edge already exists 错误。
// ❌ 错误:随机生成边时可能产生重复的 source-target 对
const edges = [];
for (let i = 0; i < 34; i++) {
for (let j = 0; j < 3; j++) {
const target = Math.floor(Math.random() * 34);
if (target !== i) {
edges.push({ source: `${i}`, target: `${target}` }); // 没有 id,可能重复!
}
}
}
// ✅ 正确方案1:为每条边指定唯一 id(推荐)
const edges = [];
let edgeIndex = 0;
for (let i = 0; i < 34; i++) {
for (let j = 0; j < 3; j++) {
const target = Math.floor(Math.random() * 34);
if (target !== i) {
edges.push({ id: `edge-${edgeIndex++}`, source: `${i}`, target: `${target}` });
}
}
}
// ✅ 正确方案2:对已有边数组去重后添加 id
const edgeSet = new Set();
const edges = [];
let edgeIndex = 0;
for (let i = 0; i < 34; i++) {
for (let j = 0; j < 3; j++) {
const target = Math.floor(Math.random() * 34);
const key = `${i}-${target}`;
if (target !== i && !edgeSet.has(key)) {
edgeSet.add(key);
edges.push({ id: `edge-${edgeIndex++}`, source: `${i}`, target: `${target}` });
}
}
}错误7:树形数据未转换直接传入
// ❌ 错误:树形结构数据不能直接传给 data
const graph = new Graph({
data: { id: 'root', children: [...] }, // 错误!
});
// ❌ 错误:treeToGraphData 未导入就使用
const graph = new Graph({
data: treeToGraphData(treeData), // ReferenceError: treeToGraphData is not defined
});
// ✅ 正确:从 @antv/g6 导入后使用
import { Graph, treeToGraphData } from '@antv/g6';
const graph = new Graph({
data: treeToGraphData(treeData),
});数据变换(Transforms)
Transforms 是在数据绑定到图元素前的处理管道,用于数据到可视属性的映射。
map-node-size(节点大小映射)
将节点数据字段映射到节点尺寸区间:
const graph = new Graph({
container: 'container',
data: {
nodes: [
{ id: 'n1', data: { label: 'A', value: 10 } },
{ id: 'n2', data: { label: 'B', value: 50 } },
{ id: 'n3', data: { label: 'C', value: 100 } },
],
edges: [
{ source: 'n1', target: 'n2' },
{ source: 'n2', target: 'n3' },
],
},
// transforms 在 Graph 配置顶层
transforms: [
{
type: 'map-node-size',
field: 'value', // 映射的数据字段(从 node.data 中读取)
range: [16, 60], // 映射到的尺寸范围 [最小, 最大](px)
},
],
node: {
type: 'circle',
style: {
// size 不需要再手动设置,transform 自动计算
fill: '#1783FF',
stroke: '#fff',
labelText: (d) => d.data.label,
labelPlacement: 'bottom',
},
},
layout: { type: 'force', preventOverlap: true },
behaviors: ['drag-canvas', 'zoom-canvas'],
});
graph.render();process-parallel-edges(平行边处理)
当两个节点之间存在多条边时,自动将它们错开展示:
transforms: [
{
type: 'process-parallel-edges',
offset: 15, // 平行边之间的间距(px)
// 只对有平行关系的边应用曲线
},
],
edge: {
type: 'quadratic', // 推荐与 quadratic 配合使用
style: {
stroke: '#aaa',
endArrow: true,
},
},内置 Transforms 列表
| 类型 | 说明 | 常用参数 |
|---|---|---|
map-node-size | 数据驱动节点大小 | field, range |
process-parallel-edges | 平行边错开展示 | offset |
place-radial-labels | 径向布局标签自动定位 | — |
arrange-draw-order | 调整元素渲染顺序 | nodeBeforeEdge |
get-edge-actual-ends | 计算边的实际端点(端口支持) | — |
update-related-edge | 节点移动时更新关联边 | — |
---
动画系统
全局动画开关
const graph = new Graph({
container: 'container',
// 禁用所有动画(提升大图性能)
animation: false,
// ...
});元素进入/退出/更新动画
const graph = new Graph({
container: 'container',
{ nodes: [...], edges: [...] },
node: {
type: 'circle',
style: { size: 40, fill: '#1783FF' },
// 动画配置(每个阶段独立)
animation: {
// 节点初始进入动画
enter: [
{
fields: ['opacity'], // 动画属性
from: { opacity: 0 }, // 起始值
to: { opacity: 1 }, // 结束值
duration: 500,
easing: 'ease-in',
},
],
// 节点更新动画(数据变化时)
update: [
{
fields: ['fill', 'size'],
duration: 300,
easing: 'linear',
},
],
// 节点退出动画(删除时)
exit: [
{
fields: ['opacity'],
to: { opacity: 0 },
duration: 300,
},
],
},
},
});视口动画配置
所有视口操作(fitView, focusElement, zoomTo, translateTo)都支持动画参数:
// ViewportAnimationEffectTiming
await graph.fitView({
padding: 20,
// 动画配置
easing: 'ease-in-out',
duration: 600,
});
await graph.zoomTo(1.5, {
easing: 'ease-out',
duration: 400,
});
await graph.focusElement('n1', {
easing: 'ease-in-out',
duration: 500,
});常用 easing 值
| 值 | 说明 |
|---|---|
'linear' | 匀速 |
'ease' | 先慢后快再慢 |
'ease-in' | 先慢后快 |
'ease-out' | 先快后慢 |
'ease-in-out' | 先慢快慢 |
'cubic-bezier(...) | 自定义三次贝塞尔 |
---
性能优化建议
// 1. 大规模图(> 1000 节点)禁用动画
animation: false,
// 2. 使用 optimize-viewport-transform 行为减少渲染
behaviors: [
'drag-canvas',
'zoom-canvas',
{
type: 'optimize-viewport-transform',
// 视口变换时隐藏细节(标签等),提升帧率
shapes: (id, elementType) => {
if (elementType === 'node') return ['label', 'icon', 'halo'];
return ['label'];
},
},
],
// 3. 布局完成后停止力导向迭代
layout: {
type: 'force',
maxIteration: 300, // 限制最大迭代次数
minMovement: 0.5, // 收敛阈值
},核心概念
Combo 是对一组节点/子 combo 的包围容器,通过 combo 字段关联:
- 节点数据中
combo: 'comboId'表示该节点属于指定 combo - Combo 自动根据内部元素计算大小
- 支持折叠(collapsed)状态
- G6 5.x 支持 combo 作为边的源或目标(即边可以连接 combo)
Combo 数据结构
| 属性 | 描述 | 类型 | 默认值 | 必选 |
|---|---|---|---|---|
id | 组合的唯一标识符 | string | - | ✓ |
type | 组合类型(circle/rect) | string | - | |
data | 业务数据(标签等) | object | - | |
style | 样式配置(位置、折叠状态等) | object | - | |
combo | 父 combo ID(用于嵌套) | string | - | |
states | 初始状态 | string[] | - |
重要:父 combo(被其他 combo 引用的容器)也需要在 combos 数组中定义,即使它只有 id 字段。
最小可运行示例(rect-combo)
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
data: {
nodes: [
{ id: 'n1', combo: 'c1', data: { label: '前端A' } },
{ id: 'n2', combo: 'c1', data: { label: '前端B' } },
{ id: 'n3', combo: 'c2', data: { label: '后端A' } },
{ id: 'n4', combo: 'c2', data: { label: '后端B' } },
{ id: 'n5', combo: 'c2', data: { label: '后端C' } },
],
edges: [
{ source: 'n1', target: 'n3' },
{ source: 'n2', target: 'n4' },
],
combos: [
{ id: 'c1', data: { label: '前端团队' } },
{ id: 'c2', data: { label: '后端团队' } },
],
},
node: {
type: 'circle',
style: {
size: 36,
fill: '#1783FF',
stroke: '#fff',
lineWidth: 2,
labelText: (d) => d.data.label,
labelPlacement: 'bottom',
},
},
combo: {
type: 'rect', // 'rect' | 'circle'
style: {
fill: '#f0f5ff',
stroke: '#adc6ff',
lineWidth: 1,
radius: 8, // 圆角
padding: 20, // 内边距
labelText: (d) => d.data.label,
labelPlacement: 'top',
labelFill: '#1d39c4',
labelFontWeight: 600,
// 折叠后的尺寸
collapsedSize: [60, 30],
collapsedFill: '#1783FF',
},
},
layout: { type: 'antv-dagre', rankdir: 'LR', nodesep: 20, ranksep: 60 },
behaviors: [
'drag-canvas',
'zoom-canvas',
'drag-element',
{
type: 'collapse-expand',
trigger: 'dblclick', // 双击 combo 折叠/展开
},
],
});
graph.render();圆形 Combo(circle-combo)
combo: {
type: 'circle',
style: {
fill: '#f0f5ff',
stroke: '#adc6ff',
lineWidth: 1,
padding: 10,
labelText: (d) => d.data.label,
labelPlacement: 'top',
},
},嵌套 Combo
嵌套 combo 时,子 combo 通过 combo 字段指定父 combo ID,父 combo 必须在 `combos` 数组中定义:
data: {
combos: [
{ id: 'parent', data: { label: '母公司' } }, // 父 combo
{ id: 'child1', combo: 'parent', data: { label: '子公司A' } }, // 子 combo
{ id: 'child2', combo: 'parent', data: { label: '子公司B' } }, // 子 combo
],
nodes: [
{ id: 'n1', combo: 'child1', data: { label: '员工1' } },
{ id: 'n2', combo: 'child1', data: { label: '员工2' } },
{ id: 'n3', combo: 'child2', data: { label: '员工3' } },
],
},Combo 作为边的端点
G6 5.x 支持将 combo 作为边的 source 或 target:
data: {
nodes: [
{ id: 'n1', combo: 'c1' },
{ id: 'n2', combo: 'c2' },
],
edges: [
{ source: 'c1', target: 'n2' }, // 从 combo 到节点
{ source: 'c1', target: 'c2' }, // 从 combo 到 combo
],
combos: [
{ id: 'c1', data: { label: '组1' } },
{ id: 'c2', data: { label: '组2' } },
],
},折叠 / 展开 API
// 折叠 combo
await graph.collapseElement('c1');
// 展开 combo
await graph.expandElement('c1');
// 判断是否折叠
const isCollapsed = graph.isCollapsed('c1');初始折叠状态
在数据中设置 combo 的初始折叠状态:
combos: [
{
id: 'c1',
data: { label: '折叠组' },
style: { collapsed: true } // 初始折叠
},
],Combo 样式属性参考
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
fill | string | — | 背景填充色 |
stroke | string | — | 边框颜色 |
lineWidth | number | 1 | 边框宽度 |
padding | `number \ | number[]` | 10 |
radius | number | 0 | 圆角(rect combo) |
collapsed | boolean | false | 是否折叠 |
collapsedSize | [number, number] | — | 折叠后尺寸 |
collapsedFill | string | — | 折叠后填充色 |
labelText | `string \ | ((d) => string)` | — |
labelPlacement | `'top' \ | 'bottom' \ | 'center'` |
常见错误与修正
错误:将父 combo 错误识别为普通节点
当解析混合数据时,父 combo(被其他 combo 引用的容器)如果没有明显的 combo 特征(如没有 style.collapsed),容易被误判为普通节点,导致 Node not found 错误。
// ❌ 错误:将 combo2 识别为节点
const rawData = [
{"id":"combo1","combo":"combo2"}, // combo1 属于 combo2
{"id":"combo2"}, // 父 combo,但可能被误判为节点
];
// 错误的解析逻辑(导致 combo2 成为节点而非 combo)
const nodes = rawData.filter(item => !item.combo && !item.style?.collapsed);
const combos = rawData.filter(item => item.combo || item.style?.collapsed);
// ✅ 正确:先收集所有 combo ID,包括被引用的父 combo
const comboIds = new Set();
rawData.forEach(item => {
if (item.combo) comboIds.add(item.combo); // 收集父 combo ID
if (item.style?.collapsed !== undefined || item.combo) {
comboIds.add(item.id); // 收集明确的 combo
}
});
// 然后根据 comboIds 分类
const nodes = rawData.filter(item => !comboIds.has(item.id));
const combos = rawData.filter(item => comboIds.has(item.id));错误:将业务数据(labelText)放在 combo 的 style 字段而非 data 字段
// ❌ style 字段用于样式覆盖(坐标、尺寸等),不是业务数据的存储位置
combos: [
{ id: 'a', style: { labelText: 'Combo A' } },
],
combo: {
style: {
labelText: (d) => d.style.labelText, // 可能在样式计算阶段读取失败
},
},
// ✅ 业务数据放在 data 字段
combos: [
{ id: 'a', data: { label: 'Combo A' } },
],
combo: {
style: {
labelText: (d) => d.data.label,
},
},错误:circle combo 使用 radius 属性
// ❌ radius 只对 rect combo 有效(用于圆角),circle combo 半径由内容自动计算
combo: {
type: 'circle',
style: { radius: 10 }, // 无效,不会生效
},
// ✅ circle combo 用 padding 控制内边距
combo: {
type: 'circle',
style: { padding: 10 },
},错误:节点 combo 字段引用了不存在的 combo id
// ❌ combo 'cx' 未在 combos 数组中定义
nodes: [{ id: 'n1', combo: 'cx', data: {} }],
combos: [],
// ✅ 确保 combo id 存在
combos: [{ id: 'cx', data: { label: '组' } }],
nodes: [{ id: 'n1', combo: 'cx', data: {} }],错误:边引用了未定义的 combo 作为端点
// ❌ combo 'c1' 未在 combos 数组中定义,但边引用了它
edges: [{ source: 'c1', target: 'n1' }],
nodes: [{ id: 'n1' }],
combos: [],
// ✅ 确保作为边端点的 combo 已定义
combos: [{ id: 'c1', data: { label: '组1' } }],
nodes: [{ id: 'n1' }],
edges: [{ source: 'c1', target: 'n1' }],边类型对比
| 类型 | 方向 | 控制点轴 | 最佳配合布局 |
|---|---|---|---|
cubic | 任意 | 两端点间距 | 通用 |
cubic-horizontal | 水平(左→右) | X 轴 | dagre rankdir: 'LR' |
cubic-vertical | 垂直(上→下) | Y 轴 | dagre rankdir: 'TB' |
---
水平三次贝塞尔曲线(cubic-horizontal)
控制点主要沿 X 轴方向分布,忽略 Y 轴变化,产生水平 S 形曲线效果。适合水平方向流程图。
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
data: {
nodes: [
{ id: 'start', data: { label: '开始' } },
{ id: 'process', data: { label: '处理' } },
{ id: 'decision', data: { label: '判断' } },
{ id: 'end', data: { label: '结束' } },
],
edges: [
{ source: 'start', target: 'process' },
{ source: 'process', target: 'decision' },
{ source: 'decision', target: 'end' },
],
},
node: {
type: 'rect',
style: {
size: [80, 36],
fill: '#e6f7ff',
stroke: '#1783FF',
radius: 4,
labelText: (d) => d.data.label,
labelPlacement: 'center',
// 连接点设置为左右两侧
ports: [{ placement: 'right' }, { placement: 'left' }],
},
},
edge: {
type: 'cubic-horizontal', // 水平三次贝塞尔曲线
style: {
stroke: '#1783FF',
lineWidth: 1.5,
endArrow: true,
labelText: (d) => d?.data?.label,
labelBackground: true,
},
},
layout: {
type: 'antv-dagre',
rankdir: 'LR', // 从左到右,与 cubic-horizontal 配合
nodesep: 20,
ranksep: 100,
},
behaviors: ['drag-canvas', 'zoom-canvas'],
});
graph.render();样式配置参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
curvePosition | `number \ | [number, number]` | [0.5, 0.5] |
curveOffset | `number \ | [number, number]` | [0, 0] |
通用边样式参数(继承自 BaseEdge):
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
stroke | string | — | 边颜色 |
lineWidth | number | 1 | 线宽 |
endArrow | boolean | false | 是否显示终点箭头 |
startArrow | boolean | false | 是否显示起点箭头 |
lineDash | number[] | — | 虚线样式 |
labelText | `string \ | Function` | — |
labelBackground | boolean | false | 是否显示标签背景 |
---
垂直三次贝塞尔曲线(cubic-vertical)
控制点主要沿 Y 轴方向分布,忽略 X 轴变化,产生垂直 S 形曲线效果。适合垂直方向层次图、组织架构图。
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
width: 600,
height: 700,
data: {
nodes: [
{ id: 'ceo', data: { label: 'CEO' } },
{ id: 'cto', data: { label: 'CTO' } },
{ id: 'cfo', data: { label: 'CFO' } },
{ id: 'dev1', data: { label: '前端团队' } },
{ id: 'dev2', data: { label: '后端团队' } },
{ id: 'finance', data: { label: '财务部' } },
],
edges: [
{ source: 'ceo', target: 'cto' },
{ source: 'ceo', target: 'cfo' },
{ source: 'cto', target: 'dev1' },
{ source: 'cto', target: 'dev2' },
{ source: 'cfo', target: 'finance' },
],
},
node: {
type: 'rect',
style: {
size: [100, 36],
fill: '#f6ffed',
stroke: '#52c41a',
radius: 4,
labelText: (d) => d.data.label,
labelPlacement: 'center',
// 连接点设置为上下两侧
ports: [{ placement: 'top' }, { placement: 'bottom' }],
},
},
edge: {
type: 'cubic-vertical', // 垂直三次贝塞尔曲线
style: {
stroke: '#52c41a',
lineWidth: 1.5,
endArrow: true,
},
},
layout: {
type: 'antv-dagre',
rankdir: 'TB', // 从上到下,与 cubic-vertical 配合
nodesep: 40,
ranksep: 80,
},
behaviors: ['drag-canvas', 'zoom-canvas'],
});
graph.render();---
调整弯曲度
edge: {
type: 'cubic-horizontal',
style: {
// curvePosition: 控制点位置(0-1),0.5 为两端点中点
curvePosition: 0.3, // 单值:两个控制点相同位置
// curvePosition: [0.4, 0.6], // 数组:分别控制两个控制点
// curveOffset: 控制点偏移(px),正值向一侧偏,负值向另一侧
curveOffset: 30, // 增大弯曲程度
},
}---
状态样式
edge: {
type: 'cubic-horizontal',
style: {
stroke: '#d9d9d9',
lineWidth: 1,
endArrow: true,
},
state: {
selected: {
stroke: '#1783FF',
lineWidth: 2,
shadowColor: 'rgba(24,131,255,0.3)',
shadowBlur: 8,
},
active: {
stroke: '#40a9ff',
lineWidth: 2,
},
inactive: {
stroke: '#f0f0f0',
lineWidth: 1,
},
},
},---
选型指南
// 水平流程图(左→右)
// dagre rankdir: 'LR' + edge type: 'cubic-horizontal'
// 节点 ports: [{placement:'right'}, {placement:'left'}]
// 垂直层次图(上→下)
// dagre rankdir: 'TB' + edge type: 'cubic-vertical'
// 节点 ports: [{placement:'top'}, {placement:'bottom'}]
// 通用弧形连接(不依赖方向)
// edge type: 'cubic'(默认)
// 正交折线(流程图风格)
// edge type: 'polyline'核心概念
cubic 使用三次贝塞尔曲线连接两点,比直线更美观,适用于任意节点位置。
三种变体:
cubic:通用曲线,适合所有布局cubic-horizontal:水平方向的 S 形曲线,配合 LR/RL 方向布局cubic-vertical:垂直方向的 S 形曲线,配合 TB/BT 方向布局
控制曲率的关键参数:
curveOffset:曲线弯曲程度(正负值控制方向)curvePosition:控制点位置(0~1)controlPoints:自定义控制点坐标
最小可运行示例
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
width: 640,
height: 480,
data: {
nodes: [
{ id: 'n1', data: { label: 'A' } },
{ id: 'n2', data: { label: 'B' } },
{ id: 'n3', data: { label: 'C' } },
],
edges: [
{ source: 'n1', target: 'n2' },
{ source: 'n2', target: 'n3' },
{ source: 'n3', target: 'n1' }, // 回环边
],
},
node: {
type: 'circle',
style: {
size: 40,
fill: '#1783FF',
labelText: (d) => d.data.label,
labelPlacement: 'center',
labelFill: '#fff',
},
},
edge: {
type: 'cubic', // 通用曲线
style: {
stroke: '#aaa',
lineWidth: 1.5,
endArrow: true,
},
},
layout: { type: 'circular', radius: 150 },
behaviors: ['drag-canvas', 'zoom-canvas'],
});
graph.render();常用变体
垂直层次图(配合 dagre TB)
import { Graph } from '@antv/g6';
const graph = new Graph({
container: 'container',
data: {
nodes: [
{ id: 'root', data: { label: '根节点' } },
{ id: 'a', data: { label: '子节点A' } },
{ id: 'b', data: { label: '子节点B' } },
{ id: 'c', data: { label: '子节点C' } },
],
edges: [
{ source: 'root', target: 'a' },
{ source: 'root', target: 'b' },
{ source: 'root', target: 'c' },
],
},
node: {
type: 'rect',
style: {
size: [100, 36],
radius: 4,
fill: '#f0f5ff',
stroke: '#adc6ff',
labelText: (d) => d.data.label,
labelPlacement: 'center',
},
},
edge: {
type: 'cubic-vertical', // 垂直 S 形曲线
style: {
stroke: '#adc6ff',
endArrow: true,
},
},
layout: {
type: 'dagre',
rankdir: 'TB',
ranksep: 60,
nodesep: 20,
},
behaviors: ['drag-canvas', 'zoom-canvas'],
});水平流程图(配合 dagre LR)
edge: {
type: 'cubic-horizontal', // 水平 S 形曲线
style: {
stroke: '#91caff',
lineWidth: 2,
endArrow: {
type: 'triangle',
fill: '#91caff',
size: 8,
},
labelText: (d) => d.data.label,
labelBackground: true,
labelBackgroundFill: '#fff',
labelBackgroundOpacity: 0.9,
},
},
layout: {
type: 'dagre',
rankdir: 'LR', // 从左到右
ranksep: 80,
nodesep: 30,
},辐射布局中的曲线边
// 辐射布局中 cubic 效果最好
edge: {
type: 'cubic',
style: {
stroke: '#ccc',
lineWidth: 1,
endArrow: false,
curveOffset: 30, // 控制弯曲幅度
},
},
layout: {
type: 'radial',
unitRadius: 100,
focusNode: 'center',
},渐变色边
// 使用线性渐变(需要 @antv/g 的渐变支持)
edge: {
type: 'cubic',
style: {
stroke: 'l(0) 0:#1783FF 1:#FF6B6B', // 渐变色
lineWidth: 2,
endArrow: true,
},
},常见错误
错误1:方向不匹配
// ❌ dagre LR 布局用 cubic-vertical(垂直曲线)
layout: { type: 'dagre', rankdir: 'LR' },
edge: { type: 'cubic-vertical' }, // 方向不匹配,曲线不美观
// ✅ LR 布局用 cubic-horizontal
layout: { type: 'dagre', rankdir: 'LR' },
edge: { type: 'cubic-horizontal' },
// ✅ TB 布局用 cubic-vertical
layout: { type: 'dagre', rankdir: 'TB' },
edge: { type: 'cubic-vertical' },错误2:curveOffset 方向混淆
// curveOffset 正值向右/上弯,负值向左/下弯
edge: {
type: 'cubic',
style: {
curveOffset: 50, // 正值向一侧弯
// curveOffset: -50, // 负值向另一侧弯
},
},Related skills
How it compares
Pick antv-g6-graph for relationship topology; use AntV G2 or ECharts skills when the visualization is primarily statistical rather than graph-structured.
FAQ
What does antv-g6-graph help developers build?
antv-g6-graph helps developers build interactive AntV G6 network visualizations—nodes, edges, layouts, and events—for dashboards, knowledge graphs, and agent tooling UIs in web frontends.
When should developers choose antv-g6-graph?
Developers should choose antv-g6-graph when relationship or topology data needs an interactive graph UI with G6 layouts and handlers, not when simpler AntV cartesian or stat charts are enough.
Which AntV library does antv-g6-graph target?
antv-g6-graph targets AntV G6 specifically from the antvis/chart-visualization-skills repository, focusing on graph and network rendering rather than general-purpose chart types.