Cytoscape.js 使用文档与说明

基于 Cytoscape.js v3.34 官方文档 整理

一、什么是 Cytoscape.js

Cytoscape.js 是一个由多伦多大学 Donnelly Centre 开发的开源 JavaScript 图论/网络可视化库,采用 MIT 许可证,已在《Bioinformatics》(2016、2023)期刊发表学术论文。不同于 AntV X6(偏图编辑器),Cytoscape.js 专注于图数据的可视化分析,擅长处理大规模节点-边关系展示与交互。

支持的图模型

图类型支持
有向图(Directed)
无向图(Undirected)
混合图(Mixed)
多重图(Multigraph,多重边)
自环(Loops)
复合图(Compound,父子节点嵌套)

核心特性

  • 图论原生支持:内置节点度、最短路径(Dijkstra/A*)、连通分量、PageRank 等图算法
  • 丰富布局算法:8 种核心内置布局(circle、concentric、breadthfirst、grid、cose、random、preset、null),通过官方第一方扩展可扩展至 15+(dagre、klay、avsdf、cola、fcose、elk 等)
  • 高性能渲染:Canvas 2D 渲染,支持数千节点的流畅交互
  • 移动端友好:原生触摸手势支持(pinch-zoom、pan 双指缩放)
  • 可扩展架构:通过 cytoscape.use(ext) 注册第一方及社区扩展,涵盖布局、UI 控件、数据导入等
  • 多模块格式:支持 UMD(cytoscape.min.js)、ESM(cytoscape.esm.min.mjs)、CommonJS(cytoscape.cjs.js
  • 跨环境运行:支持所有现代浏览器,Node.js headless 模式可用于纯计算/服务端渲染

适用场景

场景是否推荐
人物关系图、组织架构图✅ 强推
知识图谱可视化✅ 强推
网络拓扑图✅ 推荐
社交网络分析✅ 强推
流程图编辑器❌ 不适合(用 X6)
思维导图❌ 不适合(用 X6)

二、快速开始

模块格式说明

根据构建目标选择合适的引入方式:

格式文件引入方式
ESM(推荐)cytoscape.esm.min.mjsimport cytoscape from 'cytoscape'
CJScytoscape.cjs.jsconst cytoscape = require('cytoscape')
UMDcytoscape.min.js<script src="..."><&#x2F;script>

安装

1
2
3
4
5
6
7
8
# npm
npm install cytoscape

# pnpm
pnpm add cytoscape

# yarn
yarn add cytoscape

cy.ready() 回调

官方推荐使用 cy.ready() 替代手动监听 layoutstop 来等待图初始化完成(因为可能没有 layout 事件):

1
2
3
4
5
cy.ready(event => {
// 图数据加载完成且初始布局已应用
console.log('图已就绪,节点数:', cy.nodes().length);
cy.fit(undefined, 30);
});

基础示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
<style>
#cy {
width: 100%;
height: 600px;
border: 1px solid #333;
}
</style>
</head>
<body>
<div id="cy"></div>
<script>
const cy = cytoscape({
container: document.getElementById('cy'),

elements: [
// 节点
{ data: { id: 'a', label: '张三' } },
{ data: { id: 'b', label: '李四' } },
{ data: { id: 'c', label: '王五' } },
// 边
{ data: { id: 'ab', source: 'a', target: 'b', label: '同事' } },
{ data: { id: 'bc', source: 'b', target: 'c', label: '亲属' } },
],

style: [
{
selector: 'node',
style: {
'background-color': '#4e56fd',
label: 'data(label)',
color: '#fff',
'font-size': 12,
},
},
{
selector: 'edge',
style: {
width: 2,
'line-color': '#999',
'target-arrow-color': '#999',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
label: 'data(label)',
},
},
],

layout: {
name: 'grid',
},
});
</script>
</body>
</html>

在 Vue 3 项目中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!-- GraphDemo.vue -->
<template>
<div class="graph-page">
<div ref="cyRef" class="cy-container"></div>
</div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
import cytoscape from 'cytoscape';

const cyRef = ref(null);
let cy = null;

onMounted(async () => {
await nextTick();
cy = cytoscape({
container: cyRef.value,
elements: [{ data: { id: 'a', label: '节点A' } }, { data: { id: 'b', label: '节点B' } }, { data: { id: 'ab', source: 'a', target: 'b' } }],
style: [
{ selector: 'node', style: { 'background-color': '#4e56fd', label: 'data(label)' } },
{ selector: 'edge', style: { 'line-color': '#999', 'target-arrow-shape': 'triangle' } },
],
layout: { name: 'dagre' },
});
});

onBeforeUnmount(() => {
cy?.destroy();
});
</script>

<style scoped>
.cy-container {
width: 100%;
height: 600px;
}
</style>

三、核心概念

3.1 核心对象(Core)

cytoscape() 返回的 cy 对象是整个图实例,所有操作都通过它进行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const cy = cytoscape({ /* options */ })

// 常用 Core 方法
cy.add([...]) // 添加元素
cy.remove('node#a') // 删除元素
cy.getElementById('a') // 获取元素
cy.nodes() // 获取所有节点
cy.edges() // 获取所有边
cy.layout({...}).run() // 运行布局
cy.fit() // 适应画布
cy.center() // 居中所有元素
cy.zoom(2) // 缩放到 200%
cy.pan({ x: 50, y: 0 }) // 平移画布
cy.destroy() // 销毁实例

3.2 集合(Collection)

cy.nodes()cy.edges()cy.filter() 等返回的是集合(Collection),支持链式调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取所有被选中的节点
cy.nodes(':selected');

// 获取边权重 > 0.5 的边
cy.edges('[weight > 0.5]');

// 获取节点 a 的邻域(1 跳)
cy.getElementById('a').neighborhood();

// 获取节点 a 的相邻节点
cy.getElementById('a').connectedEdges().connectedNodes();

// 获取节点 a 到 b 的最短路径
cy.getElementById('a').aStar({ root: '#b' });

3.3 元素(Elements)

每个节点和边都是一个元素对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const node = cy.getElementById('a');

// 获取/设置数据
node.data('label'); // '张三'
node.data('label', '张三丰'); // 更新 label
node.data(); // { id: 'a', label: '张三丰' }

// 位置操作
node.position(); // { x: 100, y: 200 }
node.position({ x: 300, y: 400 });

// CSS 样式(渲染样式,非 data)
node.style('background-color', 'red');

// 节点特有
node.degree(); // 度(相连边数)
node.indegree(); // 入度
node.outdegree(); // 出度

// 边特有
const edge = cy.getElementById('ab');
edge.source(); // 源节点对象
edge.target(); // 目标节点对象
edge.isLoop(); // 是否是自环

3.4 Notation(关键位置/尺寸概念)

Cytoscape.js 官网清晰区分了以下 notation,理解它们是深入使用的关键:

术语官方定义示例 / 说明
model position元素的图模型坐标,即 node.position() 返回的 {x, y}布局算法操作的坐标,存储在图模型中
model dimensions元素的图模型宽高,受 style width/height 控制影响布局的碰撞检测
rendered position元素在 Canvas 视口上的屏幕像素坐标随 pan/zoom 变化,用于右键菜单位置等 UI 交互
rendered dimensions元素在 Canvas 视口上的屏幕像素尺寸受 zoom 缩放影响
1
2
3
4
5
6
7
8
9
// 坐标转换
const modelPos = node.position(); // 模型坐标
const renderedPos = node.renderedPosition(); // Canvas 像素坐标

// 事件中获取
cy.on('tap', 'node', evt => {
evt.position; // model position
evt.renderedPosition; // rendered position (用于定位 popover/tooltip)
});

3.5 Compound Nodes(复合节点 / 父子嵌套)

