“用 echarts 画流程图”,这话放在三年前我自己都不信,因为 echarts 在我印象里就是柱状图、饼图、折线图、地图这些东西的代名词。直到有一次做后台管理系统,需要把用户注册审核的流程和数据可视化大屏塞进同一个页面,既不想为了流程图单独引一套拖拽设计器,又希望所有图表可以共用一套主题、一套点击交互,我才认真把 echarts 的 graph 系列翻出来研究了一遍。实际折腾下来发现,echarts 不仅能画流程图,而且在数据驱动、联动交互这件事上,很多时候比专业的流程图工具更顺手。
这篇文章不是抄文档,是我把项目里的真实案例拆开之后的完整复盘,包括:什么时候该用 echarts 画流程图、graph 系列的核心思路、一个从零开始的“用户注册审核流程图”,以及适配大屏时的各类坑。内容偏实战,适合已经会用 echarts 但没用 graph 画过流程图的人,也适合正准备在毕设或后台系统里画一张动态流程图的同学。
1. 先从需求聊起:为什么偏偏用 echarts 画流程图
1.1 流程图里的图形符号都有固定含义,别用错
很多人一上来就写代码,结果画出来的东西叫“美术连线图”,不叫流程图。流程图有约定俗成的图形语义,这个在软件工程、数学建模、课程设计里都会被检查。最基本的几类:
| 图形 | 含义 | 典型场景 |
|---|---|---|
| 椭圆/圆角矩形 | 开始或结束 | 流程起点、终止节点 |
| 矩形 | 处理步骤 | 执行某个操作、赋值、计算 |
| 菱形 | 判断分支 | if/else、条件校验、审核 |
| 平行四边形 | 输入/输出 | 读取数据、打印结果 |
| 圆形/连接符 | 页面内跳转 | 避免跨页连线太长 |
| 双杠矩形 | 预定义过程/子流程 | 调用子模块 |
用 echarts 画流程图时,很多人会忽略这一步,直接用圆形表示“处理”,用矩形表示“判断”,别人看懂要靠猜。实际上 echarts 的 graph 系列提供了丰富的 symbol 类型:circle、rect、roundRect、diamond、triangle等等,足够覆盖流程图的常规需要。在画之前,先想清楚你每个节点在上面这个表格里属于哪一类。
1.2 技术选型对比:专业流程图工具 vs echarts graph
我在做选型的时候,主要在两个方向之间犹豫:一是引入 LogicFlow、X6、bpmn.js 这类专业图编排库,二是用 echarts graph 硬画。对比下来,结论其实很清晰:
| 维度 | echarts graph | 专业流程图库 |
|---|---|---|
| 上手成本 | 低,只需配置 nodes 和 links | 中等,需要理解 Model、Edge、Node 的生命周期 |
| 拖拽编辑 | 原生不支持,需要自己开发 | 内置拖拽、连线、锚点 |
| 数据驱动 | 非常直接,setOption 全量更新 | 依赖库内部数据模型,增量更新灵活 |
| 联动图表 | 同生态,坐标轴、提示框、主题天然统一 | 一般要自己再封装 |
| 自定义程度 | 高,动画、缩放、点击事件都现成 | 高,但很多功能要写插件 |
| 大屏适配 | resize 后整体重绘,效果稳定 | 视库而定,拖拽型编辑器在大屏里适配反而麻烦 |
所以我的结论是:如果只是“展示”一张流程,而不是让人“编辑”流程,echarts 完全够用,而且更省事。如果是给业务人员做流程设计器,需要拖拖拽拽、连连接点、保存画布数据,那还是老老实实用 LogicFlow 或 bpmn.js 这类专业库。
1.3 echarts 流程图适合什么人、什么项目
从我实际接触过的需求来看,下面这几类项目特别适合用 echarts 画流程图:
- 数据可视化大屏里的流程展示。节点旁边要挂实时指标,比如“待审核人数 12”、“今日通过率 86%”,echarts 的 label 和 tooltip 直接能绑定数据,天然适合。
- 后台管理系统里的模块关系图、用户流程说明页。不需要编辑,只需要把逻辑讲清楚。
- 课程设计、毕业设计里的系统流程图。用代码画出来可以直接嵌进网页,比截图 Visio 更专业,也好维护。
- 和 echarts 已有的其他图表组合在一个页面,保持技术栈统一。
反过来说,如果需求里有“用户要能自己拖拽节点、画连线、保存流程配置”这些字样,那一开始就别选 echarts,它不是一个图编辑框架,别硬上。
2. graph 系列:用节点和边把流程图搭起来
2.1 节点、边、布局,三个关键概念
echarts 的 graph 类型本质是图(Graph)模型,核心概念就三个:节点(node)、边(edge/line)、布局(layout)。
节点用数组传进去,每个节点至少有个id和name,然后你可以给它叠一堆样式字段,比如坐标、符号形状、大小、颜色。边用另一个数组传,每条边指定source和target,分别表示从哪个节点到哪个节点。
理解了这个模型,再看流程图就别把它当成“画图”了,而是当成“填数据”:
const nodes = [{ id: 'start', name: '开始' }, { id: 'end', name: '结束' }]; const links = [{ source: 'start', target: 'end' }];这个结构类似地铁线路图:站点是节点,站与站之间的线路是边。流程图里常见的“回退”“分支”“循环”,本质上都是边的方向和条件不同而已。
2.2 布局方式怎么选:手中没有需求就别滥用 force
graph 系列支持layout: 'force'(力导向)、layout: 'circular'(环形)、layout: 'none'(自定义布局)三种模式。很多新手一上来就用 force,因为不管什么数据,它都会自动把节点摊开,视觉上还挺有“关系图”的感觉。
但流程图恰恰不能这么干。流程图的阅读顺序是强制的:从上到下、从左到右,判断分支的位置必须固定,回退线必须清晰。force 布局会把节点“吸”到奇怪的位置,还会在数据更新时产生抖动,根本没法用。
所以画流程图的通用做法是:layout: 'none',然后手动给每个节点指定x和y,并设置fixed: true。这样 echarts 就不会去干预你的坐标。
这里有个容易踩的坑:即使你配置了 x/y,如果忘了写fixed: true,graph 在有些版本里还是会认为坐标只是“初始位置”,更新数据后就可能被重新排布。我在项目中吃过这个亏,调试了半天才发现是 fixed 的问题。
2.3 把流程图图例翻译成 config:symbol、label、线型、箭头
理解了 graph 是数据驱动的,接下来就是把流程图的图形语义映射到 echarts 的配置上。这个过程不需要死记硬背,核心就四个点:
节点形状用symbol控制
symbol: 'circle' // 开始、结束 symbol: 'rect' // 处理步骤 symbol: 'roundRect' // 处理步骤(更柔和) symbol: 'diamond' // 判断节点大小用symbolSize控制
symbolSize可以是数字,也可以是数组[宽, 高]。矩形、菱形建议都用数组形式,只给一个数字的话长宽相等,写“处理步骤”这种字多的 label 很容易被截断。注意 diamond 的宽高指的是菱形外接矩形的宽高,具体显示出来多少,建议边调边看,不要凭感觉。
箭头和线型用edgeSymbol+lineStyle控制
edgeSymbol: ['none', 'arrow'] // 起点无箭头、终点有箭头 lineStyle: { type: 'solid', // 默认流程用实线 curveness: 0 // 0为直线,回退线可以设成0.2或0.3 }如果你想突出“回退”或“异常流”,可以把线型设成dashed,颜色换成红色,视觉上一下就能区分开。
判断分支的条件文字,用边的label控制
{ source: 'check', target: 'submit', label: { show: true, formatter: '通过' } }这个是在边上显示文字,比如“通过”“驳回”“不通过”。graph 系列里这部分的字段叫label,不是edgeLabel,我见过有人照着折线图文档写edgeLabel导致不显示的情况。
3. 实操:写一个“用户注册审核流程图”
3.1 先画一张纸面草图,再列节点数据
写代码之前,我强烈建议先在纸上把这套流程的逻辑画出来,否则写 links 的时候经常会把 source 和 target 搞反。我这边用一个用户注册审核的例子,流程如下:
开始 → 填写注册信息 → 格式校验(判断)→ 校验不通过,回到填写注册信息;校验通过,进入提交注册 → 管理员审核(判断)→ 审核通过,开通账号;审核不通过,驳回回到填写注册信息 → 结束
这个流程有判断、有回退、有分支,用来演示 echarts 画流程图基本够用了。先把节点列出来:
| id | name | 符号 | 含义 |
|---|---|---|---|
| start | 开始 | circle | 开始 |
| fill | 填写注册信息 | roundRect | 处理步骤 |
| check | 格式校验 | diamond | 判断 |
| submit | 提交注册 | roundRect | 处理步骤 |
| audit | 管理员审核 | diamond | 判断 |
| pass | 开通账号 | roundRect | 处理步骤 |
| reject | 驳回 | roundRect | 处理步骤 |
| end | 结束 | circle | 结束 |
这一步就把“流程图的图形语义”和“数据模型”对应上了。后面写代码只是翻译。
3.2 节点坐标与样式控制
节点坐标我按 600 宽、700 高的画布来排,垂直方向从上到下把流程主线铺开,分支在左右两侧展开。具体坐标设计如下:
- start: (300, 40)
- fill: (300, 140)
- check: (300, 260)
- submit: (300, 380)
- audit: (300, 500)
- pass: (180, 620)
- reject: (420, 620)
- end: (300, 720)
为什么这么排?主流程全部居中,判断分支左右对称,回退线才有空间画弧线,不会和主线重叠。如果你把分支节点也排在中间,回退线就会穿来穿去,最后一张图根本没法看。
节点配置示例:
{ id: 'check', name: '格式校验', symbol: 'diamond', symbolSize: [140, 70], x: 300, y: 260, fixed: true, label: { show: true, position: 'inside' } }这里label默认在节点内部,判断节点的字比较多,可以用position: 'inside'配合fontSize控制。如果菱形里放不下,也可以放到外面,比如position: 'right',看具体设计。
3.3 连线的细节:分支、回退、曲线、边标签
节点排好后,连线是重点。流程图里大部分连线是直线,但回退线必须用曲线,否则会和主流程线重叠。echarts 的 graph 里通过lineStyle.curveness控制曲线程度,curveness越大,线越弯。
再看分支条件。格式校验节点出去的两条线,一条回到前面的fill节点,一条去submit节点。光看线不够直观,必须在线条上标注条件文字:
{ source: 'check', target: 'submit', label: { show: true, formatter: '通过', color: '#27ae60' } }, { source: 'check', target: 'fill', label: { show: true, formatter: '不通过', color: '#e74c3c' }, lineStyle: { curveness: 0.2, type: 'dashed' } }这样读图的人一眼就能看出来:绿线是“通过”,红色虚线是“不通过”,回退方向是虚线,逻辑跑得通。
3.4 完整代码:一个可运行的 echarts 配置
把上面的节点和连线整合成一个完整的 option,核心是 series 的类型设为graph:
const option = { tooltip: { trigger: 'item' }, series: [{ type: 'graph', layout: 'none', roam: true, nodes: [ { id: 'start', name: '开始', symbol: 'circle', x: 300, y: 40, fixed: true, label: { show: true, position: 'bottom' } }, { id: 'fill', name: '填写注册信息', symbol: 'roundRect', symbolSize: [160, 50], x: 300, y: 140, fixed: true, label: { show: true, position: 'inside' } }, { id: 'check', name: '格式校验', symbol: 'diamond', symbolSize: [150, 70], x: 300, y: 260, fixed: true, label: { show: true, position: 'inside' } }, { id: 'submit', name: '提交注册', symbol: 'roundRect', symbolSize: [160, 50], x: 300, y: 380, fixed: true, label: { show: true, position: 'inside' } }, { id: 'audit', name: '管理员审核', symbol: 'diamond', symbolSize: [160, 70], x: 300, y: 500, fixed: true, label: { show: true, position: 'inside' } }, { id: 'pass', name: '开通账号', symbol: 'roundRect', symbolSize: [160, 50], x: 180, y: 620, fixed: true, label: { show: true, position: 'inside' } }, { id: 'reject', name: '驳回', symbol: 'roundRect', symbolSize: [140, 50], x: 420, y: 620, fixed: true, label: { show: true, position: 'inside' } }, { id: 'end', name: '结束', symbol: 'circle', x: 300, y: 720, fixed: true, label: { show: true, position: 'bottom' } } ], links: [ { source: 'start', target: 'fill' }, { source: 'fill', target: 'check', label: { show: true, formatter: '提交', position: 'right' } }, { source: 'check', target: 'submit', label: { show: true, formatter: '通过', color: '#27ae60', position: 'right' } }, { source: 'check', target: 'fill', label: { show: true, formatter: '不通过', color: '#e74c3c', position: 'left' }, lineStyle: { curveness: 0.2, type: 'dashed', color: '#e74c3c' } }, { source: 'submit', target: 'audit' }, { source: 'audit', target: 'pass', label: { show: true, formatter: '通过', color: '#27ae60', position: 'right' } }, { source: 'audit', target: 'reject', label: { show: true, formatter: '驳回', color: '#e74c3c', position: 'left' }, lineStyle: { curveness: 0.2, type: 'dashed', color: '#e74c3c' } }, { source: 'reject', target: 'fill', label: { show: true, formatter: '重新填写', position: 'bottom' }, lineStyle: { curveness: 0.3, type: 'dashed', color: '#e74c3c' } }, { source: 'pass', target: 'end' } ] }] };直接把这段配置丢给一个初始化好的 echarts 实例,刷新就能看到流程图。注意roam: true开了缩放和平移,这对于节点数量多的流程特别有用,不然画布放不下时只能干瞪眼。
3.5 加上 interaction:点击、缩放、tooltip 换行
只是画出来肯定不够,流程图最好能点、能看详情。点击节点的事件可以这样挂:
myChart.on('click', function (params) { if (params.dataType === 'node') { const nodeData = params.data; // 在这里根据 nodeData.id 去查详情、跳转页面、打开抽屉 } });注意dataType的取值,节点是'node',边是'edge'。如果不判断,点击连线时也会触发回调,很容易误操作。
tooltip 默认对于长文本不会自动换行,如果节点要展示的详情特别长,可以在 formatter 里手动处理:
tooltip: { trigger: 'item', formatter: function(params) { if (params.dataType === 'node') { const desc = params.data.desc || ''; return params.data.name + '\n' + wrapText(desc, 20); } return ''; } } function wrapText(text, maxLen) { const arr = []; for (let i = 0; i < text.length; i += maxLen) { arr.push(text.substring(i, i + maxLen)); } return arr.join('\n'); }这个wrapText是我实际项目里一直在用的工具函数,很多后台页面都要求 tooltip 不要撑破屏幕,手动换行比依赖 CSS 靠谱得多。
4. 从单图到大屏:适配、TOOLTIP 与周边图表的坑
4.1 px 转 rem 后,echarts 为什么纹丝不动
很多用 Vue 3 + 大屏项目的人会碰到一个问题:项目里配置了 postcss-pxtorem,把 CSS 里的 px 自动转成 rem,页面上的普通 DOM 元素缩放得很好,但 echarts 画出来的图表纹丝不动,而且窗口一变化,图表要么溢出要么太小。
原因很简单:pxtorem 只处理 CSS 文件里的 px,而 echarts 的 option 里的数值是在 JS 里直接设置的数字像素,它根本不走 CSS 编译流程。而 canvas 内部绘制的文字、坐标、节点大小,全都是根据这些 JS 数值直接画到位图上的,和页面 rem 无关。所以你给 echarts 设置什么尺寸,它就画什么尺寸,不会跟随 rem 缩放。
如果你试图用transform: scale()去缩放整个 canvas 容器,图表是“看起来”小了,但 canvas 是一个位图,被 CSS 缩放之后会模糊,而且 tooltip 位置也会偏移,这不是正路。
4.2 大屏适配的正确写法:scale 比例方案
我在大屏里画 echarts 流程图时,用的是比较笨但最稳的方案:设计稿固定宽度,所有节点坐标、symbolSize、字体大小都按比例缩放。
设计稿宽度按 1920 来出,运行时拿到当前容器的宽度,计算缩放比例:
const baseWidth = 1920; const scale = window.innerWidth / baseWidth; const nodes = nodeData.map(d => ({ ...d, x: d.x * scale, y: d.y * scale, symbolSize: [d.w * scale, d.h * scale] })); const option = { series: [{ type: 'graph', layout: 'none', nodes, // ... }] };字体大小同样要乘 scale。启动时执行一次,窗口 resize 时再执行一次,并调用chart.resize()重绘。
这个方案核心思路是:把 echarts 当成一个“按像素绘制的画布”,而我们手动控制它的总大小。和 rem 没关系,也不需要 pxtorem 去处理 canvas 内部的东西。实测在大屏和普通 PC 屏幕上都能保持相对位置一致,不会出现节点错位、连线歪斜的问题。
4.3 顺手解决同类 echarts 高频问题(饼图小圆点、x 轴刻度)
在 echarts 社区里经常能看到这几个问题,虽然不是流程图的核心,但既然是 echarts 实战,拿出来一起说:
- 饼图 labelLine 末尾小圆点偏移。这个问题的根源一般是
labelLine.length2太长,而 label 的align方向不对,导致末尾圆点没有紧贴文字。处理方式是把label.align设成和 labelLine 同方向,比如'left'或'right',同时调小length2。如果你需要绝对精确,还可以改用富文本形式自定义 label,在小圆点的位置放一个文字符号。 - 折线图 x 轴刻度过多重叠。做法是设置
xAxis.axisLabel.interval: 0强制全部显示,或者配合rotate: 45、hideOverlap: true来优化。流程图里没有 x 轴,但如果你把流程图和其他图表组合到大屏,这个同样常见。
这些都是小细节,但 echarts 的坑往往就藏在细节里,先把周边问题扫干净,流程图才能真正地放进大屏。
5. 常见问题与排查技巧实录
5.1 节点不受控制、全挤在一起
现象是:明明设置了 x/y,节点还是乱跑或者挤成一团。
排查步骤按顺序来:
- 确认
layout是不是'none',只要不是'none',坐标就可能被布局算法覆盖。 - 确认每个节点都设置了
fixed: true。没有 fixed,graph 更新时会重新计算位置。 - 检查节点 id 是否重复。如果 id 重复,links 里的 source/target 可能连到错误节点,视觉上表现为位置错乱。
这个是我最常在同事代码里看到的问题,九成都是 layout 忘了改。
5.2 连线标签文字和线条重叠
边上的条件文字如果挤在线上,读起来非常费劲。处理方法有几种:
- 给 edge 的 label 加背景色和 padding:
label: { show: true, formatter: '通过', backgroundColor: '#fff', padding: [2, 6], borderRadius: 4 }- 合理利用
position,例如文字放在线条的'middle'、'start'、'end',不同曲线类型配合不同位置。 - 回退线一定要设置
curveness,曲线拉开后文字空间就出来了。
加背景色是我试过最有效的方案,没有之一。不加背景,文字和线交叉时完全没办法看。
5.3 点击节点拿不到数据 / 事件绑定不上
点击事件绑不上,先确认你的 echarts 实例有没有被其他图层挡住。graph 系列的节点如果有顶层浮层,鼠标事件可能被拦截。
另外,节点数据里如果设置了silent: true,点击也会失效。排查时可先在chart.on('click')回调里打印 params,看看触发的params.dataType是什么,再决定是判断问题还是数据问题。
还有一点容易忽略:你必须在初始化的 chart 实例上绑定事件,而不是在 option 里写。有些新手会写option.click = function(){},然后发现没有反应,这就是把 echarts 当成 DOM 在用了。
5.4 节点数量大、动画卡的问题
流程图几十个节点还好,上到上百个节点就会开始掉帧,尤其 hover 时阴影、动画全部叠加时特别明显。优化思路:
- 关闭动画:
animation: false。 - 减少阴影和
emphasis状态里的复杂样式。 symbol尽量用内置形状,不要用大尺寸图片或path字符串,path 越复杂渲染越慢。- 如果节点和边超过 500,建议做数据聚合或分屏展示,不要硬画。
echarts 性能再强也是基于 canvas 的,节点数量级到了,不要执着于一个图硬画到底,拆成多张图或者用分片渲染才是正解。
6. 相关概念:BPMN 网关、思维导图与其它流程工具
6.1 BPMN 网关符号到底长什么样、怎么用
如果你接触过业务审批系统,可能会看到“排他网关”“并行网关”这些词,它们是 BPMN(业务流程建模标注)规范里的概念。
简单理解:网关就是一个分流器/合并器,控制流程在某个节点如何分支、如何汇聚。最常见的三种:
- 排他网关(X):多个分支只走一个,相当于 if/else。
- 并行网关(+):多个分支全部走,相当于拆成多个并行任务。
- 包容网关(O):满足条件的分支都走,比排他网关更灵活。
如果只是展示用途,echarts 里可以用diamond的变形加文字模拟:菱形中间画个 X 或 +。如果要做成可编辑的 BPMN 流程设计器,还是建议用 bpmn.js 这类库,echarts 不是干这个的。
6.2 毕设/课设里的软件工程流程图怎么画才算“标准”
很多计算机专业、信息管理专业的同学在毕设里要画“用户管理模块流程图”“系统登录流程图”“数学建模流程图”。这里有个容易被忽略的考点:审核老师看重的往往不是图形多漂亮,而是逻辑是否完整、符号是否规范。
规范的流程图画法需注意:
- 每个判断框至少有两个出口,且每个出口都要有明确的条件文字。
- 每个路径最终要能走到“结束”,不能出现无法到达的死节点。
- 流程方向从上到下、从左到右,回退线允许从右到左,但不允许出现不必要的交叉。
- 起止节点只能有一个“开始”,可以有多个“结束”。
如果只是画在论文里,用 Visio、draw.io、ProcessOn 都行。但如果你想把流程图嵌到毕设系统网页里展示,用 echarts graph 画一张动态的会加分不少,它可以在线缩放、点击看详情,答辩时演示效果很直观。
6.3 什么时候用 xmind、draw.io、visio,而不是 echarts
工具没有绝对的好坏,只有合适不合适。我自己的选择标准是这样:
| 场景 | 推荐工具 |
|---|---|
| 快速梳理思路、从思维导图转流程 | xmind |
| 画静态论文插图、作业图 | draw.io、Visio |
| 在线协作、多人同时标注 | ProcessOn |
| 把流程图嵌入系统、联动其他图表、数据驱动展示 | echarts graph |
xmind 做流程图的能力其实偏弱,它最强的是思维导图。如果只是把“流程逻辑”理清楚,用 xmind 的逻辑图足够;但如果要精确控制每一个箭头、每一个分支的坐标,那还是 draw.io 或者直接写 echarts 更可控。不要把时间花在纠结工具上,先想清楚你这个图是“给人看的”还是“给系统用的”,答案自然会出来。
最后分享一个我自己的习惯。不管是用 echarts 还是其他工具,画流程图之前我一定先拿草稿纸把节点 id 列出来,再把每条边的 source 和 target 写清楚。graph 的数据结构本质上就是一张图的邻接表,links里写错一个 source,箭头就会跑到奇怪的地方,排查起来反而最费时间。
如果只让我留一条经验给后来人,那就是:先把数据模型设计干净,剩下的交给配置。流程图画的是数据逻辑,不是美术作品。