ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用ECharts绘制工程断面图:坐标轴、填挖方与交互实战

用ECharts绘制工程断面图:坐标轴、填挖方与交互实战 做了这几年工程可视化被问得最多的一个问题就是断面图能不能用 ECharts 画公路纵断面、河道疏浚断面、管线纵断面、地勘钻孔柱状图这些在工程软件里司空见惯的东西一旦要搬到 Web 端很多人的第一反应是找 GIS 库或者专业绘图引擎。但实际做下来你会发现ECharts 完全能扛住这类需求而且对接交互、图表联动、样式定制都非常顺手。这篇文章就是把我在实际项目中用 ECharts 实现断面图的完整思路和踩坑记录整理出来给同样有这类需求的朋友做个参考。1. 断面图在工程里到底是什么为什么选 ECharts1.1 断面图的本质不是画曲线而是还原空间比例断面图说白了就是沿着某条线“切一刀”把切开后在垂直方向上的起伏、结构、层次画出来。道路设计里它叫纵断面图横坐标是里程桩号纵坐标是高程河道疏浚里它反映河底地形和设计开挖线地质勘察里它又是钻孔的分层柱状图表达不同土层的厚度和分布。很多新手第一次拿到需求会觉得这不就是个折线图吗两条线一叠就完事了。真做起来才发现完全不是一回事。断面图最核心的一个要求是空间比例必须真实。横轴上相邻两个点的间距必须真实反映它们在实际工程中的水平距离不能因为 ECharts 的类目轴会均匀排布而把 10 米和 100 米的间距画成一样宽。这直接决定了坐标轴类型的选择。另外断面图通常不是单一数据至少两条线起步原地面线和设计线。两条线之间的夹层区域就是填方区或挖方区这是工程量的核心关注点。更复杂的还要叠加地下水位线、地层分界线、特征桩号标注、坡度标注。信息是分层的不是一个 series 就能搞定。1.2 为什么不用专业绘图库反而选 ECharts有朋友会问画断面图用 CAD 或者专业岩土软件不香吗香但它们解决的是“出图”问题不是“上 Web”的问题。工程报告系统、BIM 协同平台、在线审图工具都需要在浏览器里展示断面并且要求鼠标悬停能看详细数据、拖拽能缩放、还能跟平面图联动。这时候专业绘图引擎又显得太重学习成本和包体积都扛不住。ECharts 的优势在于它把交互、坐标轴、图例、tooltip、缩放全都替你封装好了你只需要专注在数据组织和 series 配置上。而且它的生态足够成熟遇到问题随便一搜都有答案。我用它做过道路纵断面、管线横断面、地勘柱状图整体体验是只要绕开几个关键的坑ECharts 画断面图是完全够用的。下面就从坐标轴和数据组织开始讲。2. 坐标轴和数据组织先把“形”搞对2.1 axis.type 怎么选value 轴才是断面图的正解ECharts 里坐标轴类型主要有 category、value、time 三种。画断面图横轴里程轴必须用type: value这是一个原则性问题。用 category 轴的人通常是觉得“里程桩号嘛不就是一串标签吗”。但 category 轴的刻度是均匀分布的不管两个测点之间实际距离是 5 米还是 50 米在图上都会占同样的宽度。这样一来真实的地形起伏会被彻底扭曲明明是一段陡坡画出来变成了缓坡明明是一段长缓坡画出来可能变成了陡坎。做工程的人对断面图的第一要求是“按比例”category 轴天生做不到这一点。value 轴则完全按数值大小来映射坐标两点间距是 5 米就画 5 个单位的宽度间距 50 米就画 50 个单位的宽度比例关系天然正确。我一般这样配置xAxis: { type: value, name: 里程(m), scale: true, min: 0, max: 320 }再看纵轴高程轴同样用 value 轴但有个细节默认情况下 ECharts 会从 0 开始画如果断面高程大致在 50 米左右0 到 50 米的空白段会占到图面的一半以上地形起伏就被压扁到几乎看不见。解决方案就是把scale设为true让纵轴从数据最小值附近开始。yAxis: { type: value, name: 高程(m), scale: true }这是我做断面图最先学会的一招很多人画出来“一条平线”其实不是数据没有起伏而是没开 scale把起伏压没了。2.2 数据变成什么格式点对数组和里程编排数据组织的核心是把每个测点整理成[里程, 高程]的坐标对。ECharts 的 line 系列原生支持这种[x, y]数据格式用在 value 轴上正好一一对应。const groundData [ [0, 52.1], [30, 53.4], [65, 51.8], [100, 50.9], [120, 52.2], [160, 53.6], [200, 54.1], [250, 53.2], [300, 51.7] ];这里要特别提醒一点数据必须按里程从小到大排序。ECharts 虽然不会因此报错但线条会来回折返断面图就乱成麻花了。如果原始数据来源是测绘仪器或者 Excel 表格导入前最好先做一步排序这种小坑我在项目里见过不止一次。另外同一对测点不同专业的数据往往不是“对齐”的。比如地面线测了 9 个点设计线的变坡点桩号可能只有 6 个。这完全没关系value 轴不要求两条线的 x 坐标一一对应ECharts 会自动在点之间做连线插值。这也是用 value 轴的另一个隐藏优势。2.3 多信息叠加原地面线、设计线、水位线怎么组织断面图的信息是分层组织的。我常用的方案是每个数据层一个 series通过name字段和legend联动原地面线实测数据实线带半透明面积填充设计线理论数据虚线不加填充地下水位线实测或勘察数据点划线特征桩号、填挖分界点用markPoint或markLine标注series 之间不冲突颜色、线型、粗细都能单独控制。比较麻烦的是填挖方区域ECharts 没有现成的“填充两条线之间”的能力这是整个实现里最需要动脑子的地方下一节我详细说。3. 完整实操道路纵断面图从数据到成图3.1 搭建最小可运行配置验证一个断面图方案是否成立我习惯先从最小配置跑起来确认坐标轴和数据没问题再加交互和样式。下面的代码是在一个已有 DOM 容器container上初始化 ECharts 的最小配置const chart echarts.init(document.getElementById(container)); const option { tooltip: { trigger: axis, formatter: function(params) { let res 里程 Math.round(params[0].value[0]) mbr/; params.forEach(function(item) { res item.marker item.seriesName item.value[1].toFixed(2) mbr/; }); return res; } }, legend: { data: [原地面线, 设计线] }, grid: { left: 50, right: 30, top: 60, bottom: 40 }, xAxis: { type: value, name: 里程(m), scale: true }, yAxis: { type: value, name: 高程(m), scale: true }, series: [ { name: 原地面线, type: line, data: groundData, symbol: circle, symbolSize: 4, lineStyle: { width: 2, color: #5470c6 }, areaStyle: { color: rgba(84, 112, 198, 0.12) } }, { name: 设计线, type: line, data: designData, symbol: none, lineStyle: { width: 2, type: dashed, color: #c2574a } } ] }; chart.setOption(option);这个配置跑起来你已经得到一张基本能看的纵断面草图。tooltip 的 formatter 里params[0].value[0]是 x 轴里程value[1]是 y 轴高程这样悬停时能同时看到两套数据。3.2 填挖方区域两种常见做法的取舍接下来是断面图的高频需求把设计线和原地面线之间的区域标出来区分填方和挖方。常见做法有两种我用下来各有适用场景。第一种是markArea分段标记。手动把整条里程分成若干个区间每个区间标记为“填方”或者“挖方”。优点是代码量小视觉直观缺点是需要提前算好填挖分界点而且markArea默认是矩形区域会覆盖整个 y 轴高度视觉上其实是一个半透明色带。真正落项目的时候这个色带带一点透明度叠在图下面倒也不算难看。series.push({ name: 填挖区域, type: line, data: [], markArea: { silent: true, data: [ [ { name: 挖方, coord: [0, 0], itemStyle: { color: rgba(194, 87, 74, 0.18) } }, { coord: [65, 0] } ], [ { name: 填方, coord: [65, 0], itemStyle: { color: rgba(46, 146, 89, 0.18) } }, { coord: [200, 0] } ] ] } });第二种做法是自己计算填挖区域的多边形边界然后用一个独立的填充系列画在图层里。逻辑是先求设计线与地面线的交点再生成“地面线→设计线→地面线”的闭合多边形填方和挖方各画一个填充区域。这种做法效果最接近专业出图但代码要多不少而且要写求交函数。我个人的建议是工期紧、区间固定、交点明确的场景先用 markArea 顶上去如果是要做工程量提取或者出正式图就必须走多边形计算路线。很多项目后来都逃不过“算填挖面积”这一步早做早省心。3.3 特征桩号与关键点标注道路纵断面图少不了桩号标注比如起终点、变坡点、桥涵位置。这个用markLine和markPoint就能搞定。比如在 K0065 位置标注一个变坡点series[1].markPoint { data: [ { coord: [65, 51.6], name: 变坡点 } ], symbol: pin, symbolSize: 30, label: { show: true, formatter: K0065, position: top } };markPoint的coord和 series 数据一样也是[x, y]的形式只要 x 坐标落在 value 轴范围内就能显示。用下来有个经验桩号文字如果直接写在name里悬停提示会带上完整前缀比较啰嗦我一般把name作为点位的说明另用label.formatter控制图面上显示的文字两者互不干扰。3.4 填挖方工程量估算的补充思路前面提到求交和多边形这里补充一个可以直接用的简化思路。填挖面积本质上是两条折线围成的曲边多边形面积。如果不引入第三方几何库可以用数值积分近似在所有测点的里程区间上做梯形面积累加超过某个阈值再分段计算两条线差值。function computeFillCutArea(ground, design, type) { let area 0; for (let i 1; i ground.length; i) { // 当前区间两端差值 const d1 design[i - 1][1] - ground[i - 1][1]; const d2 design[i][1] - ground[i][1]; if ((d1 0 d2 0) || (d1 0 d2 0)) { if (type fill d1 0) { area ((d1 d2) / 2) * (ground[i][0] - ground[i - 1][0]); } else if (type cut d1 0) { area ((Math.abs(d1) Math.abs(d2)) / 2) * (ground[i][0] - ground[i - 1][0]); } } else { // 存在交点需要分段拆开这里简化为均值近似 const mid Math.abs(d1) / (Math.abs(d1) Math.abs(d2)); const xMid ground[i - 1][0] (ground[i][0] - ground[i - 1][0]) * mid; if (type fill d1 0) { area (Math.abs(d1) / 2) * (xMid - ground[i - 1][0]); } if (type fill d2 0) { area (Math.abs(d2) / 2) * (ground[i][0] - xMid); } if (type cut d1 0) { area (Math.abs(d1) / 2) * (xMid - ground[i - 1][0]); } if (type cut d2 0) { area (Math.abs(d2) / 2) * (ground[i][0] - xMid); } } } return area; }这套梯形法在测点密集的时候精度完全够用。配合前面说的填挖多边形填充出来的图既能看又能计算算是性价比很高的组合。3.5 加交互缩放、图例、数据区域选择断面图放到 Web 上最大的优势就是能和平面图、属性面板联动。我的标配交互是dataZoom加legend。dataZoom: [ { type: inside, xAxisIndex: 0, start: 0, end: 100 }, { type: slider, xAxisIndex: 0, start: 0, end: 100, height: 20, bottom: 10 } ]inside缩放支持滚轮操作适合桌面端slider滑块适合移动端触屏两者叠加用户体验最好。缩放时要注意纵轴不要跟着缩放变化否则地形起伏会被不断拉伸建议给 yAxis 固定一个合理的min/max或者保持scale: true让 ECharts 自己维护比例但不要同时缩放两根轴。4. 进阶地质分层断面图的实现思路4.1 用堆积柱状图模拟地层分层道路断面画完后很多项目会继续提“地质断面”需求。这时数据结构完全变了横轴不再是连续里程而是一排钻孔编号纵轴不再是高程而是自孔口向下的深度或者分层厚度。专业软件里通常画成钻孔柱状图ECharts 里最顺手的方案是堆积柱状图。思路是这样把每个钻孔视为一根柱子每一层土作为一个 stack 系列厚度作为柱子的数值层层堆叠。因为地层是不连续的某个钻孔可能缺失某一层缺失层的数据填 0 就行。const holes [ZK1, ZK2, ZK3, ZK4]; const layers [ { name: 素填土, color: #f4cba2, values: [2.5, 1.8, 0, 2.0] }, { name: 粉质黏土, color: #c4a265, values: [4.5, 5.2, 6.1, 4.0] }, { name: 中砂, color: #e6d37e, values: [6.0, 5.5, 4.2, 7.0] } ]; const series layers.map(layer ({ name: layer.name, type: bar, stack: total, barWidth: 40, data: layer.values, itemStyle: { color: layer.color } })); const option { xAxis: { type: category, data: holes, name: 钻孔编号 }, yAxis: { type: value, name: 深度(m), inverse: true }, series: series };4.2 关于纵轴方向和孔口高程的细节注意上面代码里yAxis: { inverse: true }。地质柱状图习惯上深度从上往下增加所以纵轴要翻转。如果你拿到的是分层底界高程而不是厚度处理上要稍微绕一下先把各层厚度算出来再堆叠。实际操作中我还会在每个钻孔顶部叠加一个透明的“孔口高程”系列把地下水位线用一条线叠加在柱状图之上这样一张图就同时表达了“深度”和“高程”两套信息。不过这种叠加对数据要求较高如果只是汇报展示深度的堆积柱状图已经足够清晰。4.3 地层缺失和尖灭的处理地质数据里“某层在某孔缺失”太常见了。前面说的填 0 是一种办法但填 0 后两根柱子之间会出现肉眼可见的断层这在图上是合理的因为地层尖灭了。但如果连续多个钻孔都缺失同一层legend 里就会有一层一直显示 0看着很误导。我的经验是给缺失钻孔的数据填一个极小的值如 0.01然后在 tooltip 的 formatter 里把这层判断成“缺失”而不是显示 0.01m 这种怪数字。这样柱子依旧连续堆叠不会出现莫名其妙的大缝数据含义又表达准确。5. 常见问题与排查技巧实录5.1 图表闪烁前端最常见的“薛定谔 bug”“ECharts 闪烁”是搜索频率极高的话题结合我自己的经历闪烁原因基本绕不开下面几类对照排查效率最高原因表现解决方案重复初始化实例页面反复跳动、闪烁每次创建前echarts.dispose旧实例或者复用初始化过的实例resize 被高频触发窗口拖拽时抖动对window.resize做 debounce200ms 防抖后再chart.resize()setOption 默认合并动画数据更新时旧图闪一下再变新图更新时用chart.setOption(option, { notMerge: true })动画重复播放每次更新都从零播放动画关闭动画animation: false或更新时animationDurationUpdate: 0容器尺寸为 0图表初始化时容器被隐藏画布高度为 0容器显示后再初始化或初始化后调用一次chart.resize()数据和配置未深拷贝同一个对象被复用旧图配置被意外污染用工具函数深拷贝配置对象或者每次单独生成 option这里重点说下resize这个坑。做单页面应用的时候侧边栏折叠、菜单展开、路由切换都可能导致容器尺寸变化如果每次都直接调用chart.resize()很容易触发多次重绘。我的标准写法是let resizeTimer null; window.addEventListener(resize, function() { if (resizeTimer) { clearTimeout(resizeTimer); } resizeTimer setTimeout(function() { chart.resize(); }, 200); });如果是实时刷新数据导致闪烁比如每 3 秒从后端拉一次数据问题的关键反而在动画上。连续高频更新还开着默认动画图表每一帧都在过渡看起来就像鳞片一样闪。这种情况直接关掉动画对性能提升立竿见影。5.2 数据量大、文字多导致渲染卡顿现在前端页面往往不止一个图表旁边可能还有一长串用 markdown-it 渲染的说明文档、表格、报告正文。ECharts 图表本身是 canvas 绘制和 markdown-it 的 DOM 渲染相互影响不大但同页面同时执行大量 DOM 操作和 canvas 重绘时浏览器主线程就会吃紧表现是两个区域轮流卡顿。针对 ECharts 本身的大数据量渲染我的配置习惯是series: [{ type: line, sampling: lttb, progressive: 5000, animation: false }]sampling: lttb用 LTTB 算法对线条降采样在保形的前提下大幅减少绘制点progressive开启渐进渲染数据多时不会一次性把几千个点全部塞进绘图流程。针对页面同时渲染大量 markdown 文本的情况我的建议是把长文档拆开渲染或者用requestIdleCallback分片处理别把一串几十千字的文本一次性innerHTML塞进页面。这个思路跟 ECharts 的 progressive 其实是相通的把大任务拆成小片让浏览器有喘息的机会。5.3 tooltip 和 label 太长怎么办断面图的 tooltip 如果一股脑塞太多信息比如每个点都显示几层地层、多种属性弹窗宽得能盖住半个图。我是这样处理的tooltip 里只保留核心字段里程、原地面高程、设计高程、填挖值大量文本放入点击事件后的详情面板而不是塞在 tooltip 里axisLabel使用interval和rotate避免坐标数字重叠axisLabel: { interval: 2, rotate: 45 }如果实在要在 label 上显示很多文字比如桩号加地名我的做法是formatter里只显示桩号地名放到 tooltip 的name里面。别在一行里塞超过十几个字符否则不仅渲染慢视觉上也是一团乱麻。5.4 坐标轴数值对不上、比例失真最后补充一个经常被忽略但很影响专业度的细节纵轴的实际刻度和数据范围对不上。很多人画完断面图发现图面上高差很大旁边的数字却只有轻微变化这是因为默认情况下 ECharts 会在scale: false时强制从 0 开始把真实起伏“压缩”了。所以我一再强调yAxis.scale: true。反过来如果某个断面高差只有十几厘米scale: true也会把这个微小起伏放大得看起来很夸张。这其实是断面图的“相对比例”问题工程上是允许的但汇报时要清楚标注高程基准。我的办法是在图上加一条基准线markLine: { silent: true, symbol: none, label: { formatter: 基准 50.00m }, data: [{ yAxis: 50 }], lineStyle: { type: dotted, color: #999 } }写在最后实际做下来ECharts 画断面图最大的门槛反而不是配置语法而是你有没有把工程语义想清楚坐标轴是否真实反映距离比例、多条线的叠加顺序是否合理、填挖区域怎么计算、大数据量怎么保性能。我自己的体会是先花半小时把数据和坐标轴理清楚比闷头调样式管用得多。上面这些配置和坑都是我拿真实项目数据一步步试出来的希望能帮你少踩几个。最后再分享一个小技巧做任何复杂图表之前先手动把数据画到方格纸上或者用一个最简单的散点图看一遍分布很多时候问题在数据阶段就暴露了根本轮不到调 ECharts。
返回列表