☰
用ECharts graph绘制流程图:从节点布局到动态数据联动实战
2026/10/1 6:28:17 网站建设 项目流程

1. 为什么我会用echarts来画流程图

先说个真实经历。大概一年前,我接到一个内部数据平台的需求,要在用户管理模块里展示完整的注册、登录、权限校验链路,还要跟后端返回的实时数据联动,比如每个环节当前的请求量、失败率、平均耗时。当时团队里有人提议用bpmn.js,有人建议上antv X6,还有人嫌麻烦说干脆用visio截图扔上去。我琢磨了一下,最后选了echarts的graph系列。

这个选择当时被同事质疑过,毕竟echarts在大多数人印象里就是画柱状图、折线图、饼图的,跟流程图八竿子打不着。但我的理由其实很现实:项目里已经引入了echarts,数据可视化的部分大量依赖它,我不希望为了一个流程图再额外引入一套几百KB的渲染引擎;而且需求里明确要求节点颜色、大小、文字内容要随着接口数据动态变化,甚至点击某个节点要联动旁边的统计面板。这种“数据驱动展示型流程图”的场景,echarts的graph系列反而比专业流程编辑工具更顺手。

这里要先说清楚一件事:echarts画的流程图和bpmn.js画的流程图,本质上不是同一个东西。bpmn.js的核心价值在于流程定义的编辑、校验、序列化,它面向的是“需要保存成标准BPMN模型”的业务流程设计场景,比如OA审批流、工作流引擎配置。而echarts graph系列面向的是“把流程结构可视化展示出来”的场景,它不做流程合法性校验,也不负责生成可执行的流程定义文件,但它胜在灵活、轻量、跟数据绑定极其自然。

所以如果你接到的需求是“做一个流程图给领导看”“把系统模块调用关系可视化”“根据日志数据实时渲染调用链”,那echarts完全够用,而且体验不差。如果需求是“用户要拖拖拽拽自己设计流程,还要保存成XML”,那请直接选bpmn.js或logicflow,别用echarts硬扛。这个边界想清楚了,后面的技术选型才不会跑偏。

2. 动手前的知识铺垫:graph系列的核心概念

在用echarts画流程图之前,有几个基础概念必须先吃透。不然你很可能跟我第一次一样,数据填进去图就是出不来,或者出来了一团乱麻。

2.1 节点、边和图结构

echarts的graph类型(关系图)由两部分组成:nodes数组和links数组(你也可以写成edges)。nodes里的每个对象代表图中的一个节点,比如一个流程步骤、一个系统模块、一个人;links里的每个对象代表一条连接关系,通常包含source和target两个字段,分别指向节点的id或下标。

拿用户管理模块举例,nodes可能是这样的结构:

const nodes = [ { id: 'start', name: '开始', symbolSize: 60 }, { id: 'login', name: '登录', symbolSize: 60 }, { id: 'checkRole', name: '角色校验', symbolSize: 60 }, { id: 'adminPanel', name: '管理员面板', symbolSize: 60 } ]; const links = [ { source: 'start', target: 'login' }, { source: 'login', target: 'checkRole' }, { source: 'checkRole', target: 'adminPanel' } ];

只要给option里配一个type: 'graph'的series,把这两个数组填进去,就能渲染出最简单的流程图。但注意,这只能得到最基本的效果:节点按默认布局排布、连线是直线、没有箭头、没有各种流程形状。要做到像模像样的流程图,还需要处理布局、造型和交互。

2.2 理解布局:layout属性是流程图成败的关键

graph系列里有一个让人又爱又恨的属性:layout。它有三种取值:'none'、'force'和'circular'。

第一次尝试用echarts画流程图的人,十个有九个会栽在'force'上。力引导布局会让所有节点像带电粒子一样互相排斥,同时连线又像弹簧一样把节点拉近,整个图会不断抖动、自动寻找平衡位置。听起来很智能对吧?但对于流程图,这基本是灾难。你想表达“步骤A到步骤B再到步骤C”的严格顺序,力引导布局却可能把C甩到A旁边,整个流程的阅读顺序全部乱掉。

所以我的建议很直接:画流程图一律使用layout: 'none',然后手动指定每个节点的x和y坐标。坐标系的原点在echarts图表的中心,向右是x正方向,向下是y正方向。虽然手动布局需要自己计算位置,但这些付出是值得的——只有你能完全掌控节点的位置,才能保证流程图的清晰性和可读性。