Cytoscape.js 原生支持父子节点嵌套——节点可以包含子节点,形成层级结构的复合图。这是 AntV X6 不具备的原生能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 定义父子关系(在 data 中通过 parent 字段指定)
const cy = cytoscape({
elements: [
// 父节点(容器)
{ data: { id: 'parent-1', label: '分组 A' } },
// 子节点
{ data: { id: 'a', parent: 'parent-1', label: '张三' } },
{ data: { id: 'b', parent: 'parent-1', label: '李四' } },
// 无父节点的正常节点
{ data: { id: 'c', label: '王五' } },
],
style: [
{ selector: 'node', style: { label: 'data(label)' } },
// 父节点样式(不同的边框和背景)
{
selector: ':parent',
style: {
'background-opacity': 0.3,
'background-color': '#1a3a5c',
'border-color': '#2a6eb0',
'border-width': 2,
'border-style': 'dashed',
},
},
// 边也支持复合:连接两个父节点
{ data: { id: 'e1', source: 'parent-1', target: 'c', label: '组间关联' } },
],
});

关键 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 检查节点是否为父节点
node.isParent(); // true 如果该节点有子节点

// 获取父节点 / 子节点
node.parent(); // 返回父节点(或空集合)
node.children(); // 返回直接子节点
node.descendants(); // 返回所有后代(递归)
node.ancestors(); // 返回所有祖先

// 修改父子关系
node.move({ parent: 'parent-1' }); // 将 node 移动到 parent-1 下
node.move({ parent: null }); // 从父节点中移出

// 折叠/展开
node.collapse(); // 折叠(隐藏子节点)
node.expand(); // 展开(显示子节点)

3.6 Scratch Data(临时绑定数据)

node.data()(会被序列化)外,Cytoscape.js 还提供 scratch() 方法用于绑定运行时临时数据,不会被导出/序列化:

1
2
3
4
5
6
7
8
9
10
11
// 绑定临时数据
node.scratch('isExpanded', true)
node.scratch('_tooltipTimer', setTimeout(...))
node.scratch('_positions', { originalX: 100, originalY: 200 })

// 读取
if (node.scratch('isExpanded')) { ... }

// 删除
node.removeScratch('isExpanded') // 删除指定 key
node.scratch() // 获取整个 scratch 对象

3.7 Batch Operations(批量操作)

官网推荐使用 cy.batch() 进行批量操作以提升性能。批量回调中的位置变化会在回调结束后一次性应用,减少多次重绘:

1
2
3
4
5
6
7
8
9
// 批量添加节点并设置初始位置,只触发一次重绘
cy.batch(() => {
for (let i = 0; i < 100; i++) {
cy.add({
data: { id: `node-${i}`, label: `节点 ${i}` },
position: { x: Math.random() * 1000, y: Math.random() * 600 },
});
}
});

cy.batch()cy.startBatch() / cy.endBatch() 的区别:

方法说明
cy.batch(callback)推荐方式,自动管理批处理周期。callback 可嵌套(内层与外层共享同一个批)
cy.startBatch()手动开始批处理
cy.endBatch()手动结束批处理

注意:多层嵌套的 batch() 调用中,渲染只在最外层 batch 结束后触发一次。

3.8 Graph Model(elements JSON 格式)

Cytoscape.js 的 elements 使用扁平 JSON 格式,不同于 X6 的嵌套 model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const elements = {
nodes: [{ data: { id: 'n1', label: '节点A', type: 'root', weight: 10 } }, { data: { id: 'n2', label: '节点B', type: 'branch' } }, { data: { id: 'n3', label: '节点C', type: 'leaf' } }],
edges: [
{
data: {
id: 'e1',
source: 'n1',
target: 'n2',
label: '就职于',
relType: 'RELATED',
weight: 5,
},
},
{
data: {
id: 'e2',
source: 'n1',
target: 'n3',
label: '拥有',
relType: 'CONTAINS',
},
},
],
};

重要:节点的 position 是可选字段,如果不指定则布局算法自动计算。data 中除 id/source/target 外的字段都是自定义业务数据。

四、选择器系统(Selectors)

Cytoscape.js 的选择器与 CSS 非常相似,用于从图中筛选元素。所有 cy.filter()cy.nodes()cy.edges()cy.$() 等方法都接受选择器字符串。

4.1 基础选择器

选择器语法示例说明
按类型node / edge / *cy.nodes() = cy.filter('node')* 匹配所有元素
按 ID#idcy.$('#a')精确匹配元素 ID
按类名.classNamecy.$('.a-class')元素可以有多个类名(空格分隔)
按数据字段[field] / [field = value]cy.$('[type = "root"]')支持 =, !=, >, >=, <, <=
按 scratch 数据[[field]] / [[field = value]]cy.$('[[expanded = true]]')匹配 scratch() 绑定的临时数据

4.2 复合选择器

1
2
3
4
5
6
7
8
9
10
// 同时匹配多个条件(AND)
cy.$('node[type = "root"][degree > 5]'); // type 为 root 且度 > 5

// 匹配多个选择器之一(OR 用逗号分隔)
cy.$('node[type = "root"], node[type = "branch"]');

// 按元数据字段(meta fields,使用 ? 前缀)
cy.$('node[?degree]'); // 有 degree 值的节点
cy.$('node[?degree > 5]'); // degree 值 > 5
cy.$('node[?removed = false]');

4.3 状态伪类选择器

