上个月接了一个数据可视化大屏的需求产品经理在需求评审会上提了一句“后台用户管理模块这里要放一张权限判断流程图用户点击节点要看详情。” 我当时第一反应是找ProcessOn或者draw.io导出图片然后SVG怼进大屏。结果真做起来全是问题图片放大发虚、配色跟大屏主题对不上、点击节点联动右侧面板根本做不到。后来我直接在项目里打开了已经装好的echarts用graph系列花了一个多小时把这张流程图做了出来。说实话ECharts官方并没有“流程图”这个图表类型但它提供的graph关系图、symbol形状、links连线加上自定义坐标布局完全能撑起大多数“只读型流程图”场景。这篇文章把整个实现过程、业务语义建模、交互细节和大屏适配的坑一次性讲清楚适合正在做数据可视化大屏、管理后台流程展示又不想为了一个只读图引入重量级流程框架的团队参考。1. 流程图需求落地时为什么先从echarts而不是bpmn.js说起1.1 这类需求实际长什么样先别急着写代码我们盘一下“用echarts画流程图”这个需求到底长什么样。根据我接触过的项目绝大多数场景跑不出下面几类大屏上的流程概览图比如用户管理模块的登录鉴权流程、图书管理系统的借书还书流程节点数量通常在10到50个之间数据从后端接口动态返回页面只需要静态展示加基础交互。项目文档里的业务架构图软件工程结课报告里的模块流程图、算法课设里的排序流程、数学建模论文里的模型求解流程这类图讲究清晰、规范、能截图放进文档不需要在线编辑。带数据联动的流程演示页点击某个节点旁边面板展示该节点的详细说明、接口耗时、异常次数等指标这是大屏最常见的玩法也是图片方案完全做不到的。这些场景有一个共同特点流程是既定的不是用户临时拖出来的重点是展示状态、数据和交互而不是流程建模本身。那这时候项目里已有的echarts就是最优解之一。1.2 拿ECharts画流程图本质是在用什么能力ECharts确实没有一个叫flowchart的系列但它有几个系列天生适合表达流程关系系列类型适合场景局限graph任意有向/无向关系图节点可指定坐标连线可配置箭头和样式没有内置泳道和BPMN语义需要自己建模tree严格父子关系、层级清晰的树状流程不支持任意连线无法表达分支合并sankey有流量、能量、数量含义的上下游流转节点自动排布无法自定义菱形判断节点专业流程库bpmn.js / LogicFlow / AntV X6可编辑流程图、BPMN标准建模、流程引擎集成依赖重、学习成本高只为展示属于杀鸡用牛刀graph系列里有一个非常关键的配置symbol。它支持rect、roundRect、circle、diamond这些形状而流程图的“开始/结束”“普通步骤”“判断分支”恰好就是靠这些形状来区分的。也就是说ECharts虽然没叫它流程图但底层绘制能力已经够了。我自己判断用不用ECharts就看一条这个流程图是画给人看的还是要让用户动手改的。前者优先ECharts后者直接上LogicFlow或bpmn.js不用犹豫。2. 用graph类型搭建流程图骨架节点、连线与坐标布局2.1 最小可运行示例先给一个最小示例。假设我们要画一个“用户管理模块”的权限判断流程开始 → 输入账号密码 → 校验是否通过 → 通过则进入后台不通过则提示错误 → 记录登录日志 → 结束。import * as echarts from echarts; const chart echarts.init(document.getElementById(flowChart)); const option { tooltip: {}, series: [ { type: graph, layout: none, data: [ { name: 开始, x: 60, y: 200, symbol: roundRect, symbolSize: [80, 36], itemStyle: { color: #91cc75 } }, { name: 输入账号密码, x: 220, y: 200, symbol: roundRect, symbolSize: [120, 36], itemStyle: { color: #5470c6 } }, { name: 校验是否通过, x: 420, y: 200, symbol: diamond, symbolSize: [130, 80], itemStyle: { color: #fac858 } }, { name: 进入后台, x: 600, y: 100, symbol: roundRect, symbolSize: [100, 36], itemStyle: { color: #5470c6 } }, { name: 提示错误, x: 600, y: 320, symbol: roundRect, symbolSize: [100, 36], itemStyle: { color: #ee6666 } }, { name: 记录登录日志, x: 780, y: 200, symbol: roundRect, symbolSize: [120, 36], itemStyle: { color: #5470c6 } }, { name: 结束, x: 960, y: 200, symbol: roundRect, symbolSize: [80, 36], itemStyle: { color: #ee6666 } } ], links: [ { source: 开始, target: 输入账号密码 }, { source: 输入账号密码, target: 校验是否通过 }, { source: 校验是否通过, target: 进入后台 }, { source: 校验是否通过, target: 提示错误 }, { source: 进入后台, target: 记录登录日志 }, { source: 提示错误, target: 记录登录日志 }, { source: 记录登录日志, target: 结束 } ], label: { show: true, position: inside } } ] }; chart.setOption(option);这段代码跑起来你就已经有了一张带箭头、带节点文字、带分支的流程图。注意第layout: none这是整个方案的核心。2.2 为什么必须用layout: none手动指定坐标graph系列默认的布局有两种force力引导布局和circular环形布局。力引导布局用在社交网络、知识图谱这类关系图上很合适节点会自己弹开、收敛但它的位置是算法算出来的不是我们想要的。流程图讲究从左到右、从上到下的阅读顺序你不可能让“开始”节点跑到“结束”节点下面去。所以流程图的正确做法是layout: none然后在每个节点的data里手动指定x和y。这两个坐标是canvas画布里的像素坐标不是百分比。以我上面这段为例节点之间的横向间距设置为160到180像素纵向间距看分支是否需要上下错开。有人会问节点多一点怎么办手工写坐标确实麻烦但流程图的节点数量通常不会太多而且后面我会讲怎么用简单的分层算法自动生成坐标这里先知道原理layout为none时x和y就是节点的绝对绘制位置。2.3 连线的箭头、曲线和分支标记graph系列的连线配置在links数组里每条link通过source和target指向节点名称。默认情况下连线没有箭头需要加两个配置links: [ { source: 校验是否通过, target: 进入后台, lineStyle: { color: #52c41a, width: 2 } }, { source: 校验是否通过, target: 提示错误, lineStyle: { color: #ff4d4f, width: 2, curveness: 0.2 } } ], edgeSymbol: [none, arrow], edgeSymbolSize: [4, 10], lineStyle: { color: #333, width: 1.5 }edgeSymbol数组的第二个元素arrow就是箭头第一个元素none表示连线起点不加符号。edgeSymbolSize控制箭头大小curveness让连线变成曲线用在从判断节点分流出去的两条线上特别合适避免直角交叉。这里的业务语义要自己加绿色的进入后台、红色的提示错误颜色本身就是流程图里的“条件标签”。如果要在连线上直接显示文字可以在link上配置label{ source: 校验是否通过, target: 进入后台, label: { show: true, formatter: 通过, color: #52c41a } }ECharts没有BPMN网关组件但分支条件完全可以用连线颜色加文字标签表达清楚。2.4 坐标从哪来手工计算还是自动分层节点一多手工算坐标就容易乱。我常用的办法是按深度分层把没有入边的节点放在第0层后续节点的层数等于其所有上游节点最大层数加1然后同一层的节点垂直方向均匀排列或水平方向均匀排列。function layoutByDepth(nodes, links) { const depthMap {}; const inDegree {}; nodes.forEach((n) { depthMap[n.name] 0; inDegree[n.name] 0; }); links.forEach((l) { inDegree[l.target] (inDegree[l.target] || 0) 1; }); // 简化版拓扑重复遍历直到所有节点深度稳定 let changed true; let round 0; while (changed round nodes.length) { changed false; links.forEach((l) { const nextDepth depthMap[l.source] 1; if (nextDepth depthMap[l.target]) { depthMap[l.target] nextDepth; changed true; } }); round; } // 同一层节点纵向排列层与层横向拉开 const groups {}; nodes.forEach((n) { const d depthMap[n.name] || 0; (groups[d] groups[d] || []).push(n); }); const COL_GAP 180; const ROW_GAP 100; let maxDepth 0; Object.keys(groups).forEach((d) { maxDepth Math.max(maxDepth, Number(d)); }); nodes.forEach((n) { const d depthMap[n.name] || 0; const list groups[d]; const index list.indexOf(n); n.x 60 d * COL_GAP; n.y 80 index * ROW_GAP - ((list.length - 1) * ROW_GAP) / 2; }); return { maxDepth }; }这个函数是按流程图从左到右画的层数为横坐标同层节点纵向均分。如果流程里有循环回退比如“输入账号密码不通过回到重新输入”这样写会死循环所以我在while循环里加了round nodes.length保护超过节点数量就强制退出保证执行效率。布局算法建议写在数据处理的公共模块里后面无论是做图书管理系统的借书流程还是做数学建模的算法流程图直接传入节点和连线数据就能出坐标。3. 把业务语义画出来判断分支、泳道分组和标签3.1 开始/结束/判断节点的差异化表达流程图里不同形状代表不同语义这是几十年的标准读者已经形成了条件反射。所以颜色可以自由发挥但形状一定要遵循直觉开始节点圆角矩形或者圆形绿色系表示入口。结束节点圆角矩形红色系表示出口。普通处理节点普通矩形或圆角矩形蓝色系。判断节点菱形黄色系表示条件分支。子流程节点标准做法是矩形左右两侧加竖线ECharts没有这个内置形状需要用到自定义path。如果你要严格对齐软件工程流程图标准子流程节点可以用symbol: path://自己画。ECharts的symbol支持SVG path字符串比如一个带左右竖线的矩形const subProcessPath M0,0 L20,0 L20,40 L0,40 Z M24,0 L44,0 L44,40 L24,40 Z;这段path就是两个矩形并排中间留了4像素空隙视觉上模拟出标准子流程符号。不过日常项目里我很少用大多数业务方不区分“子流程”和“普通处理”用颜色区分就够了。3.2 分支汇聚和BPMN网关的区别分支汇聚本质上是多条连线指向同一个节点ECharts自动就会这样画。真正要花心思的是怎么让汇聚看起来是有意的而不是画错了。热词里有人搜“bpmn流程图网关使用”这里必须说清楚BPMN网关是带语义的排他网关表示多选一、并行网关表示所有分支同时执行、包容网关表示满足条件的都执行。ECharts里没有这些逻辑它只能“画得像”不负责“按语义跑”。所以我在做这类图的时候会先问需求方一句话这个流程图是需要逻辑引擎驱动的还是展示用的如果是展示用那判断节点画菱形、分支线画不同颜色标注条件完全OK。如果后面要接流程引擎、要做节点状态流转趁早换bpmn.js别在ECharts上用hack硬撑。3.3 泳道和区域背景的三种做法泳道是流程图里很常见的需求比如“申请人员”“审批人员”“系统”三个泳道。ECharts的graph没有原生泳道但实现起来并不难我试过三种方案方案一用graphic组件画背景矩形。这是我最推荐的方式。graphic: [ { type: rect, left: 30, top: 50, z: 0, shape: { width: 500, height: 120 }, style: { fill: #f6f8fa, stroke: #d9dde3, lineWidth: 1 } }, { type: text, left: 36, top: 56, z: 1, style: { text: 申请人, fill: #333, font: 12px sans-serif } } ]graphic的矩形和文字会在graph节点之前绘制只要把节点的z值调高一点就不会被遮挡。泳道的宽度和位置最好和节点坐标对齐否则节点漂在泳道外会很奇怪。方案二用假节点当背景。往data里塞一个symbol: rect的透明节点把x、y正好放在泳道区域中心symbolSize对应泳道尺寸。这个方案的数据虽然好维护但节点点击事件、高亮逻辑都会误伤不推荐。方案三用多个series叠加。一个graph series画节点另一个graph series画背景矩形节点。这方案能用但配置复杂不如graphic直观。泳道图配合上面的layoutByDepth函数有个问题同一层的节点可能落在不同泳道里。这种情况我的处理是先按业务判断节点属于哪个泳道再在自动布局之后手动修正节点的y坐标让节点落在对应的泳道矩形范围内。3.4 节点标签怎么放才不乱流程图的节点文字是个容易翻车的地方尤其是菱形判断节点空间小文字一多就挤成一团。我总结了几个实用规则判断节点文字不要超过6个字。“校验是否通过”已经是极限了再长就用缩写或拆成两行。label的formatter支持插入换行符label: { show: true, formatter: (params) { const name params.name; if (name.length 6) { return name.slice(0, 3) \n name.slice(3); } return name; } }普通矩形节点文字放在内部菱形节点文字放在右侧或下方。菱形内部空间小position: inside容易溢出改成position: right后观感好很多。长节点名称做截断。有些业务节点叫“用户提交的申请单需要管理员进行二级审批”这种名字直接显示会撑爆节点。我的做法是在formatter里做字符数截断超过10个字符就显示前8个加省略号然后tooltip里展示完整名称。节点标签本身也是一种信息层级不要所有文字都一个字号。开始、结束节点用12px普通节点用12px判断节点用11px大屏上可以整体放大这个后面讲适配时再说。4. 交互体验和写进大屏前的细节处理4.1 tooltip自动换行的正确姿势ECharts的tooltip默认是单行显示的节点描述稍微长一点就会把容器撑得很宽甚至超出大屏边界。网上搜“echarts tooltip自动换行”很多人给出一堆CSS方案我实测下来最稳定的组合是这样tooltip: { trigger: item, confine: true, extraCssText: max-width:220px;white-space:normal;word-break:break-all;, formatter: (params) { const name params.data.name; const desc params.data.description || ; const status params.data.status || 正常; return [ b name /b, 描述 desc, 状态 status ].join(br/); } }confine: true会让tooltip始终在图表容器内不会跑到屏幕外看不见。extraCssText里的white-space: normal允许在HTML标签内换行max-width限制宽度。真正的长文本是后端返回的description最好在formatter里再按字符切一次避免一个超长英文单词撑破布局。function wrapText(text, limit) { const str String(text || ); if (str.length limit) return str; let result ; let line ; for (let i 0; i str.length; i) { line str[i]; if (line.length limit) { result line \n; line ; } } return result line; }这个wrapText既可以用在tooltip formatter里也可以用在节点label formatter里属于画流程图的“万金油工具函数”。4.2 缩放拖拽与移动端适配大屏流程图经常会遇到节点多、画布放不下的情况。graph系列自带roam配置开启后用户可以用鼠标拖动画布、滚轮缩放series: [ { type: graph, roam: true, scaleLimit: { min: 0.6, max: 2 }, draggable: false } ]注意draggable我故意设置成false。很多新手发现开了roam之后节点还能被单独拖动那是因为没配draggable: false。只读流程图里节点被拖乱是很糟糕的体验用户下次刷新前位置就乱了所以除非你是要做可视化编排工具否则别开节点拖拽。移动端适配我踩过一个很深的坑容器是flex布局子元素初始化时宽度还没被撑开图就画完了结果内容只有一半。解决办法是用ResizeObserver监听容器变化宽度变了就调一次chart.resize()const container document.getElementById(flowChart); const ro new ResizeObserver(() { chart.resize(); }); ro.observe(container);页面卸载时记得ro.disconnect()不然会有内存泄漏。4.3 pxtorem对echarts没效果是怎么回事这个坑在Vue3项目里太典型了搜“pxtorem 对echarts没起到效果 vue3”就能看到一大片讨论。根因其实很简单pxtorem是PostCSS插件它只处理CSS文件里的px单位把它编译成rem。而ECharts绘制在canvas里节点文字、tooltip里的字体大小都是渲染器根据配置项里的fontSize按像素画的根本不经过CSS解析。所以你在ECharts的label、tooltip里写fontSize: 12这个12就是canvas里的12像素它不会跟随pxtorem转换。这不叫“没效果”而是“本来就不该由它管”。真正会出问题的是容器本身的尺寸。假设你的容器CSS写的是width: 400pxpxtorem把它编译成25rem如果根字体大小不是16px容器的实际像素宽就会变而ECharts的canvas是通过clientWidth读取容器尺寸的初始化完成后就不会自动跟着变。这就导致容器实际宽度600px但canvas还是400px宽的状态。解决方式就是我上面说的ResizeObserver容器尺寸一变就手动resize()。大屏场景的字体适配我习惯用设计稿比例动态计算const designWidth 1920; const fontSize (size) { return Math.round((size * document.documentElement.clientWidth) / designWidth); };用这个函数生成ECharts配置里的fontSize和symbolSize大屏在不同分辨率下缩放才不会乱。这是纯canvas方案和pxtorem完全两条路。4.4 大屏联动点击节点展示详情大屏上的流程图最出效果的就是点击联动。graph系列的事件处理和普通图表一样用chart.on(click)监听就行chart.on(click, (params) { if (params.dataType ! node) return; // 先清掉上一次的高亮 chart.dispatchAction({ type: downplay }); // 高亮当前节点 chart.dispatchAction({ type: highlight, seriesIndex: 0, dataIndex: params.dataIndex }); // 联动右侧面板params.data.name就是节点名称 detailPanel.update(params.data); });这里有个细节highlight状态和节点颜色是叠加关系如果节点本身设置了itemStyle.color高亮后可能会不明显。建议在高亮时改border和shadow而不是依赖默认高亮样式emphasis: { itemStyle: { borderColor: #fff, borderWidth: 2, shadowBlur: 10, shadowColor: rgba(255,255,255,0.6) } }大屏还有一个隐藏需求自动轮播高亮。用setInterval定时遍历节点dispatchAction模拟出“流程正在执行”的效果。做这个之前一定先确认业务方是否真的需要否则就是一种视觉噪音。5. 真实项目里的性能表现和“要不要换库”的判断5.1 节点数量一多ECharts会怎样很多人担心ECharts画流程图性能不够我用一个后台权限系统的真实数据测过节点数在50到100之间时动画全开完全没问题500个节点时关闭动画后基本流畅超过1000个节点tooltip频繁触发会明显掉帧。节点规模建议配置体验50以下正常开启动画流畅50-200动画保留tooltip简单化流畅200-500animation: false减少自定义path基本流畅500以上关闭动画、简化样式、考虑分页展示体感明显下降1000个节点的流程图本身就不符合人的阅读习惯——你根本看不过来。所以与其优化ECharts不如先优化信息架构把流程拆成子流程点进去再展开细节大屏上永远只显示主干流程。如果确实有几千节点要展示我建议换renderer: svg模式试一下。SVG模式对直线、曲线的渲染更清晰而且能直接导出高清图适合放进论文和文档。但SVG模式下节点数多了反而更卡因为每个节点都是一个DOM。大屏这种长时间播放的场景老老实实用canvas。5.2 什么时候应该换bpmn.js、LogicFlow、AntV X6这个判断越早做越好不然代码写到一半才发现走错路是最难受的。我给自己定了三条标准第一流程图要可编辑。用户需要拖拽节点、手动连线、删除分支这直接绕过ECharts用LogicFlow或AntV X6。ECharts的draggable只能拖动位置不能建立连接关系硬做等于造轮子。第二要对接BPMN标准或流程引擎。bpmn.js是这方面的标准答案它内置了事件、网关、任务、泳道这些BPMN元素导出的xml文件可以被流程引擎解析执行。ECharts画的菱形判断节点没有语义无法导入引擎。第三需要节点表单、连线校验、审批流转这类业务能力。LogicFlow在拓扑图编辑和React/Vue生态融合上做得不错AntV X6在思维导图、DAG图、ER图等图形场景也很成熟。一句话总结我的选型逻辑做展示选ECharts做建模工具选专业流程库。两者之间没有谁替代谁只有合不合适。5.3 我在实际项目里踩过的坑和留下来的习惯最后分享几个每次画流程图都会用到的习惯都是拿真实项目的教训换来的。第一个习惯节点坐标永远从数据层生成不手写进option。哪怕只有10个节点也把节点数组、连线数组单独抽出来坐标用布局函数生成。因为需求方大概率会改流程你手改一个坐标很快但二十个节点改起来就是灾难。第二个习惯tooltip formatter里不做重活。有一次我在formatter里循环了几百个数组项去拼接字符串鼠标移上去卡到爆。后来把所有需要展示的数据预先处理成字符串formatter只负责取字段和拼接HTML性能立刻正常了。第三个习惯大屏的图表颜色统一走主题变量。流程图的“通过”和“不通过”跟大屏里其他图表的成功/失败状态必须是同一套色系不要这边用绿色、那边用青色。我踩过配色不统一的坑被视觉同事追着改了半个下午。第四个习惯每次画完图都跑一遍导出功能。大屏方案评审经常要截图chart.getDataURL()导出的图片如果背景不是透明在大屏深色背景上会突兀。导出前记得设置backgroundColor: transparent或者干脆留一个专门的导出函数。流程图用ECharts画不神秘也不复杂。它解决的是“展示型流程”这个够大的真实需求难点从来不在绘图本身而在数据建模、布局计算和交互细节这些不起眼的地方。希望这篇文章能帮你在下一次遇到“用echarts绘制流程图”这个需求时少走几步弯路。