除非你只是画一个简单的环形调用图,或者节点之间没有明显的先后次序,那circular布局倒是可以用一下,把节点排成一个圈。

2.3 流程图形状怎么做:玩转symbol

流程图的视觉灵魂在于各种形状:圆角矩形表示起止、矩形表示处理步骤、菱形表示判断、平行四边形表示输入输出。但echarts的graph节点默认是圆形,怎么折腾成流程图的样子?

答案是利用symbol属性。它支持内置的形状(circle、rect、roundRect、triangle、diamond等),也支持自绘SVG path或dataURI。做流程图最有用的是两个方向:

第一,用内置形状。symbol: 'roundRect'可以得到圆角矩形,适合做开始/结束节点;symbol: 'diamond'可以得到菱形,适合做判断节点。这几个内置形状虽然无法严格区分矩形和平行四边形,但只要颜色、文字标注到位,效果完全够用。

第二,用SVG path自定义形状。如果你想做得更专业,比如结束节点用双圆线框,或者处理节点要带阴影立体感,就需要自绘symbol。比如我要画一个输入/输出感的平行四边形,可以这样写:

symbol: 'path://M30,0 L70,0 L40,40 L0,40 Z'

这个path字符串的写法其实就是SVG的路径语法:M移动到起点,L画直线到指定坐标,Z闭合路径。用这种方式你可以画出任何形状。要注意的是symbol默认的原点在图形左上角,所以如果你想精确对齐位置,可以通过symbolOffset微调。

2.4 连线和箭头:流程走向的表达

有了节点和形状,还需要箭头来指明流程方向。echarts graph的边默认是纯直线,不带箭头。要加箭头需要配置:

