画关系图这事,我踩过不少坑,得先说结论:pyecharts 的 Graph 组件,是 Python 生态里把关系数据画成关系图最顺手的选择之一。你不用碰前端,也不用纠结 ECharts 的复杂配置项,只要把节点和边整理成 dict 列表丢进去,就能拿到一个支持拖拽、缩放、点击高亮的 HTML 交互图。
这篇东西适合谁看?一类是做系统分析、数据治理的同学,经常要展示模块依赖、调用链路;另一类是做用户画像、社交网络分析的,需要把人物或实体之间的关系可视化;还有一类就是单纯觉得 Excel 表格太干,想给领导汇报时拿出一张能转动、能点选的关系网。不管你属于哪种,只要你手头的数据是“谁和谁有关系”这种二元结构,Graph 就是你的首选武器。
1. 整体设计与思路拆解
1.1 什么时候轮得到关系图出场
很多人一看到“可视化”就想着画柱状图、折线图,但关系数据和普通数值型数据完全是两码事。你有一份学员报名表,字段是“姓名、班级、成绩”,这种适合柱状图;但如果你有一份“谁引荐了谁加入社群”的名单,这种数据天然就是一张网,画成表格就废了。
我一般会先问自己三个问题:数据里有没有明确的实体?实体之间有没有二元关系?关系的密度大不大?三个都满足,就果断上 Graph。比如系统模块依赖、知识图谱里的概念关联、组织架构里的汇报关系、交易风控里的资金流向,这些场景用 Graph 表达,一眼就能看出谁处于核心位置、哪些节点是枢纽、哪些关系是孤立的。
1.2 为什么是 pyecharts 而不是其他方案
做 Python 可视化,你绕不开几个选择:networkx 配合 matplotlib、原生 ECharts 写 JS、pyecharts 封装的 Graph。
networkx 不是不好,但它偏重图论计算,比如最短路径、中心性分析,输出到 matplotlib 以后基本是静态图,节点一多就糊成一团,也不方便给业务方做交互。原生 ECharts 的 graph 确实强大,但你要在 Python 和 JS 之间来回倒数据,写一整套前端模板,对纯 Python 团队来说维护成本太高。
pyecharts 的 Graph 走了中间路线:底层还是 ECharts,渲染效果和交互能力不打折,但调用方式完全 Python 化。你把数据整理成 nodes 和 links 两个列表,传进去就完事。尤其在 Jupyter Notebook 里可以直接渲染,在自动化报表场景里又能输出独立的 HTML 文件,灵活度很高。
1.3 关系图的核心数据模型
Graph 的数据模型并不复杂,核心就三个概念:节点、边、分类。
打个比方,你要画一张“团队通讯录关系网”。nodes 就是通讯录里的每个人,每个人的姓名必须唯一,这决定了图上会不会出现两个同名但不同的点。links 就是通话记录里的一条条关系,每条关系必须包含“发起方”和“接收方”,名字要和 nodes 里的完全一致。categories 则是你给这些人贴的标签,比如“产品组”“研发组”“测试组”,每组可以有自己的颜色,图例也靠它来生成。
理解了这三个概念,后面所有代码都在这上面做文章。颜色、大小、标签、位置、粗细,全是围绕节点和边的属性去微调。
1.4 从数据到成图的完整流程
我的实测经验是,画 Graph 之前先别急着写代码,把流程在脑子里过一遍,能少走很多弯路。完整链路大概是:
- 数据清洗:去掉空值、重复关系,统一实体命名。
- 名称归一:同一个实体在不同表里可能叫“A同学”和“A同同学”,要合并成同一个 name。
- 分组设计:想清楚图上要分几类,大类还是细类,这直接影响图例的复杂程度。
- 布局选择:关系密集用力导向,层次清晰用环形,确定好方向再调参。
- 样式调试:节点大小、边粗细、标签位置、颜色深浅。
- 交互检查:拖拽是否顺滑、缩放是否卡顿、点击节点后高亮是否明显。
这套流程里,最花时间的是“名称归一”,最容易被忽略的是“布局选择”。很多图画出来乱成一锅粥,不是数据错了,而是布局没选对。
2. 核心概念解析与数据准备
2.1 环境和安装
先解决环境问题。pyecharts 目前主流版本是 2.x,底层依赖 ECharts 5.x,渲染效果比 1.x 时代好了不少。安装很简单:
pip install pyecharts装完以后不需要额外配 ECharts 环境,pyecharts 会在渲染 HTML 时自动把依赖打进去。唯一要注意的是,如果你在 Jupyter Notebook 里用,建议顺手装一下:
pip install jupyter-echarts-pypkg或者直接升级到最新版,因为新版 pyecharts 已经内置了 notebook 渲染支持。如果你用的是公司内网环境,装完记得检查版本,低于 2.0 的话接口会有差异,后面我会专门讲这个坑。
2.2 节点数据结构,每个字段都说清楚
nodes 是一个列表,列表里每个元素是一个 dict。我至今见过最典型的错误写法是只给 name 字段,画出来全是默认小黑点,毫无信息量。一个合格的 node 字典长这样:
{ "name": "数据采集", # 必填,唯一标识 "category": 0, # 选填,分类索引,对应 categories 里第几组 "symbolSize": 50, # 选填,节点直径,数值越大节点越大 "value": 10, # 选填,数值,可用于 tooltip 展示 "x": 100, "y": 200, # 选填,仅在 layout="none" 时建议手动指定 "itemStyle": {"color": "#5470c6"}, # 选填,单独覆盖节点颜色 "label": {"show": True} # 选填,单独控制该节点的标签 }这里有几个经验点。首先是 name 必须全局唯一,否则 ECharts 会认为这是同一个节点,导致链接混乱。其次是 symbolSize,我建议设置一个基础值,比如 40 到 60,太小了看不清,太大了节点互相遮挡。第三,如果你打算用 force 力导向布局,x 和 y 可以完全不用给,让算法自动布局;但如果你用 layout="none",x 和 y 就必须精确指定,否则所有节点都会堆到左上角。
2.3 链接数据结构,别在 name 上犯低级错误
links 是关系图的核心灵魂,结构比 nodes 简单:
{ "source": "数据采集", # 起点节点的 name "target": "数据清洗", # 终点节点的 name "value": 3, # 选填,关系的权重,可用于提示信息 }我踩过最大的坑就是 source 和 target 里的字符串,跟 nodes 里对应 name 必须严格一致,连空格都不能差一个。你从 Excel 读进来的时候,某行数据末尾可能带着看不见的空格,结果那条边死活不显示。排查半天才发现是字符串匹配问题。
还有一个文档里不会强调的细节:links 里的 value 字段并不会自动变成线条粗细。很多教程不说明这点,导致你传了 value 却发现所有线条一样粗。想要线条粗细反映权重,需要自己实现一个映射函数,或者用 linestyle_opts 统一粗度,后面实操部分我会给示例。
2.4 categories 分组,颜色和图例都靠它
categories 的官方定义是给节点做分类,但它在实际应用中承担了两个职责:一是给节点上色,二是生成图例。节点的 category 字段传的是索引值,比如 categories 列表里的第 0 组,节点就归到第 0 组。
categories = [ {"name": "数据层", "itemStyle": {"color": "#5470c6"}}, {"name": "算法层", "itemStyle": {"color": "#91cc75"}}, {"name": "服务层", "itemStyle": {"color": "#fac858"}}, ]一个常见误解是:分类越多越好。真实场景里,分类超过 5 个,图例就会变得臃肿,颜色也难选。如果关系图里的实体没有明显的分组属性,宁可不要 categories,让所有节点用统一颜色,重点靠标签和大小传递信息。
2.5 布局模式,先选对再调参
pyecharts Graph 支持好几种布局,我实际用下来最常碰的是这三个:
| 布局参数 | 效果特征 | 适用场景 | 关键参数 |
|---|---|---|---|
| force | 力导向布局,节点互相排斥又受到边牵引,最终达到动态平衡 | 社交网络、复杂依赖关系、无层级结构的数据 | repulsion(斥力)、edge_length(边长)、gravity(重力) |
| circular | 所有节点围成一圈,适合展示环形关系 | 环路检测、对称结构、节点数量较少的场景 | 无特殊参数,只需要调整节点大小 |
| none | 完全手动定位,节点坐标不由算法决定 | 精确控制位置的架构图、拓扑图 | x、y 必须手动给出 |
radial 辐射布局在新版 ECharts 里也支持,效果是从中心向外扩散,适合一个人连接多人的星型结构。pyecharts 2.x 部分新版本已经透传了这个参数,但老版本会直接报错。如果你的版本不支持,可以升个级或者退而求其次用 force 加高斥力模拟。
3. 实操过程与关键环节实现
3.1 先跑通一个最小示例
我习惯先写一个最小的 Graph,把链路跑通,再逐步加需求。这是我最常用的一套最小代码:
from pyecharts import options as opts from pyecharts.charts import Graph nodes = [ {"name": "节点A", "symbolSize": 50}, {"name": "节点B", "symbolSize": 40}, {"name": "节点C", "symbolSize": 40}, ] links = [ {"source": "节点A", "target": "节点B"}, {"source": "节点A", "target": "节点C"}, ] graph = ( Graph(init_opts=opts.InitOpts(width="900px", height="600px")) .add( series_name="关系图", nodes=nodes, links=links, layout="force", repulsion=2000, edge_length=120, ) .set_global_opts(title_opts=opts.TitleOpts(title="最小关系图示例")) ) graph.render("minimal_graph.html")跑完这段代码,浏览器打开 HTML 文件,你会看到三个节点和两条边,节点可以拖拽,滚轮可以缩放。这就是 Graph 的基础形态。如果这一步都渲染不出来,先检查 pyecharts 版本,再检查 HTML 文件是不是被浏览器安全策略拦截了。
3.2 从 Pandas 表结构生成 nodes 和 links
现实项目里,数据几乎不会直接以 nodes 和 links 的格式摆在你面前,通常是一张两列表:一列是发起方,一列是接收方。我们需要写个转换函数。
import pandas as pd df = pd.DataFrame({ "source": ["数据采集", "数据采集", "数据清洗", "特征工程", "模型训练"], "target": ["数据清洗", "特征工程", "特征工程", "模型训练", "模型评估"], }) # 收集所有唯一实体 all_names = pd.unique(df[["source", "target"]].values.ravel("K")) name_to_id = {name: idx for idx, name in enumerate(all_names)} nodes = [{"name": name, "symbolSize": 50, "category": 0} for name in all_names] links = [{"source": row["source"], "target": row["target"]} for _, row in df.iterrows()]这个转换逻辑几乎没有坑点,唯一要注意的是 all_names 的去重顺序会影响 category 索引,如果你有分组需求,记得先打好分组标签再生成 nodes。
3.3 让节点大小和颜色表达更多信息
画图不只是为了好看,节点大小、颜色、标签都是信息的载体。我做过一个系统模块依赖图,把节点大小映射为模块代码行数,颜色映射为模块归属团队,效果比单纯看表格直观得多。
base_nodes = [] for idx, row in module_df.iterrows(): base_nodes.append({ "name": row["module_name"], "symbolSize": 30 + row["code_lines"] / 100, # 代码越多节点越大 "category": team_to_category.get(row["team"], 0), "value": row["code_lines"], "tooltip": {"formatter": "{b}: {c}"}, # 鼠标悬停显示行数 })关键点是,symbolSize 建议做线性映射,避免出现超大节点。如果你要按 value 设置颜色,可以在 itemStyle 里直接给 color 值,或者用 visual_map 组件做连续映射,不过我实测下来 visual_map 对 Graph 的边支持不太稳定,建议主要映射节点。
3.4 边的样式与交互,别让线条呆板
边的样式直接影响整张图的质感。默认的细黑线在节点多的时候非常难看,我一般会这样设置:
.add( ..., edge_symbol=["circle", "arrow"], # 起点是圆点,终点是箭头 edge_symbol_size=8, linestyle_opts=opts.LineStyleOpts( color="#999", width=2, curve=0.2, # 轻微弯曲,适合有向关系 type_="solid", # 或 "dashed" ), is_focus_node_adjacency=True, # 点击节点后只高亮相邻节点和边 is_roam=True, # 允许缩放和平移 )is_focus_node_adjacency 这个参数强烈推荐打开。节点一多,整张图密密麻麻看不清,开启后点击某个节点,其他无关的节点会变灰,焦点节点的邻居高亮,交互体验一下子提升一个档次。is_roam 也保持默认开启,不然图一放大就找不到北了。
3.5 一个完整的实战示例:模块依赖关系图
最后给一个可以直接抄的完整代码。场景是一个模拟数据平台的模块依赖分析,节点分数据层、算法层、服务层三类,边用有向箭头,开启焦点高亮。
from pyecharts import options as opts from pyecharts.charts import Graph nodes = [ {"name": "数据采集", "category": 0, "symbolSize": 60, "value": 10}, {"name": "数据清洗", "category": 0, "symbolSize": 50, "value": 8}, {"name": "特征工程", "category": 0, "symbolSize": 50, "value": 8}, {"name": "模型训练", "category": 1, "symbolSize": 70, "value": 12}, {"name": "模型评估", "category": 1, "symbolSize": 40, "value": 6}, {"name": "服务部署", "category": 2, "symbolSize": 55, "value": 9}, {"name": "监控告警", "category": 2, "symbolSize": 35, "value": 5}, ] links = [ {"source": "数据采集", "target": "数据清洗", "value": 3}, {"source": "数据清洗", "target": "特征工程", "value": 2}, {"source": "特征工程", "target": "模型训练", "value": 4}, {"source": "模型训练", "target": "模型评估", "value": 2}, {"source": "模型评估", "target": "服务部署", "value": 2}, {"source": "服务部署", "target": "监控告警", "value": 1}, ] categories = [ {"name": "数据层", "itemStyle": {"color": "#5470c6"}}, {"name": "算法层", "itemStyle": {"color": "#91cc75"}}, {"name": "服务层", "itemStyle": {"color": "#fac858"}}, ] graph = ( Graph(init_opts=opts.InitOpts(width="1200px", height="800px")) .add( series_name="模块依赖", nodes=nodes, links=links, categories=categories, layout="force", repulsion=3000, edge_length=150, edge_symbol=["circle", "arrow"], edge_symbol_size=8, linestyle_opts=opts.LineStyleOpts(color="#999", width=1.5, curve=0.2), label_opts=opts.LabelOpts(is_show=True, position="right"), is_focus_node_adjacency=True, is_roam=True, ) .set_global_opts( title_opts=opts.TitleOpts(title="某数据平台模块依赖关系"), legend_opts=opts.LegendOpts(orient="vertical", pos_left="2%", pos_top="15%"), tooltip_opts=opts.TooltipOpts(formatter="{b}: {c}"), ) ) graph.render("module_graph.html")这段代码跑出来后,你会得到一张带图例、带箭头、可交互的关系图。想换成环形布局,只需要把 layout 改成 "circular",其他参数基本不用动。
3.6 布局切换和数据量控制
我在不同项目里反复折腾过布局,给你一个选型口诀:关系层级清晰、以树状为主,试试 circular;关系交叉密集、没有明显层级,用 force;节点数量超过 150 个,优先考虑 force 并调大斥力、缩小节点。因为 circular 布局在节点多的时候会变成一个大圈,肉眼根本分不清谁是谁。
force 布局有几个参数需要联动调:repulsion 控制节点之间的排斥力,数值太小节点会叠在一起,太大图会被推开;edge_length 控制边的目标长度,它和 repulsion 一起决定最终布局的松紧程度。我一般从 repulsion=2000、edge_length=120 起步,再根据渲染效果微调。如果节点数量很大,建议把动画关掉,或者直接用 layout="none" 预计算好坐标,避免页面卡死。
4. 常见问题与排查技巧
4.1 节点重叠、连线乱飞
这是 force 布局最常见的翻车现场。现象是节点全部堆在中间或者不停抖动,通常是 repulsion 太小、gravity 太大、数据本身有大量孤立节点。解决方法很简单:先调大 repulsion,调到 3000 以上再观察;然后适当增加 edge_length,给边一个目标长度;最后如果仍有孤立节点,考虑单独处理,把孤立节点集中到图的下半部分。
这里插一句,force 布局节点越多越需要耐心,它本质上是一个迭代收敛的过程。如果你的数据有上千个节点,光靠调参很难得到完美布局,更好的做法是先用 networkx 算出一个初始布局,把坐标传给 Graph 并用 layout="none"。
4.2 标签挤成一团,完全没法看
节点多的时候,标签全是字,叠在一起比乱线还乱。我的处理方案是分三层:第一层,超过 80 个节点就把标签默认关闭,只保留关键节点的标签;第二层,给标签设置 position,比如 "right" 或 "top",避免标签压住节点;第三层,使用 formatter 只展示关键信息,不要显示所有字段。
label_opts=opts.LabelOpts( is_show=True, position="right", formatter="{b}", # 只显示节点名 )有人会想用 reduce 或缩放解决,实测意义不大。交互场景下,让用户通过 tooltip 查看完整信息,反而比堆标签更优雅。
4.3 图例不显示或位置不对
图例来自 categories,如果你的 categories 传了空列表,图例会消失;如果图例出现了,但名字全是“关系图”这种系列名,说明节点没有正确关联到分类。排查方式很简单:打印 nodes 里每个节点的 category 字段,确认它是整数索引,且索引在 categories 列表范围内。我踩过一次负索引的坑,某个节点的 category 是 -1,导致它既不在任何分类里,颜色也变成了默认色。
图例位置用 legend_opts 控制,我建议放在右下角或左侧垂直排列,避免挡住关系图的中心区域。因为 Graph 的节点布局是动态的,图例会遮挡内容,这种体验问题很少有人提。
4.4 边不显示,或只有箭头没有线
边消失的原因 90% 是 name 匹配不一致。比如 nodes 里是“数据清洗”,links 里却是“数据清洗 ”(带了个空格),这条边直接失效。还有一个情况是两个节点之间的边重复,或者方向反了,导致视觉上“少了”关系。
建议在构造数据后先做一层校验:
name_set = {node["name"] for node in nodes} valid_links = [ link for link in links if link["source"] in name_set and link["target"] in name_set ]如果校验之后 links 数量明显减少,说明原始数据里有大量指向未知实体的关系,这就是名称清洗没做到位。
4.5 大数据量性能问题,图表打开就卡死
Graph 性能瓶颈主要在 force 布局的实时计算和节点渲染数量。200 个节点以下基本无感,500 个节点就会开始掉帧,1000 个节点基本只能看静态图了。我的优化建议按优先级排序:
- 节点太多时先做连通分量分析,把大图拆成多个小图分开展示。
- 关闭标签,或者只在缩放级别较高时才显示标签。
- 关闭布局动画,减少计算量。
- 减小节点 symbolSize,降低渲染压力。
- 预计算坐标,用 layout="none" 导入。
4.6 pyecharts 版本差异带来的接口变化
pyecharts 从 1.x 到 2.x 有一些不兼容改动,最典型几个:Graph() 的 add 方法返回对象还是 None、set_global_opts 的参数名调整、部分内置主题的路径变化。如果你照着网上的老教程写,报错一堆,大概率是版本问题。
我建议统一用最新稳定版,并且写代码时留意一下当前版本:
import pyecharts print(pyecharts.__version__)如果版本是 1.x,遇到 add 方法链式调用报错,就在 Graph() 构造后分行调用,每调一个方法就换一行,也能规避不少奇怪的问题。新版链式写法是主流,我的示例代码都是按 2.x 写的。
最后再分享一个小技能,也是我每次做 Graph 前必做的一步:把转换好的 nodes 和 links 分别 print 前三条,肉眼确认 name 的一致性。别嫌这一步啰嗦,我多次因为 Excel 里藏了个空格或者全角字符,在整张图上找一条消失的边,找了半小时。数据小的时候看不出来,数据一大,返工成本高得吓人。先把数据结构理干净,再到样式和布局上做文章,这是做关系图最省时间的捷径。