选择器说明
:selected被选中的元素
:unselected未被选中的元素
:selectable可被选中的元素
:visible可见元素
:hidden隐藏元素(display: nonevisibility: hidden
:locked被锁定的节点
:animated正在执行动画的元素
:childless无子节点的节点
:parent有子节点的节点
:orphan无父节点的节点

4.4 图元字段选择器(Metadata / degree 等)

以下字段作为元素的原生图元属性,可使用 ? 前缀查询:

字段说明适用于
?degree度(相连边数)node
?indegree入度node
?outdegree出度node
?isEdge是否为边node, edge
?isLoop是否为自环edge
?isSimple是否为简单边(非 Loop)edge
?source源节点 IDedge
?target目标节点 IDedge
?group元素分组 (‘nodes’/‘edges’)node, edge
?removed是否已被移除node, edge
1
2
3
4
5
// 实用选择器示例
cy.$('node[?degree > 10]'); // degree > 10 的枢纽节点
cy.$('node:orphan'); // 孤立节点(无父节点)
cy.$('edge[?isLoop]'); // 所有自环边
cy.$('node[?removed = false]:visible'); // 当前可见且未被移除的节点

4.5 函数式选择器

1
2
3
4
5
// 使用函数作为过滤器
cy.nodes().filter(node => node.degree() > 5);

// 等效的选择器写法
cy.$('node[?degree > 5]');

五、节点与边的样式(Style)

5.1 声明式样式架构

Cytoscape.js 的样式系统是全声明式的,与 CSS 高度相似,使用选择器 + 属性对象进行组织:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const style = [
{
selector: 'node', // CSS-like 选择器(支持全部选择器语法)
style: {
// 样式键值对
width: 60,
height: 60,
// 属性值可以使用函数动态计算
'background-color': ele => typeColorMap[ele.data('type')] || '#ccc',
},
},
{
selector: 'node:selected',
style: {
'border-width': 3,
'border-color': 'rgb(78, 162, 240)',
},
},
];

// 运行时动态修改样式
cy.style().selector('node:selected').style('border-color', '#ff4444').update(); // .update() 应用更改,否则不生效

// 批量更新多个选择器
cy.style().selector('node').style('background-opacity', 0.8).selector('edge').style('opacity', 0.5).update();

重要:样式更新必须调用 .update() 才会生效。如果忘记调用,样式不会应用到画布。

5.2 核心 / 容器级样式

这些样式属性在 cytoscape() 初始化选项中直接配置(不在 style 数组内):

属性类型默认值说明
selectionTypestring'single'选择模式:'single'(单选)、'additive'(多选/按住 Shift)
selectionBoxColorstring'#2a65b7'框选矩形边框颜色
selectionBoxOpacitynumber0.3框选矩形填充透明度
selectionBoxBorderWidthnumber1框选矩形边框宽度
activeBgColorstring'#35b757'活跃背景色(用于指示拖拽目标等)
activeBgOpacitynumber0.2活跃背景透明度
activeBgSizenumber1活跃背景相对于节点尺寸的缩放比例

5.3 节点形状完整列表

Cytoscape.js 官方共提供 12 种内置节点形状,通过 shape 属性控制:

形状名效果说明
ellipse椭圆形默认形状width=height 时即为正圆
rectangle矩形直角矩形
roundrectangle圆角矩形配合 border-radius 控制圆角半径
round-rectangle圆角矩形(别名)同上
triangle三角形等边三角形
pentagon五边形正五边形
hexagon六边形正六边形
heptagon七边形正七边形
octagon八边形正八边形
diamond菱形旋转 45° 的正方形
veeV 形倒三角形/箭头形
star星形五角星
tag标签形右侧有尖角的标签形状
rhomboid平行四边形倾斜的矩形
cut-rectangle切角矩形右上角被切掉的矩形
barrel桶形上下边弯曲的矩形
bottom-round-rectangle底部圆角矩形仅底部有圆角
concave-hexagon凹六边形凹进的六边形
polygon多边形通过 shape-polygon-points 自定义多边形边数
1
2
3
4
// 形状示例
{ selector: 'node', style: { 'shape': 'roundrectangle', 'border-radius': '8px' } }
{ selector: 'node[type="case"]', style: { 'shape': 'diamond' } }
{ selector: 'node[type="leaf"]', style: { 'shape': 'hexagon' } }

5.4 完整节点样式属性表

属性类型说明
widthnumber / string节点宽度(px),可动态函数
heightnumber / string节点高度(px),可动态函数
shapestring节点形状(见上表)
shape-polygon-pointsnumbershapepolygon 时的边数
border-radiusnumber圆角半径(roundrectangle 时有效)
background-colorstring节点背景色
background-opacitynumber (0-1)背景透明度
background-imagestring / function背景图片 URL
background-fitstring图片适配:'none''contain''cover'
background-image-opacitynumber (0-1)背景图片透明度
background-clipstring裁剪:'none''node'
background-width / background-heightnumber / string背景图尺寸(相对于节点)
background-position-x / background-position-ynumber / string背景图偏移
border-widthnumber边框宽度
border-stylestring'solid''dotted''dashed''double'
border-colorstring边框颜色
border-opacitynumber (0-1)边框透明度
paddingnumber / string内容内边距(影响复合节点的子节点边界)
padding-relative-tostring'width' / 'height' / 'average' / 'min' / 'max'
Label 属性
labelstring / function标签内容,支持 'data(key)' 或函数
colorstring标签文字颜色
font-sizenumber字体大小(px)
font-familystring字体族
font-weightstring字重:'normal''bold''lighter'、数字
font-stylestring字体样式:'normal''italic'
text-valignstring垂直对齐:'top''center''bottom'
text-halignstring水平对齐:'left''center''right'
text-margin-xnumber文字水平偏移
text-margin-ynumber文字垂直偏移
text-wrapstring换行:'none''wrap''ellipsis'
text-max-widthnumber文字最大宽度(超出则换行)
text-rotationnumber / string文字旋转角度(deg/rad)
text-outline-widthnumber文字描边宽度
text-outline-colorstring文字描边颜色
text-outline-opacitynumber (0-1)文字描边透明度
text-background-colorstring文字背景色
text-background-opacitynumber (0-1)文字背景透明度
text-background-shapestring文字背景形状:'rectangle''roundrectangle'
text-background-paddingstring文字背景内边距,如 '3px'
text-border-colorstring文字边框颜色
text-border-widthnumber文字边框宽度
text-border-stylestring文字边框样式
text-border-opacitynumber (0-1)文字边框透明度
Ghost 效果
ghoststring'yes' / 'no' — 拖拽时显示节点半透明鬼影
active-bg-colorstring活跃状态背景色
active-bg-opacitynumber (0-1)活跃状态背景透明度
active-bg-sizenumber活跃状态背景缩放比例
其他
displaystring'element' / 'none' — 隐藏节点(不参与布局)
visibilitystring'visible' / 'hidden' — 隐藏节点(仍占布局空间)
opacitynumber (0-1)元素透明度
z-indexnumber层级(影响渲染顺序和事件命中顺序)
z-compound-depthstring'auto' / 'top' / 'bottom' — 复合层级中的 z 排序策略
z-index-comparestring'auto' / 'manual' — 是否自动处理子元素 z-index
min-zoomed-font-sizenumber最小可读字体(缩小时字体不会小于此值)
eventsstring'yes' / 'no' — 是否接收事件
overlay-colorstring遮罩颜色
overlay-opacitynumber (0-1)遮罩透明度
overlay-paddingnumber遮罩扩展宽度

5.5 完整边样式属性表

属性类型说明
widthnumber边线宽度
line-colorstring边线颜色
line-stylestring'solid''dotted''dashed''double'
line-opacitynumber (0-1)边线透明度
line-capstring端点样式:'butt''round''square'
line-fillstring填充模式:'solid''linear-gradient''radial-gradient'
line-dash-patternnumber[]虚线模式,如 [6, 3]
line-dash-offsetnumber虚线偏移
曲线样式
curve-stylestring'haystack'(直线)、'straight'(可弯曲直线)、'bezier'(贝塞尔曲线、默认)、'unbundled-bezier'(不捆绑贝塞尔、平行边自动分离)、'segments'(自定义折线)、'taxi'(直角折线/正交)
haystack-radiusnumberhaystack 曲线半径
control-point-step-sizenumberbezier 控制点的步进大小
control-point-distancenumberunbundled-bezier 控制点距离
control-point-weightnumberunbundled-bezier 控制点权重
segment-distancesnumber[]segments 曲线各段的长度
segment-weightsnumber[]segments 曲线各段的权重
edge-distancesstringtaxi 曲线模式:'node-position' / 'intersection' / 'node-center'
箭头
arrow-scalenumber箭头缩放比例
target-arrow-shapestring目标端箭头形状
target-arrow-colorstring目标端箭头颜色
target-arrow-fillstring箭头填充:'filled''hollow'
source-arrow-shapestring源端箭头形状
source-arrow-colorstring源端箭头颜色
source-arrow-fillstring箭头填充:'filled''hollow'
mid-target-arrow-shapestring中段目标箭头
mid-target-arrow-colorstring中段目标箭头颜色
mid-source-arrow-shapestring中段源箭头
mid-source-arrow-colorstring中段源箭头颜色
Label 属性(与节点 label 属性基本相同)
source-labelstring源端标签文本
target-labelstring目标端标签文本
source-text-offsetnumber源端标签偏移
target-text-offsetnumber目标端标签偏移
其他
opacitynumber (0-1)透明度
displaystring'element' / 'none'
visibilitystring'visible' / 'hidden'
z-indexnumber层级
eventsstring'yes' / 'no'
overlay-colorstring遮罩颜色
overlay-opacitynumber (0-1)遮罩透明度
overlay-paddingnumber遮罩扩展

5.6 完整箭头形状列表

箭头形状名效果说明
triangle标准三角形
triangle-tee三角形 + 横线(T 边框效果)
circle-triangle圆形三角形
triangle-crossX 形三角
triangle-backcurve后弯三角
veeV 形
tee横线
square方块
circle圆形
diamond菱形
chevronV 形箭头
none无箭头
half-triangle-overshot半三角超伸

5.7 样式声明完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const style = [
// 通用节点样式
{
selector: 'node',
style: {
width: 60,
height: 60,
'background-color': '#4e56fd',
'border-width': 2,
'border-color': '#fff',
label: 'data(label)',
color: '#fff',
'font-size': 12,
'text-valign': 'bottom',
'text-halign': 'center',
'text-margin-y': 8,
},
},

// 按 data 字段选择(type 为 root 的节点)
{
selector: 'node[type="root"]',
style: {
shape: 'ellipse',
'background-color': '#007AFC',
},
},

// 按 data 字段选择(type 为 branch 的节点)
{
selector: 'node[type="branch"]',
style: {
shape: 'roundrectangle',
'background-color': '#71ED4D',
},
},

// 选中状态
{
selector: 'node:selected',
style: {
'border-width': 3,
'border-color': 'rgb(78, 162, 240)',
'overlay-opacity': 0.1,
'overlay-color': '#4ea2f0',
'overlay-padding': 6,
},
},

// 边的通用样式
{
selector: 'edge',
style: {
width: 2,
'line-color': '#999',
'target-arrow-color': '#999',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
label: 'data(label)',
'font-size': 10,
},
},

// 不同类型边不同颜色(函数式动态样式)
{
selector: 'edge',
style: {
'line-color': ele => edgeColorMap[ele.data('relType')] || '#999',
'target-arrow-color': ele => edgeColorMap[ele.data('relType')] || '#999',
},
},
];

5.8 使用图片作为节点背景

不同类型节点使用不同图标是最常见的需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 方案一:通过样式声明
{
selector: 'node[type="root"]',
style: {
'background-image': './static/images/node-icon.png',
'background-fit': 'cover',
'width': 40,
'height': 40,
}
}

// 方案二:运行时动态设置(适用于从后端接口获取图标 URL)
cy.nodes('[type="root"]').forEach(node => {
node.style('background-image', `./static/images/${node.data('iconType')}.png`)
node.style('background-fit', 'cover')
})

5.9 暗色主题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const darkThemeStyle = [
{
selector: 'node',
style: {
'background-color': '#1a3a5c',
'border-color': '#2a6eb0',
color: '#e0e0e0',
'font-size': 11,
},
},
{
selector: 'edge',
style: {
'line-color': '#3a5a8c',
'target-arrow-color': '#3a5a8c',
'loop-direction': '-45deg',
'loop-sweep': '-90deg',
},
},
];

5.10 常用样式属性速查

节点样式

属性说明示例值
width / height节点尺寸60
background-color背景色'#4e56fd'
background-image背景图片'url(path)'
background-fit背景图适配方式'cover' / 'contain' / 'none'
border-width边框宽度2
border-color边框颜色'#fff'
border-style边框样式'solid' / 'dashed' / 'double'
shape节点形状'ellipse' / 'rectangle' / 'roundrectangle' / 'diamond' / 'hexagon'
label标签文本'data(label)'
color标签颜色'#fff'
font-size字体大小12
text-valign文字垂直对齐'center' / 'top' / 'bottom'
text-halign文字水平对齐'center' / 'left' / 'right'
text-margin-y文字垂直偏移8
opacity透明度0.8
z-index层级10

边样式

属性说明示例值
width边宽度2
line-color边颜色'#999'
line-style边样式'solid' / 'dashed' / 'dotted'
curve-style曲线样式'bezier' / 'haystack' / 'straight' / 'unbundled-bezier'
target-arrow-shape箭头形状'triangle' / 'triangle-backcurve' / 'chevron' / 'tee' / 'diamond' / 'none'
target-arrow-color箭头颜色'#999'
source-arrow-shape源端箭头同上
arrow-scale箭头缩放1.5

六、布局系统(Layout)

6.1 内置布局速查

Cytoscape.js 提供 10+ 内置布局,无需额外安装:

布局名说明适用场景
dagre层次布局(自上而下)⭐ 人员关系、组织架构
breadthfirst广度优先布局树状层级数据
concentric同心圆布局中心辐射关系
circle圆形布局对等关系展示
cose力导向布局(CoSE)社交网络、大规模图
cose-bilkent增强力导向布局大规模图优化
grid网格布局等距排列
random随机布局初始占位
preset预设位置保持已有坐标
null空布局(不改变位置)手动管理位置

6.2 布局配置详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// dagre - 树形层次布局(最常用)
cy.layout({
name: 'dagre',
rankDir: 'TB', // 方向: TB(上→下) | LR(左→右) | BT(下→上) | RL(右→左)
nodeSep: 60, // 同层节点间距
edgeSep: 10, // 边间距
rankSep: 100, // 层级间距
nodeDimensionsIncludeLabels: true, // 节点尺寸是否包含标签
animate: true, // 是否动画过渡
animationDuration: 500, // 动画时长 (ms)
}).run();

// circle - 圆形布局
cy.layout({
name: 'circle',
fit: true, // 是否适应视口
avoidOverlap: true, // 是否避免重叠
nodeDimensionsIncludeLabels: true,
startAngle: 0, // 起始角度
clockwise: true, // 是否顺时针
}).run();

// cose - 力导向布局
cy.layout({
name: 'cose',
idealEdgeLength: 100, // 理想边长
nodeOverlap: 20, // 节点重叠容忍度
gravity: 1, // 重力强度
numIter: 1000, // 最大迭代次数
animate: true,
fit: true,
}).run();

// grid - 网格布局
cy.layout({
name: 'grid',
rows: undefined, // 行数(留空则自动计算)
cols: undefined, // 列数
avoidOverlap: true,
}).run();

// concentric - 同心圆布局
cy.layout({
name: 'concentric',
concentric: node => node.degree(), // 按度数分层
minNodeSpacing: 40,
}).run();

// breadthfirst - 广度优先布局
cy.layout({
name: 'breadthfirst',
directed: true,
spacingFactor: 1.5,
roots: '#a', // 指定根节点
}).run();

6.3 官方第一方扩展布局

扩展包布局名适用场景安装
cytoscape-dagredagre层次布局(自上而下/左到右)npm i cytoscape-dagre
cytoscape-klayklayKlay 分层布局(支持端口/复合节点)npm i cytoscape-klay
cytoscape-avsdfavsdf圆形力导向(避免节点重叠)npm i cytoscape-avsdf
cytoscape-colacolaCoLa 约束布局(支持对齐约束)npm i cytoscape-cola
cytoscape-fcosefcosefCoSE 快速力导向(CoSE 的增强版)npm i cytoscape-fcose
cytoscape-ciseciseCiSE 圆形簇布局npm i cytoscape-cise
cytoscape-spreadspread展开布局(将重叠节点分散)npm i cytoscape-spread
cytoscape-elkelkELK 布局引擎(最强分层布局)npm i cytoscape-elk
cytoscape-cose-bilkentcose-bilkent大规模力导向(支持万级节点)npm i cytoscape-cose-bilkent

常用扩展布局:

1
2
3
4
pnpm add cytoscape-avsdf   # 圆形力导向布局
pnpm add cytoscape-klay # Klay 分层布局(横向)
pnpm add cytoscape-cola # CoLa 约束布局
pnpm add cytoscape-dagre # dagre 布局(推荐显式安装)
1
2
3
4
5
6
7
8
9
10
import cytoscape from 'cytoscape';
import avsdf from 'cytoscape-avsdf';
import klay from 'cytoscape-klay';

cytoscape.use(avsdf); // 注册 AVSDF 布局
cytoscape.use(klay); // 注册 Klay 布局

// 使用
cy.layout({ name: 'avsdf', animate: true }).run();
cy.layout({ name: 'klay', klay: { direction: 'RIGHT' } }).run();

6.4 布局切换实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 布局切换函数
const layoutModes = {
dagre: { name: 'dagre', rankDir: 'TB', nodeSep: 60, rankSep: 100, animate: true },
circle: { name: 'circle', fit: true, avoidOverlap: true, animate: true },
grid: { name: 'grid', avoidOverlap: true, animate: true },
cose: { name: 'cose', idealEdgeLength: 100, numIter: 1000, animate: true },
avsdf: { name: 'avsdf', animate: true },
klay: { name: 'klay', klay: { direction: 'RIGHT' }, animate: true },
concentric: { name: 'concentric', concentric: n => n.degree(), animate: true },
breadthfirst: { name: 'breadthfirst', directed: true, animate: true },
};

const switchLayout = mode => {
if (!cy || mode === currentLayout.value) return;
const config = layoutModes[mode];
if (!config) return;

currentLayout.value = mode;
cy.layout(config).run();
};

七、事件系统

7.1 事件绑定与解绑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 绑定事件
cy.on('tap', 'node', evt => {
const node = evt.target;
console.log('点击节点:', node.data('label'));
});

// 解绑
cy.off('tap', 'node');

// 一次性事件
cy.one('layoutstop', () => {
console.log('首布局完成');
});

// 派发自定义事件
cy.emit('customEvent', { msg: 'hello' });

7.2 常用事件分类

交互事件

事件触发时机
tap点击
dbltap双击
cxttap右键点击(context menu)
tapstart / tapend按下 / 释放
mousedown / mouseup / mousemove鼠标事件
mouseover / mouseout鼠标进入/离开

拖拽事件

事件触发时机
grab开始拖拽节点
drag拖拽过程中(高频触发)
free释放节点
dragfree释放后(别名)
dragfreeon释放到某个位置

视口事件

事件触发时机
zoom缩放变化
pan平移变化
resize容器尺寸变化
viewport视口任何变化(zoom/pan/resize)

布局事件

事件触发时机
layoutstart布局开始
layoutready布局就绪(初始位置已计算)
layoutstop布局完成

元素变更事件

事件触发时机
add添加元素
remove删除元素
data元素的 data 更新
position节点位置变化

7.3 实战:交互示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 右键菜单
cy.on('cxttap', 'node', evt => {
const node = evt.target;
const renderedPos = evt.renderedPosition; // 画面坐标
const modelPos = evt.position; // 模型坐标

showContextMenu({
x: renderedPos.x,
y: renderedPos.y,
items: [
{
label: '展开关联节点',
action: () => expandNode(node),
},
{
label: '高亮关联',
action: () => highlightNeighbors(node),
},
{
label: '复制节点名称',
action: () => copyToClipboard(node.data('label')),
},
{
label: '删除节点',
action: () => cy.remove(node),
},
],
});
});

// 双击节点 - 查看详情
cy.on('dbltap', 'node', async evt => {
const node = evt.target;
const detail = await fetchNodeDetail(node.id());
showNodeDetailDialog(detail);
});

// 双击边 - 查看关系详情
cy.on('dbltap', 'edge', async evt => {
const edge = evt.target;
const detail = await fetchRelationDetail(edge.id());
showEdgeDetailDialog(detail);
});

// 拖拽时邻域节点跟随移动
cy.nodes().on('drag', evt => {
const draggedNode = evt.target;
const delta = { x: evt.originalEvent.movementX, y: evt.originalEvent.movementY };
// 相邻节点跟随移动
draggedNode
.neighborhood()
.nodes()
.forEach(neighbor => {
const pos = neighbor.position();
neighbor.position({
x: pos.x + delta.x * 0.3, // 阻尼系数 0.3
y: pos.y + delta.y * 0.3,
});
});
});

八、交互与操作

8.1 画布控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 缩放
cy.zoom(1.5); // 缩放到 150%
cy.zoom({ level: 1.5, renderedPosition: { x: 300, y: 200 } }); // 以某点为中心缩放
cy.zoom(cy.zoom() + 0.1); // 放大 0.1

// 适应画布
cy.fit(); // 适应所有元素
cy.fit(cy.nodes(':selected'), 50); // 适应选中元素,留 50px 边距

// 居中
cy.center(); // 居中所有元素
cy.center(cy.nodes(':selected')); // 居中选中元素

// 平移
cy.pan({ x: 50, y: 0 });
cy.panBy({ x: 100, y: 0 });

// 动画
cy.animate(
{
zoom: 1.5,
pan: { x: 200, y: 100 },
center: { eles: cy.nodes(':selected') },
},
{
duration: 500,
},
);

8.2 元素操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 添加元素
const newNode = cy.add({
group: 'nodes', // 'nodes' | 'edges'
data: { id: 'new-node', label: '新节点' },
position: { x: 300, y: 200 },
});

const newEdge = cy.add({
group: 'edges',
data: {
id: 'new-edge',
source: 'a',
target: 'new-node',
label: '新关系',
},
});

// 批量添加
cy.add([{ data: { id: 'n1', label: 'A' }, position: { x: 100, y: 100 } }, { data: { id: 'n2', label: 'B' }, position: { x: 200, y: 200 } }, { data: { id: 'e1', source: 'n1', target: 'n2' } }]);

// 删除
cy.remove('#new-node'); // 通过选择器
cy.remove(newNode); // 通过对象
cy.remove(collection); // 通过集合

// 选择
cy.nodes('[type="root"]');
cy.edges('[relType="FAMILY"]');
cy.nodes().filter(node => node.degree() > 5);

// 高亮关联(过滤不相干元素)
const highlightNeighbors = node => {
cy.elements().addClass('light-off'); // 全部变暗
node.removeClass('light-off'); // 目标节点保持明亮
node.neighborhood().removeClass('light-off'); // 邻域保持明亮
};

// 恢复
const resetHighlight = () => {
cy.elements().removeClass('light-off');
};

在 CSS 中配合:

1
2
3
4
.light-off {
opacity: 0.3;
transition: opacity 0.3s;
}

8.3 撤销/重做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 自建历史栈
const historyStack = [];
const redoStack = [];
const MAX_HISTORY = 50;

const saveSnapshot = () => {
historyStack.push(cy.json()); // 保存完整快照
if (historyStack.length > MAX_HISTORY) historyStack.shift();
redoStack.length = 0; // 清空重做栈
};

const undo = () => {
if (historyStack.length <= 1) return;
const current = historyStack.pop();
redoStack.push(current);
const prev = historyStack[historyStack.length - 1];
cy.json(prev); // 恢复快照
};

const redo = () => {
if (redoStack.length === 0) return;
const next = redoStack.pop();
historyStack.push(next);
cy.json(next);
};

// 监听变更自动保存
cy.on('add remove data position', () => {
saveSnapshot();
});

注意:cy.json() 会导出完整的元素和样式信息,对大规模图会有性能开销。可以优化为只保存 elements JSON。

8.4 导出图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 导出 PNG
const exportPNG = () => {
const blob = cy.png({
output: 'blob', // 'blob' | 'blob-promise' | 'blob-url' | 'base64' | 'base64uri'
bg: '#1a1a2e', // 背景色(暗色主题)
full: true, // 导出全部元素(超出视口的部分也包含)
scale: 2, // 2x 高清
maxWidth: 4096,
maxHeight: 4096,
});

// 下载
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `graph_export_${Date.now()}.png`;
a.click();
URL.revokeObjectURL(url);
};

// 也可以使用 html2canvas 获得更高清的输出
import html2canvas from 'html2canvas';
const exportWithHtml2Canvas = async () => {
const canvas = await html2canvas(cy.container(), { backgroundColor: '#1a1a2e' });
const link = document.createElement('a');
link.download = `图谱_${Date.now()}.png`;
link.href = canvas.toDataURL();
link.click();
};

8.5 搜索与定位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 搜索节点并居中
const searchAndFocus = keyword => {
const node = cy.nodes().filter(n => n.data('label').includes(keyword));

if (node.length > 0) {
cy.animate(
{
center: { eles: node },
zoom: 1.5,
},
{ duration: 500 },
);
node.style('border-color', '#ff4444');
setTimeout(() => node.style('border-color', ''), 2000);
}
};

九、动画系统

Cytoscape.js 提供两套动画 API:

  • cy.animate() — 对整个视口(zoom/pan)做动画
  • ele.animation() — 对单个元素(节点/边)做动画

9.1 视口动画(cy.animate)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 视口平滑过渡
cy.animate(
{
// 目标值
zoom: 1.5,
pan: { x: 200, y: 100 },

// 或者居中到元素
center: { eles: cy.nodes(':selected') },
fit: { eles: cy.elements(), padding: 30 },
},
{
duration: 500, // 动画时长 (ms)
easing: 'ease-in-out', // 缓动函数
queue: false, // 是否排队(false 则立即执行并中断当前动画)
complete: () => {
// 完成回调
console.log('动画完成');
},
},
);

// 停止所有视口动画
cy.stop(true); // true = 跳转到最终位置
cy.stop(false); // false = 保持当前位置

9.2 元素动画(ele.animation)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 对单个节点做位置动画
const anim = node.animation(
{
position: { x: 500, y: 300 },
style: {
'background-color': '#ff4444',
'border-width': 4,
},
},
{
duration: 300,
easing: 'ease-out',
},
);

anim
.play()
.promise('complete')
.then(() => {
// 动画完成后继续
node.style('background-color', '#4e56fd');
});

// 边动画
edge
.animation(
{
style: {
'line-color': '#ffd700',
width: 4,
},
},
{ duration: 500 },
)
.play();

9.3 缓动函数(Easing)

缓动函数曲线效果
'linear'匀速
'ease'标准缓入缓出
'ease-in'缓入
'ease-out'缓出
'ease-in-out'缓入缓出
'ease-in-sine'正弦缓入
'ease-out-sine'正弦缓出
'ease-in-out-sine'正弦缓入缓出
'ease-in-quad' / 'ease-out-quad' / 'ease-in-out-quad'二次缓动
'ease-in-cubic' / 'ease-out-cubic' / 'ease-in-out-cubic'三次缓动
'ease-in-back' / 'ease-out-back' / 'ease-in-out-back'回弹缓动
'ease-in-bounce' / 'ease-out-bounce' / 'ease-in-out-bounce'弹跳缓动

9.4 动画控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const anim = node.animation({ position: { x: 300, y: 200 } }, { duration: 1000 })

anim.play() // 播放
anim.pause() // 暂停
anim.stop() // 停止并清除
anim.apply() // 立即应用最后一帧
anim.promise('complete').then(() => { ... }) // 等待完成

anim.playing() // 是否正在播放
anim.progress() // 当前进度 (0-1)
anim.time() // 已过时间 (ms)

// 反向播放
anim.reverse().play()

9.5 布局动画

布局本身也支持 animate 选项,运行布局时会平滑过渡节点位置:

1
2
3
4
5
6
7
cy.layout({
name: 'dagre',
animate: true,
animationDuration: 500,
animationEasing: 'ease-in-out',
animateFilter: (node, i) => node.degree() > 0, // 只动画非孤立节点
}).run();

十、插件生态

10.1 常用插件

1
2
3
4
pnpm add cytoscape-edgehandles    # 边手动拖拽连线
pnpm add cytoscape-avsdf # AVSDF 圆形布局
pnpm add cytoscape-klay # Klay 分层布局
pnpm add cytoscape-dagre # dagre 布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import cytoscape from 'cytoscape';
import edgehandles from 'cytoscape-edgehandles';
import avsdf from 'cytoscape-avsdf';
import klay from 'cytoscape-klay';
import dagre from 'cytoscape-dagre';

cytoscape.use(edgehandles);
cytoscape.use(avsdf);
cytoscape.use(klay);
cytoscape.use(dagre);

// edgehandles 使用
const eh = cy.edgehandles({
snap: false, // 是否吸附到节点中心
handleNodes: 'node', // 可以连线的节点
hoverDelay: 100, // 悬停延迟
toggleOffOn: 'dblclick', // 双击切换模式
});

// 启用/禁用连线模式
eh.enable();
eh.disable();

10.2 其他推荐插件

插件功能安装
cytoscape-context-menus右键菜单(比手写更规范)npm i cytoscape-context-menus
cytoscape-cxtmenu圆形右键菜单npm i cytoscape-cxtmenu
cytoscape-navigator缩略图导航npm i cytoscape-navigator
cytoscape-colaCoLa 约束布局npm i cytoscape-cola
cytoscape-popperTooltip 定位npm i cytoscape-popper
cytoscape-fcosefCoSE 快速力导向npm i cytoscape-fcose
cytoscape-spread展开布局npm i cytoscape-spread
cytoscape-svgSVG 渲染器npm i cytoscape-svg

10.3 右键菜单插件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import cytoscape from 'cytoscape';
import contextMenus from 'cytoscape-context-menus';

cytoscape.use(contextMenus);

cy.contextMenus({
menuItems: [
{
id: 'expand',
content: '展开关联',
selector: 'node',
onClickFunction: evt => {
expandNode(evt.target);
},
},
{
id: 'remove',
content: '删除节点',
selector: 'node',
onClickFunction: evt => {
cy.remove(evt.target);
},
},
{
id: 'detail',
content: '查看详情',
selector: 'edge',
onClickFunction: evt => {
showEdgeDetail(evt.target);
},
},
],
});

十一、数据接口对接

Cytoscape.js 使用扁平 JSON 格式管理图数据,后端返回的数据通常需要转换才能渲染。

11.1 后端数据格式转换

常见的后端图数据需要通过转换函数转为 Cytoscape elements 格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 后端返回的数据格式示例
const apiResponse = {
nodes: [
{ id: 'n1', name: '节点A', type: 'root', properties: { weight: 10 } },
{ id: 'n2', name: '节点B', type: 'branch', properties: { weight: 5 } },
{ id: 'n3', name: '节点C', type: 'leaf', properties: { weight: 2 } },
],
relationships: [
{ id: 'r1', source: 'n1', target: 'n2', type: 'RELATED', properties: { count: 5 } },
{ id: 'r2', source: 'n1', target: 'n3', type: 'CONTAINS', properties: { count: 3 } },
],
};

// 转换为 Cytoscape elements 格式
const toCyElements = data => ({
nodes: (data.nodes || []).map(n => ({
data: {
id: n.id,
label: n.name || n.label,
type: n.type,
...n.properties,
},
})),
edges: (data.relationships || data.edges || []).map(r => ({
data: {
id: r.id,
source: r.source,
target: r.target,
label: r.type || r.label,
relType: r.type,
...r.properties,
},
})),
});

const elements = toCyElements(apiResponse);
cy.add(elements);
cy.layout({ name: 'dagre', animate: true }).run();

11.2 节点增量展开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 展开某个节点的关联数据
const expandNode = async (node, depth = 1) => {
const nodeId = node.id();

// 调用后端接口获取关联数据
const resp = await apiExpandNode({ nodeId, depth });

// 转换并添加到画布
const newElements = toCyElements(resp.data);
cy.add(newElements);
cy.layout({ name: 'dagre', animate: true }).run();

// 标记已展开,避免重复查询
node.data('expanded', true);
};

11.3 数据清洗

推荐在添加到画布前对数据进行清洗,避免脏数据影响渲染效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const cleanData = (rawData) => {
let data = { ...rawData };

// 1. 去重
data.nodes = dedupeBy(data.nodes, 'id');

// 2. 过滤自环边
data.edges = data.edges.filter(e => e.source !== e.target);

// 3. 过滤零度节点(无任何关系的孤立节点)
const connectedIds = new Set();
data.edges.forEach(e => {
connectedIds.add(e.source);
connectedIds.add(e.target);
});
data.nodes = data.nodes.filter(n => connectedIds.has(n.id));

return data;
};

十二、性能优化

Cytoscape.js 通过 Canvas 渲染已经相当高效,但对于大规模图(1000+ 节点),官方提供了以下性能优化选项。

12.1 渲染性能初始化选项

这些选项在 cytoscape() 初始化时配置,直接影响渲染帧率和交互流畅度:

选项类型默认值说明
hideEdgesOnViewportbooleanfalse推荐开启。交互时(pan/zoom/动画)自动隐藏边,大幅提升帧率。释放后恢复显示
textureOnViewportbooleanfalse交互时使用纹理缓存渲染节点(降低精度换取性能)
motionBlurbooleanfalse开启运动模糊(让低帧率动画看起来更平滑)
pixelRationumber / 'auto''auto'渲染像素比。大图可设为 1(牺牲清晰度换性能)
wheelSensitivitynumber1滚轮缩放灵敏度,减小可降低重绘次数
headlessbooleanfalse无头模式。无 DOM 渲染,仅用于纯计算(服务端/单元测试)
styleEnabledbooleantrue是否启用样式解析。在纯计算场景设为 false 可跳过样式计算
1
2
3
4
5
6
7
8
9
10
// 高性能配置示例(适用 2000+ 节点场景)
const cy = cytoscape({
container: document.getElementById('cy'),
hideEdgesOnViewport: true, // 交互时隐藏边
textureOnViewport: true, // 纹理缓存
motionBlur: true, // 运动模糊
pixelRatio: 1, // 1 倍渲染(牺牲清晰度)
wheelSensitivity: 0.2, // 降低滚轮灵敏度
// ... 其余配置
});

12.2 边曲线优化

边的曲线样式对性能影响很大,按性能排序:

曲线样式性能视觉效果建议
haystack⭐⭐⭐ 最快直线(忽略节点位置)适合大规模图(5000+ 节点)
straight⭐⭐ 较快可弯曲直线中等规模
bezier⭐ 正常标准贝塞尔曲线中小规模(< 500 节点)
unbundled-bezier💤 最慢平行边自动分离仅复杂关系图
segments⭐ 正常自定义折线按需使用
taxi⭐ 正常正交折线按需使用
1
2
3
4
5
6
7
8
9
// 大规模图下的边样式
const largeGraphEdgeStyle = {
selector: 'edge',
style: {
'curve-style': 'haystack', // 最快
'haystack-radius': 0.5,
opacity: 0.5, // 半透明减少视觉负担
},
};

12.3 元素层级与事件优化

优化项说明
z-index给重要节点更高的 z-index,减少事件命中计算量
events: 'no'对纯装饰性边/节点禁用事件
display: 'none'隐藏视口外元素(不参与布局和渲染)
text-wrap: 'ellipsis'限制标签长度,减少文本渲染开销
min-zoomed-font-size设置最小可视化字体,缩小后自动不渲染小文字

12.4 Node.js 无头模式

用于纯图算法计算或服务端布局预计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const cytoscape = require('cytoscape');

const cy = cytoscape({
headless: true, // 无头模式
styleEnabled: false, // 跳过样式计算
});

cy.add([{ data: { id: 'a' } }, { data: { id: 'b' } }, { data: { id: 'e1', source: 'a', target: 'b' } }]);

// 执行布局(纯计算,无渲染)
cy.layout({ name: 'cose' }).run();

// 导出计算位置
const positions = cy.nodes().map(n => ({ id: n.id(), pos: n.position() }));
fs.writeFileSync('layout.json', JSON.stringify(positions));

12.5 实用性能检查清单

场景建议配置
节点数 < 200默认配置即可
200 - 1000 节点curve-style: 'bezier',开启 hideEdgesOnViewport
1000 - 5000 节点curve-style: 'haystack'pixelRatio: 1textureOnViewport: true
5000+ 节点curve-style: 'haystack'pixelRatio: 1hideEdgesOnViewportmotionBlur,禁用 label
服务端计算headless: true, styleEnabled: false

十三、Vue 3 集成实战

Cytoscape.js 在 Vue 3 中的集成模式比较固定,核心是管理好 cytoscape 实例的生命周期:mounted 时创建,unmounted 时销毁。

13.1 基础组件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!-- GraphCanvas.vue -->
<template>
<div ref="cyContainer" class="cy-container"></div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
import cytoscape from 'cytoscape';

const cyContainer = ref(null);
let cy = null;

onMounted(async () => {
await nextTick();
cy = cytoscape({
container: cyContainer.value,
elements: [],
style: [],
layout: { name: 'dagre' },
});
});

onBeforeUnmount(() => {
cy?.destroy();
cy = null;
});
</script>

<style scoped>
.cy-container {
width: 100%;
height: 600px;
}
</style>

13.2 加载数据

1
2
3
4
5
6
7
8
const loadGraphData = async () => {
const resp = await fetchGraphData();
const elements = toCyElements(resp.data);
cy.batch(() => {
cy.add(elements);
});
cy.layout({ name: 'dagre', animate: true }).run();
};

13.3 布局切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const layoutConfigs = {
dagre: { name: 'dagre', rankDir: 'TB', nodeSep: 60, rankSep: 100, animate: true },
cose: { name: 'cose', idealEdgeLength: 100, numIter: 1000, animate: true },
circle: { name: 'circle', fit: true, avoidOverlap: true, animate: true },
grid: { name: 'grid', avoidOverlap: true, animate: true },
concentric: { name: 'concentric', concentric: n => n.degree(), animate: true },
breadthfirst: { name: 'breadthfirst', directed: true, animate: true },
};

const switchLayout = (mode) => {
if (!cy) return;
const config = layoutConfigs[mode];
if (config) cy.layout(config).run();
};

13.4 交互事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 节点点击 → 显示详情
cy.on('tap', 'node', evt => {
const node = evt.target;
selectedNodeData.value = node.data();
nodeDetailVisible.value = true;
});

// 边点击 → 显示关系详情
cy.on('tap', 'edge', evt => {
const edge = evt.target;
selectedEdgeData.value = {
...edge.data(),
sourceLabel: edge.source().data('label'),
targetLabel: edge.target().data('label'),
};
edgeDetailVisible.value = true;
});

// 右键节点 → 弹出菜单
cy.on('cxttap', 'node', evt => {
contextMenu.value = {
show: true,
x: evt.renderedPosition.x,
y: evt.renderedPosition.y,
items: [
{ label: '展开关联节点', action: () => handleExpand(evt.target) },
{ label: '高亮关联', action: () => highlightNeighbors(evt.target) },
{ label: '删除节点', action: () => cy.remove(evt.target) },
],
};
});

// 点击空白关闭菜单
cy.on('tap', evt => {
if (evt.target === cy) contextMenu.value.show = false;
});

// 拖拽时邻域跟随
cy.nodes().on('drag', evt => {
const draggedNode = evt.target;
const delta = { x: evt.originalEvent.movementX, y: evt.originalEvent.movementY };
draggedNode.neighborhood().nodes().forEach(neighbor => {
const pos = neighbor.position();
neighbor.position({
x: pos.x + delta.x * 0.3,
y: pos.y + delta.y * 0.3,
});
});
});

13.5 工具函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// dataTransform.js —— 数据格式转换
export const toCyElements = data => ({
nodes: (data.nodes || []).map(n => ({
data: {
id: n.id,
label: n.name || n.label,
type: n.type,
...n.properties,
},
})),
edges: (data.edges || data.relationships || []).map(r => ({
data: {
id: r.id,
source: r.source,
target: r.target,
label: r.type || r.label,
relType: r.type,
...r.properties,
},
})),
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// graphConfig.js —— 默认样式配置
export const defaultStyle = [
{
selector: 'node',
style: {
width: 50,
height: 50,
'background-color': '#4e56fd',
'border-width': 2,
'border-color': '#fff',
label: 'data(label)',
color: '#333',
'font-size': 11,
'text-valign': 'bottom',
'text-halign': 'center',
'text-margin-y': 6,
'text-wrap': 'ellipsis',
'text-max-width': '80px',
},
},
{
selector: 'node:selected',
style: {
'border-width': 3,
'border-color': 'rgb(78, 162, 240)',
},
},
{
selector: 'edge',
style: {
width: 2,
'line-color': '#999',
'target-arrow-color': '#999',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
label: 'data(label)',
color: '#666',
'font-size': 9,
'text-background-color': '#fff',
'text-background-opacity': 0.8,
'text-background-padding': '3px',
'text-background-shape': 'roundrectangle',
},
},
];

export const darkThemeStyle = [
{
selector: 'node',
style: {
width: 50,
height: 50,
'background-color': '#4e56fd',
'border-width': 2,
'border-color': '#333',
label: 'data(label)',
color: '#e0e0e0',
'font-size': 11,
'text-valign': 'bottom',
'text-halign': 'center',
'text-margin-y': 6,
'text-wrap': 'ellipsis',
'text-max-width': '80px',
},
},
{
selector: 'node:selected',
style: {
'border-width': 3,
'border-color': 'rgb(78, 162, 240)',
},
},
{
selector: 'edge',
style: {
width: 2,
'line-color': '#555',
'target-arrow-color': '#555',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
label: 'data(label)',
color: '#aaa',
'font-size': 9,
'text-background-color': '#222',
'text-background-opacity': 0.8,
'text-background-padding': '3px',
'text-background-shape': 'roundrectangle',
},
},
];

十四、AntV X6 vs Cytoscape.js 对比

维度AntV X6Cytoscape.js
定位图编辑器(Diagram Editor)图可视化分析(Graph Visualization)
渲染方式SVGCanvas
性能(大图)500+ 节点开始卡顿2000+ 节点仍流畅
内置布局依赖 @antv/layout,需额外安装10+ 内置布局,开箱即用
图算法最短路径、度数、连通分量等原生支持
自定义节点React/Vue 组件注册(强大但重)Canvas 绘制(高性能)
编辑功能连线、对齐、吸附(原生)需插件(edgehandles)
撤销重做History 插件需自建或插件
移动端一般原生触摸手势
包体积~800KB(+ layout)~300KB(完整版 ~500KB)
学习曲线较陡(概念多)较低(风格扁平)

选型建议

需求推荐
图编辑器、流程图、ER 图AntV X6
社交网络图、知识图谱、关系分析Cytoscape.js
支持自定义 Vue 组件作节点AntV X6
大规模节点(1000+)高性能渲染Cytoscape.js
精细的编辑操作(拽线、吸附)AntV X6

十五、从 AntV X6 迁移到 Cytoscape.js

15.1 概念映射

X6 概念Cytoscape.js 对应
new Graph({ container })cytoscape({ container })
graph.addNode({ id, x, y })cy.add({ data: { id }, position: { x, y } })
graph.addEdge({ source, target })cy.add({ data: { source, target } })
node.setPosition(x, y)node.position({ x, y })
node.attr({ ... })node.style('prop', value)
node.getData() / node.setData()node.data() / node.data('key', val)
graph.getNodes()cy.nodes()
graph.getEdges()cy.edges()
graph.removeCell(node)cy.remove(node)
graph.centerContent()cy.fit()
graph.zoom(1.5)cy.zoom(1.5)
X6 自定义 Cell样式选择器 node[type="root"]
X6 Port无原生 Port,通过节点 click + 边创建模拟
graph.freeze() / unfreeze()不需要(Canvas 批量渲染天然高效)

15.2 迁移步骤

第一步:替换依赖

1
2
pnpm remove @antv/x6 @antv/layout
pnpm add cytoscape cytoscape-dagre cytoscape-avsdf cytoscape-klay

第二步:替换初始化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Before (X6)
import { Graph } from '@antv/x6';
const graph = new Graph({
container: containerRef.value,
width: 1200,
height: 800,
grid: true,
connecting: { router: 'manhattan' },
});

// After (Cytoscape.js)
import cytoscape from 'cytoscape';
const cy = cytoscape({
container: containerRef.value,
minZoom: 0.1,
maxZoom: 10,
wheelSensitivity: 0.2,
});

第三步:替换数据模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Before (X6)
const model = {
nodes: nodes.map(node => ({
id: node.id,
x: pos.x,
y: pos.y,
width: NODE_WIDTH,
height: NODE_HEIGHT,
})),
edges: edges.map(edge => ({
source: edge.getSourceCellId(),
target: edge.getTargetCellId(),
})),
};

// After (Cytoscape) - 不需要 width/height,由样式控制
const elements = {
nodes: nodes.map(node => ({
data: { id: node.id, label: node.label },
position: { x: node.x, y: node.y },
})),
edges: edges.map(edge => ({
data: { source: edge.source, target: edge.target },
})),
};

第四步:替换自定义节点

1
2
3
4
5
6
7
8
9
10
11
12
// Before (X6) - 注册 Vue 组件作为节点
Graph.registerNode('relation-node', { ... }, true)

// After (Cytoscape) - 用 CSS 选择器 + 背景图片
{
selector: 'node[type="root"]',
style: {
'shape': 'roundrectangle',
'background-image': 'url(/images/node-icon.png)',
'background-fit': 'cover',
}
}

第五步:替换布局切换

1
2
3
4
5
6
7
8
9
// Before (X6) - 依赖 @antv/layout
const layout = new ForceLayout({ center, preventOverlap: true, ... })
await layout.execute(model)
layout.forEachNode(node => {
cell.setPosition(node.x, node.y)
})

// After (Cytoscape) - 一行搞定
cy.layout({ name: 'cose', animate: true }).run()

15.3 关键差异备忘

  1. 没有 freeze/unfreeze:Cytoscape 的 Canvas 渲染天然支持批量操作,不需要手动控制
  2. 没有 Port 概念:边的关系由 source/target ID 直接确定,不依赖 Port
  3. 样式是 CSS-like 声明式:主题切换可以直接替换 style 数组
  4. 布局不需要手动读取坐标layout.run() 自动更新节点位置
  5. 数据是扁平的 JSON:没有 Model 类,纯数据驱动

十六、常见问题与排查

Q1: 布局切换后节点位置不更新?

A: layout.run() 是异步的,需要监听 layoutstop 事件或确保 animate: true 的动画已完成。不要手动调用 layout.forEachNode()

官方推荐使用 layout.promiseOn('layoutstop') 替代事件监听:

1
2
3
4
5
const layout = cy.layout({ name: 'dagre' });
layout.promiseOn('layoutstop').then(() => {
cy.fit(undefined, 30); // 布局完成后适应画布
});
layout.run();

Q2: 节点图片不显示?

A: 检查:

  1. background-image 的值是否正确(需是 url(path) 格式,或直接 URL 字符串)
  2. 图片路径是否正确(相对于 HTML 页面的路径)
  3. CORS 问题(跨域图片需要设置 crossorigin

官方支持多种背景图格式:

1
2
3
4
5
6
7
// URL 字符串
'background-image': './images/node.png'
// 函数返回
'background-image': (ele) => `./images/${ele.data('type')}.png`
// 多个图片(分层)
'background-image': ['url(img1.png)', 'url(img2.png)']
'background-image-containment': 'over'

Q3: 大图(1000+ 节点)渲染卡顿?

A: 参考 十二、性能优化 章节:

  • 启用 hideEdgesOnViewport: true — 交互时自动隐藏边
  • 使用 curve-style: 'haystack' 替代 'bezier' — 直线比曲线快 3-5 倍
  • 设置 pixelRatio: 1 — 牺牲清晰度换性能
  • 启用 textureOnViewport: true — 纹理缓存
  • 使用 motionBlur: true — 视觉掩盖掉帧
  • 考虑 text-wrap: 'ellipsis' 限制标签长度

Q4: Cytoscape 和 X6 能共存吗?

A: 技术上可以(不同 DOM 容器),但不推荐

  • 图谱库体积叠加,打包体积 ~1MB+
  • API 风格差异大,维护成本高
  • 建议统一为一个库

Q5: 如何自定义节点形状?

A: Cytoscape 支持多种内置形状,自定义形状需用 Canvas API:

1
2
3
4
5
6
7
8
9
// 内置形状映射
cy.style()
.selector('node')
.style('shape', ele => {
if (ele.data('type') === 'root') return 'ellipse';
if (ele.data('type') === 'branch') return 'roundrectangle';
return 'rectangle';
})
.update();

如需完全自定义节点,参考官方文档的 Custom Node Shapes 部分。

Q6: 多个实例时如何管理?

A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 推荐:用一个 Map 管理多实例
const instanceMap = new Map();

const createInstance = (id, container, options) => {
const cy = cytoscape({ container, ...options });
instanceMap.set(id, cy);
return cy;
};

const destroyInstance = id => {
const cy = instanceMap.get(id);
if (cy) {
cy.destroy();
instanceMap.delete(id);
}
};

// 路由切换时清理
onBeforeRouteLeave(() => {
instanceMap.forEach(cy => cy.destroy());
instanceMap.clear();
});

Q7: cy.resize() 什么时候需要调用?

A: 当容器尺寸变化时(如侧边栏展开/收起、窗口 resize),需要手动调用:

1
2
3
4
5
6
// 监听容器尺寸变化
const resizeObserver = new ResizeObserver(() => cy.resize());
resizeObserver.observe(cyContainer.value);

// 或在窗口 resize 时调用
window.addEventListener('resize', () => cy.resize());

Q8: 如何正确销毁 Cytoscape 实例?

A:

1
2
3
4
5
6
7
8
9
// 完整销毁流程
cy.destroy(); // 移除 DOM、事件监听、定时器
cy = null; // 释放引用

// Vue 中
onBeforeUnmount(() => {
cy?.destroy();
cy = null;
});

cy.destroy() 会做以下清理:

  • 移除所有绑定的事件监听
  • 取消所有动画和布局
  • 从 DOM 中移除 Canvas 元素
  • 释放内部图数据

参考资源