links: [{ source: 'login', target: 'checkRole', lineStyle: { color: '#333', width: 2, type: 'solid' }, symbol: ['none', 'arrow'], // 数组第二项表示在target端加箭头 symbolSize: 10 }]

这里symbol可以设成一个数组,第一个值代表source端的箭头样式,第二个值代表target端的箭头样式。通常我们在流程终点加箭头,所以写['none', 'arrow']。

此外,如果你觉得直角折线更像流程图(很多标准流程图画法确实是横平竖直的),echarts graph本身不支持自动生成正交折线,但你可以通过给links设置不同段的控制点和拐点来实现,或者用一条折线polyline加多个点模拟。不过坦白说,我在实际项目里很少折腾直角折线,曲线其实更优雅,阅读顺序感也更自然。只要箭头清楚,别人不会纠结你是直角还是圆角。

3. 核心实战:从零开始做一个登录注册流程图

理论铺垫得差不多了,下面我带你完整走一遍实操。我的场景是“用户登录与权限分配流程”,包含开始、输入账号密码、判断格式、请求后端、角色校验、管理员/普通用户分支、结束等多个节点。我们一步步把代码写出来。

3.1 定义流程图结构和数据

首先确定整个流程包含哪些步骤,以及它们之间的连线关系。流程大致是这样:

开始 -> 输入账号密码 -> 格式校验(判断) -> 是则请求登录接口 -> 后端返回角色 -> 管理员分支/普通用户分支 -> 结束

这个流程里有判断节点,有分支,足够说明大部分问题。先把节点和边的数据定义好:

const nodes = [ { id: 'start', name: '开始', x: 0, y: -240, symbol: 'roundRect' }, { id: 'input', name: '输入账号密码', x: 0, y: -140, symbol: 'rect' }, { id: 'checkFormat', name: '格式是否正确', x: 0, y: -40, symbol: 'diamond' }, { id: 'request', name: '请求登录接口', x: 0, y: 60, symbol: 'rect' }, { id: 'getRole', name: '解析角色信息', x: 0, y: 160, symbol: 'rect' }, { id: 'isAdmin', name: '是否管理员', x: 0, y: 260, symbol: 'diamond' }, { id: 'adminPanel', name: '管理员面板', x: -160, y: 360, symbol: 'rect' }, { id: 'userPanel', name: '普通用户面板', x: 160, y: 360, symbol: 'rect' }, { id: 'end', name: '结束', x: 0, y: 460, symbol: 'roundRect' } ];

这里我用x/y直接指定了每个节点的坐标,整体呈纵向自上而下的布局,判断节点出分支时左右分开。需要注意的是,x:0并不是图表的最左边,而是水平方向的中点,所以负值表示靠左,正值靠右。在实际项目中,建议先画出流程草图,标出每个节点的大致位置,再转换成坐标值,这样不容易乱。

3.2 配置连线与逻辑分支

边的定义同样直接:

const links = [ { source: 'start', target: 'input' }, { source: 'input', target: 'checkFormat' }, { source: 'checkFormat', target: 'request', label: { show: true, formatter: '是' } }, { source: 'checkFormat', target: 'start', label: { show: true, formatter: '否' }, lineStyle: { type: 'dashed' } }, { source: 'request', target: 'getRole' }, { source: 'getRole', target: 'isAdmin' }, { source: 'isAdmin', target: 'adminPanel', label: { show: true, formatter: '是' } }, { source: 'isAdmin', target: 'userPanel', label: { show: true, formatter: '否' } }, { source: 'adminPanel', target: 'end' }, { source: 'userPanel', target: 'end' } ];

这里有两个细节值得展开说。

第一个是判断分支的标签。格式不正确时,我让连线回到“开始”节点,用虚线表示,并加上label显示“否”;格式正确时用实线进到下一步。这样看的人一眼就能理解条件分支的语义。

第二个是标签的显示。graph的边默认是不显示文字的,需要单独配置label.show: true,然后用formatter指定文字内容。如果分支多,你还得考虑文字会不会重叠,比如判断框出来的“是”和“否”方向要错开。简单办法是给label再加一个position: 'middle',然后配合边的曲线偏移把标签推离连线中心。

3.3 组装完整的series与option

核心series这样写:

const series = { type: 'graph', layout: 'none', data: nodes, links: links, roam: true, label: { show: true, position: 'inside', fontSize: 12, color: '#333', formatter: function(params) { return params.name; } }, lineStyle: { color: '#999', width: 2, curve: 0.2, type: 'solid' }, emphasis: { focus: 'adjacency', lineStyle: { width: 4, color: '#5470c6' } } };

把series填进option,再myChart.setOption(option),就能看到一张基础流程图了。这里我建议把roam设为true,允许用户缩放和平移。流程图如果节点很多,超出屏幕是常事,没有roam的话用户只能干瞪眼。

focus: 'adjacency'这个配置也很关键。它实现了“鼠标悬停某个节点时,只高亮与该节点直接相连的边和节点,其他元素变暗”的效果。在复杂流程里,这个交互能帮你快速看清某一步的前后依赖关系,体验提升非常明显。

3.4 代码层面的完整示例

如果你只想要一个能直接用起来的最小demo,可以参考下面这段代码:

const chartDom = document.getElementById('main'); const myChart = echarts.init(chartDom); const nodes = [ { id: 'start', name: '开始', x: 0, y: -240, symbol: 'roundRect' }, { id: 'input', name: '输入账号密码', x: 0, y: -140, symbol: 'rect' }, { id: 'checkFormat', name: '格式是否正确', x: 0, y: -40, symbol: 'diamond' }, { id: 'request', name: '请求登录接口', x: 0, y: 60, symbol: 'rect' }, { id: 'getRole', name: '解析角色信息', x: 0, y: 160, symbol: 'rect' }, { id: 'isAdmin', name: '是否管理员', x: 0, y: 260, symbol: 'diamond' }, { id: 'adminPanel', name: '管理员面板', x: -160, y: 360, symbol: 'rect' }, { id: 'userPanel', name: '普通用户面板', x: 160, y: 360, symbol: 'rect' }, { id: 'end', name: '结束', x: 0, y: 460, symbol: 'roundRect' } ]; const links = [ { source: 'start', target: 'input' }, { source: 'input', target: 'checkFormat' }, { source: 'checkFormat', target: 'request', label: { show: true, formatter: '是' } }, { source: 'checkFormat', target: 'start', label: { show: true, formatter: '否' }, lineStyle: { type: 'dashed' } }, { source: 'request', target: 'getRole' }, { source: 'getRole', target: 'isAdmin' }, { source: 'isAdmin', target: 'adminPanel', label: { show: true, formatter: '是' } }, { source: 'isAdmin', target: 'userPanel', label: { show: true, formatter: '否' } }, { source: 'adminPanel', target: 'end' }, { source: 'userPanel', target: 'end' } ]; const option = { tooltip: {}, series: [{ type: 'graph', layout: 'none', data: nodes, links: links, roam: true, label: { show: true, position: 'inside', fontSize: 12, color: '#333' }, lineStyle: { color: '#999', width: 2, curve: 0.2 }, emphasis: { focus: 'adjacency', lineStyle: { width: 4, color: '#5470c6' } } }] }; myChart.setOption(option);

这套代码我实际跑过,直接粘到HTML里配合echarts.min.js就能看到效果。跑起来之后你基本就有了一个可以交互、可以缩放、节点造型区分明显的流程图。

4. 进阶美化与实践技巧

说实话,第一步能画出来只是完成了20%。一个合格的流程图表现在还缺少很多细节:tooltip友好提示、自动换行、颜色分级、节点尺寸自适应、复杂分支的路径规划。这一节我挑几个高频问题重点讲。

4.1 tooltip自动换行的两种解法

echarts的tooltip默认在一行内展示所有内容,如果节点描述太长,很容易撑爆弹层或者显示不全。网上刷到很多人在问“echarts tooltip自动换行”,这里给出两个常用解法。

第一种,在formatter回调里手动拼接换行标签。tooltip的formatter支持HTML字符串,所以你可以这么做:

tooltip: { formatter: function(params) { return '<div style="max-width:220px;">' + '<div><b>' + params.name + '</b></div>' + '<div style="font-size:12px;color:#888;">节点ID:' + params.data.id + '</div>' + '<div style="word-break:break-all;">描述:这里是很长的描述文字,用于说明该流程节点的具体职责与注意事项。</div>' + '</div>'; } }

这样通过max-width和word-break:break-all,长文本就能自动折行。注意在formatter里不要用\n换行,直接拼HTML标签更可靠。

第二种,如果不想手动拼HTML,可以给tooltip设置confine: true,加上extraCssText来限制宽度。但实测下来,处理复杂内容时自己拼formatter最灵活,后面的加粗、变色、字段过滤都能按需定制。

4.2 让节点颜色与数据联动

流程图的另一个常用场景是根据指标动态染色。比如管理员面板的请求量超过阈值就变红,正常是绿色。这个需求实现起来非常简单,因为nodes的数据本身就是普通的JS对象,页面可以随时更新它们。

常见的做法是维护一个状态map,然后在每次渲染时把状态字段合并进node数据:

const statusMap = { adminPanel: 'error', userPanel: 'success', request: 'running' }; const colorMap = { success: '#52c41a', error: '#ff4d4f', running: '#1890ff' }; nodes.forEach(node => { const status = statusMap[node.id]; if (status) { node.itemStyle = { color: colorMap[status], shadowColor: colorMap[status], shadowBlur: 10 }; } });

刷新方式就是我之前讲的那两行:myChart.setOption({ series: [{ data: nodes }] })。

这种联动在监控大屏场景里特别实用。整个流程的链路健康状态一眼就能看出来,哪个节点挂了、哪个节点有延迟,通过颜色就一目了然。

4.3 节点拖拽与布局微调

graph系列默认在layout: 'none'时,节点是不允许拖拽的。但是 echarts 提供了graphDraggable配置项,让节点可以被拖动:

series: [{ type: 'graph', draggable: true }]

拖动节点之后,节点位置会保留在你拖到的位置。如果你希望位置变化后能持久化到后端,可以监听georoam对应的事件,或者监听graphRoam事件。有一个坑需要注意:如果用户拖乱了布局,你通过setOption重新传了新的data(即使坐标没变),拖拽可能被重置。解决办法是先myChart.clear(),再重新setOption;或者在你的data里明确记录x/y,每次刷新都主动写回坐标。

4.4 复杂分支怎么不交叉

流程图节点多了以后,最容易出现的问题就是连线交叉。比如两个分支从同一个判断节点出来,经过几步后又汇聚到同一个节点,画出来很可能变成“X”形交叉,观感很差。

我的经验有两个土办法。

第一,调整分支节点的坐标,使两条路径在空间上错开。比如左边的分支先向下走再向右汇合,右边的分支先向下走再向左汇合,形成一个“括弧”的形状。这需要你在设计坐标时就规划好,每条边尽量走“C”形或“S”形路径。

第二,利用边的curve属性。给不同的边设置正负不同的曲率,可以让它们在视觉上分离:

// 同一条路径上的分支,一条左弯,一条右弯 const links = [ { source: 'isAdmin', target: 'adminPanel', lineStyle: { curve: 0.3 } }, { source: 'isAdmin', target: 'userPanel', lineStyle: { curve: -0.3 } } ];

这样两条线会明显分开,不会重合纠缠。值越大弯曲越厉害,具体数值需要根据节点距离微调。

4.5 节点尺寸自适应

流程图要放到大屏或者PC端,不同屏幕尺寸下节点大小最好能自适应。不建议单纯写死symbolSize,而是基于图表宽度算一个比例。可以在窗口resize时重新计算:

function getAdaptiveSize() { const width = chartDom.clientWidth; return Math.max(40, Math.min(80, width / 20)); } window.addEventListener('resize', () => { const size = getAdaptiveSize(); myChart.setOption({ series: [{ data: nodes.map(node => ({ ...node, symbolSize: node.symbolSize || size })) }] }); myChart.resize(); });

当然如果只是内部展示,固定symbolSize也没问题。我建议至少保留roam: true,让用户在需要的时候自己放大局部看清楚。

5. 常见问题排查实战笔记

最后把我踩过的坑和读者常见的问题整理成一个速查表,方便你复用。

问题原因解决方案
图表空白,控制台无报错没有指定layout, 或者data/links为空数组检查series.data和links是否为空;layout显式指定为'none'
连线出现在节点下层/被挡住echarts渲染层级问题给node设置z: 10,给lineStyle设置z: 2;或者通过emphasis提升hover层级
自定义symbol显示成大色块path字符串写错或坐标系范围不对用path://前缀,确保SVG path的坐标在合理范围内;可以先测一个纯矩形path
箭头不见了没有配置symbol或symbolSizelinks里配置symbol: ['none', 'arrow'],symbolSize: 10
拖拽后节点错位拖拽改变了内部坐标用myChart.clear()或重新写入x/y;监听事件记录最终坐标
tooltip不换行formatter没有设置宽度约束用formatter返回HTML字符串并设置max-width
focus adjacency无效果emphasis配置层级不对确认配置在series的emphasis.focus而不是legend里
标签显示不全或重叠标签默认在节点外部设置label.position: 'inside'或调整label.padding;分支文字用position: 'right'/'left'分别设定

再补充一个我踩过比较深的坑:用setOption更新graph数据时,如果新旧数据的id对不上,echarts会重新计算布局,导致节点位置跳动。尤其是我用layout: 'none'并且手动指定坐标时,id必须保持稳定。如果id变化了,图表会认为来了新节点,之前的坐标信息就丢了。所以生产环境里我一般会约定id就是数据库里的主键,绝不用数组下标当id。

关于缩放后文字模糊,目前没有非常完美的方案。echarts的canvas渲染在浏览器缩放时会出现发虚的情况。缓解办法是监听窗口尺寸变化,调用chart.resize()强制重绘。如果对清晰度要求极高,可以尝试SVG渲染:

const myChart = echarts.init(chartDom, null, { renderer: 'svg' });

SVG渲染模式下文字清晰度会好不少,但节点数量超过几百个时性能会明显下降。你需要在清晰度和性能之间做取舍,我一般推荐展示型流程图用SVG,数据监控型的大规模图用canvas。

6. 一些踩坑后的真实体会

做完整套流程后,我再回头说点实在的。用echarts画流程图,最大的优势不是它有多专业,而是它能让流程跟数据“活”在一起。传统的流程编辑工具画出来的图是静态的,节点状态、颜色、大小都必须手动改;echarts里这些全是数据驱动,后端接口一刷新,图自己就变了。对于做数据可视化大屏、运维监控、业务链路追踪这类项目,这是不可替代的优势。

但我也想说清楚它的边界。如果你要做一个让用户自己拖拽、连线、编辑并且保存流程定义的工具,别用echarts硬扛。我见过有些团队为了省依赖,非要用graph模拟节点编辑,最后边拖拽边计算坐标,写了几百行代码还是不够流畅,体验跟专业编辑器差很多。选型这件事,适合才是王道。

最后分享一个小技巧:如果你希望流程图的连线看起来更“柔”,不要在lineStyle里把curve调太大,0.15到0.3之间是比较舒服的区间。调太大线会弯成一个大圈,反而影响阅读。另外,判断节点出来的“是/否”标签,建议把标签放在靠近目标节点的一端,而不是线的正中间,可以用label.position: 'right'或者label.position: 'bottom'配合offset微调,这样视觉上更清晰,读者看分支时不需要费力找文字。

流程图的本质是把复杂逻辑变简单,画图只是手段,让人一眼看懂才是目的。希望这篇分享能帮你少走一些弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询