ARTICLE DETAIL

资讯详情

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

ECharts自定义底图实战:SVG转GeoJSON实现大屏可视化

ECharts自定义底图实战:SVG转GeoJSON实现大屏可视化 做可视化大屏这几年我打交道最多的组合就是 ECharts 加地图系列。大部分需求都逃不过一个诉求底图得是我们自己的图纸不是标准行政区划。工业园区、仓储库区、商场楼层、机房平面、地下管廊、停车场布局这类场景用 ECharts 自带的中国地图或者省市 GeoJSON 是完全对不上号的。我最早的做法是把甲方给的 CAD 图导出 PNG然后用 graphic 组件铺一张图片再把散点算好像素坐标硬怼上去。小屏能糊弄一上 4K 大屏就露馅——图片一放大边缘发虚缩放拖拽时点位和底图还会飘。后来改用 SVG 当底图才算把这条链路真正跑顺。这篇就把我踩过的坑、验证过的方案、以及可以直接抄的参数配置一次讲清楚。数据可视化这个圈子里大家聊 ECharts 地图系列时默认谈的是 GeoJSON但很多时候你手里根本没有 GeoJSON只有一张设计稿、一张 CAD、一个 AI 文件。SVG恰好是这些工具都能导出的通用矢量格式把它变成 ECharts 能吃的底图中间那条路就是本文要讲的。它适合做园区级、楼层级、设备级这种自定义空间的底图绘制不适合需要真实经纬度做地理分析的场景这点我会在正文里反复强调免得你选错方向。1. 先搞清楚 ECharts 地图系列的底图是怎么来的1.1 默认地图数据为什么经常不够用ECharts 的 map 系列本质上是把一份 GeoJSON 注册成一张地图然后 series 或 geo 组件按 map 名字去取这份数据来渲染。早期 ECharts 把世界、中国、各省的 GeoJSON 直接打包进主包后来因为体积和边界数据更新的问题官方把这些地图数据从包里拆了出去现在你必须自己准备 GeOJSON 再调echarts.registerMap注册。这一步很多人第一次做地图时都会栽代码里写map: china结果页面一片空白控制台还看不到明显报错因为数据根本没注册。即便你老老实实引入了官方省份数据在真实项目里还是不够用。我总结过三类高频问题。第一类是行政边界和时间对不上某些区域做过调整社区里的 GeoJSON 更新滞后画出来的边界和甲方要求的对不上返工极其难受。第二类是精度过剩一份省市级 GeoJSON 动辄几百 KB 到几 MB大屏首屏加载被它拖累而甲方要展示的只是十几个点位用不上那么细的海岸线。第三类也是最要命的——需求压根不是行政区划而是园区内部的功能分区、厂房轮廓、道路、绿化带这些东西在标准 GeoJSON 里查不到也不该去查。1.2 SVG 当底图到底补上了哪块短板SVG 是矢量图形格式本质是用 XML 描述线条、路径、填充色。它能补上的短板其实非常直接。首先是来源自由AI、Figma、Sketch、CorelDRAW、甚至 CAD 转出的图都能存成 SVG设计同学给你什么你就用什么不用求着 GIS 同事帮你做地理配准。其次是矢量特性路径是数学描述放大到 4K、8K 都不糊而 PNG 底图在 2 倍变焦后就明显发虚。第三是文件体积一张园区线稿 SVG 通常只有几十到几百 KB比动辄几 MB 的精细 GeoJSON 友好得多。第四是样式可控你可以直接改描边颜色、填充透明度来贴合大屏主题不需要重新导图。更关键的一点是SVG 的 path 数据和你想要的区域概念天然契合。园区里的 A 栋厂房就是一段闭合路径停车场就是另一个闭合路径把它们转成 GeoJSON 的 Polygon 之后ECharts 就能像处理省份一样处理这些区域——能 hover、能点选、能根据数据填充不同颜色。这是用图片当底图完全做不到的。1.3 什么场景该上 SVG什么场景别硬上判断标准其实就一条你到底需不需要真实的地理坐标。如果你要做的是区域热力、设备告警、人流统计、管线流向这类相对位置表达SVG 底图完全够用而且比 GeoJSON 更贴合业务。我做过一个地下管廊项目管廊走向是设计院出的图纸根本没有经纬度数据用 SVG 转 GeoJSON 之后每个舱段的温度、湿度、告警状态都能挂到对应区域上效果比硬套地图好得多。反过来如果你要做的是真正的 GIS 分析——两点间实际距离计算、缓冲区分析、路径规划、跨区域的地理聚合——那 SVG 底图帮不了你因为它的坐标是画布坐标不是经纬度。这种情况下老老实实上 ArcGIS 或者带真实坐标的 GeoJSON。另外如果项目要求底图带丰富的建筑纹理、卫星影像、地形晕渲SVG 也不合适那属于栅格底图的领域。我见过有人硬要用 SVG 画三维建筑最后得到一个极其复杂的平面图得不偿失。2. SVG 底图接入 ECharts 的三条技术路线2.1 路线一SVG 转 GeoJSON走标准 registerMap 流程这条路线是最正宗的也是我最终在项目里落地的方案。核心思路是把 SVG 里每个代表区域的闭合 path解析成一系列坐标点组装成 GeoJSON 的 Polygon 或者 MultiPolygon然后echarts.registerMap注册成一张自定义地图后面所有 map series、geo 组件的用法都和标准地图一模一样。它的好处是能用上 ECharts 地图系列的全部能力区域填色、hover 高亮、label、tooltip、区域与数据绑定、visualMap 映射甚至连select选中态都能直接用。代价是转换这一步有工作量。你需要写一个 path 解析器把d属性里的指令翻译成点序列。听起来吓人其实核心逻辑不到一百行代码后面第 4 章我会把脚本贴出来。转换过程里有两个细节必须处理一是 SVG 的坐标系 y 轴向下而 ECharts 地图渲染时 y 轴向上需要做翻转二是每个闭合路径要确保首尾点一致否则填充可能出问题。2.2 路线二用 graphic 组件把 SVG 当图形层叠加如果你只是想在一张底图上放点位不需要区域级别的交互那 graphic 组件更省事。ECharts 5 的 graphic 支持type: image可以直接加载一张 SVG 图片作为背景层然后用type: group组织散点、标签、连接线。这种方式的优势是 SVG 原有样式完完整整保留设计稿什么样大屏上就什么样不需要任何数据转换。但它的问题也很明显。散点坐标你得自己算而且要在 SVG 的 viewBox 坐标系和 ECharts 画布坐标系之间做映射一旦涉及 zoom 和 roam坐标换算会变得很折磨。更麻烦的是graphic 层和 map 层不是一个坐标系你想让某个点和某个区域联动得手动维护两套坐标。我早期做过一版后来因为 roam 拖拽后点位跟不上底图果断放弃了。2.3 路线三图片或 background 兜底最省事的做法是把 SVG 转成高分辨率 PNG通过 series 的type: map配合map: custom不太行更常见的还是 geo 组件的 background 或者直接一个绝对定位的 img 放在图表容器下层。这种方式胜在快半小时能出效果适合原型演示。但它的缺点同样致命图片不能随容器缩放保持清晰点位坐标是写死的像素换个分辨率就全乱而且完全没有区域交互。下面这张表是我对三条路线的横向对比你可以按项目实际情况对号入座。对比维度SVG 转 GeoJSONgraphic 叠加图片兜底实现难度中需要转换脚本中需坐标映射低直接铺图区域级交互完整支持不支持不支持缩放清晰度矢量无损矢量无损依赖图片分辨率与数据绑定直接用 map series手动维护坐标手动维护坐标适配 roam 缩放原生支持需自行换算基本不可用适用场景园区、楼层、管廊简单点位展示快速原型3. 手工做一张能用的 SVG 底图3.1 素材从哪来别一上来就自己画做底图最忌讳的就是从零开始画。园区有规划图楼层有平面图机房有布线图这些都是现成素材。我的常规流程是先找甲方要原始图纸CAD 的 dwg 最好设计稿 ai、figma 也行实在没有就上百度地图截一张做参考描线。拿到 dwg 之后用 Illustrator 或者 Inkscape 打开删掉标注、尺寸线、图框这些不需要的元素只保留区域轮廓然后导出 SVG。Inkscape 是免费的处理 CAD 转出的矢量图非常顺手我一般用它做前期清理。如果连原始图纸都没有只有一张 JPG 效果图那就用 Inkscape 的位图描摹功能把图片转成矢量路径。描摹出来的路径会比较碎需要手动用节点工具合并简化只留下你要做交互的那几个大区域。这个过程有点耐心活但比硬啃 CAD 数据快多了。3.2 viewBox 和坐标系必须先对齐SVG 的viewBox属性决定了它的内部坐标系格式是viewBoxminX minY width height。这个值直接决定了你后续转换出来的坐标范围必须一开始就理清楚。我遇到过最常见的问题是设计同学导出 SVG 时 viewBox 起点不是 0 0比如viewBox120 80 800 600结果转换时没有减掉偏移量地图整体偏出画布。所以拿到 SVG 第一件事就是用文本编辑器打开看根标签上的 width、height 和 viewBox。统一的做法是把 viewBox 规整到0 0 width height这样坐标原点在左上角转换逻辑最干净。规整的方法很简单用 Inkscape 打开后文档属性里把页面尺寸贴合绘图内容重新导出即可。另外要注意 SVG 里的图层g 标签可能带transform属性比如translate、scale这些变换会影响子路径的实际坐标。要么在导出前把变换应用掉Inkscape 里有应用变换操作要么在解析脚本里做矩阵运算显然后者麻烦我更推荐前者。3.3 给底图瘦身把节点数压下来从 CAD 导出的 SVG 路径往往节点极其密集一条厂房轮廓可能有上千个点。这种数据直接转 GeoJSON体积会膨胀得很难看渲染时也可能卡。瘦身的思路有三步。第一步是合并同图层元素把同一种颜色、同一类填充的区域合成一个 group便于批量处理。第二步是简化路径Inkscape 的路径简化或者在线工具都能按容差抽稀节点一般把容差设到 1 到 3 个像素单位肉眼几乎看不出差别节点能砍掉一大半。第三步是去掉冗余属性SVG 里那些style、class、id你如果用不上就删掉导出前用 SVGO 压一遍体积还能再降。这里有个经验值供参考一张 A1 幅面的园区平面图清理前 SVG 大概 1.5 MB节点总数几万个简化加压缩之后能压到 100 到 200 KB节点控制在几千个以内ECharts 渲染毫无压力。这个体量放进大屏首屏完全不用担心加载问题。3.4 导出前的自检清单导出之前我会过一遍这几项任何一项不过关后面转换都要返工。坐标原点是不是 0 0所有需要交互的区域是不是独立闭合路径路径有没有应用掉 transform填充色是不是统一避免同区域出现多个碎块文本元素是不是单独分离出来别混在区域路径里最后确认路径没有用到 SVG 的圆弧A指令如果有要么转成贝塞尔要么在解析脚本里做处理。注意SVG 的path里如果混用了A椭圆弧指令简单解析器很容易算错。稳妥做法是在矢量软件里把圆弧转成曲线Inkscape 的路径转曲线能批量处理。4. SVG 转 GeoJSON 的完整实操4.1 先理解 GeoJSON 的坐标结构ECharts 能识别的 GeoJSON 是标准格式最外层是FeatureCollection每个Feature代表一个区域properties.name是区域名geometry描述形状。对于面状区域geometry 的 type 是Polygoncoordinates 是一个三维数组第一层是环的集合第二层是外环的点序列第三层是每个点的[x, y]。如果区域带孔洞外环后面跟内环。这里的坐标是二维的不需要真实经纬度ECharts 只要求坐标系一致即可。结构大致长这样{ type: FeatureCollection, features: [ { type: Feature, properties: { name: A栋厂房 }, geometry: { type: Polygon, coordinates: [ [[0, 0], [200, 0], [200, 120], [0, 120], [0, 0]] ] } } ] }所以转换脚本要做的事情很清楚从 SVG 的每个 path 里提取点序列塞进 coordinates把 path 的 id 或者自定义属性作为 name。4.2 path 的 d 指令怎么读SVG path 的d属性就是一串绘图指令常见的命令有这些。大写表示绝对坐标小写表示相对上一个点的偏移。M/m是移动到起点L/l是画直线H/h和V/v是水平、垂直直线C/c是三次贝塞尔曲线Q/q是二次贝塞尔Z/z是闭合路径。我们做底图转换多数情况下只需要处理 M、L、H、V、Z因为设计图里的轮廓通常已经转成了折线。如果遇到 C、Q就是把控制点也采样成点密集采样能逼近曲线视觉上几乎无差别。解析的关键点是把相对坐标还原成绝对坐标用一个当前点游标一路累加。同时注意隐式重复比如M0,0 10,10 20,20后面的坐标对是连续 L 的简写很多设计软件导出的路径都这德行不处理会漏点。4.3 一个够用的转换脚本下面这个 Python 脚本我用了很久处理标准折线路径足够。它对 M、L、H、V、Z 做完整支持遇到 C、Q 用密集采样近似输出就是可以直接给 ECharts 用的 GeoJSON。import re import json TOKEN_RE re.compile(r([MLHVCSQTAZmlhvcsqtaz])|(-?\d*\.?\d(?:[eE][-]?\d)?)) def parse_path(d, curve_steps12): tokens TOKEN_RE.findall(d) flat [t[0] if t[0] else t[1] for t in tokens] points [] cur [0.0, 0.0] start [0.0, 0.0] i 0 cmd None while i len(flat): tok flat[i] if re.match(r[A-Za-z], tok): cmd tok i 1 if cmd in Zz: if points and points[0] ! points[-1]: points.append(points[0][:]) cur start[:] continue # 读取坐标根据命令决定参数个数 def num(): nonlocal i v float(flat[i]); i 1; return v if cmd in Mm: x, y num(), num() if cmd m: x cur[0]; y cur[1] cur [x, y]; start [x, y] points.append([round(x, 3), round(y, 3)]) elif cmd in Ll: x, y num(), num() if cmd l: x cur[0]; y cur[1] cur [x, y] points.append([round(x, 3), round(y, 3)]) elif cmd in Hh: x num() if cmd h: x cur[0] cur [x, cur[1]] points.append([round(x, 3), round(cur[1], 3)]) elif cmd in Vv: y num() if cmd v: y cur[1] cur [cur[0], y] points.append([round(cur[0], 3), round(y, 3)]) elif cmd in Cc: p [num() for _ in range(6)] c1 [cur[0] p[0], cur[1] p[1]] if cmd c else p[0:2] c2 [cur[0] p[2], cur[1] p[3]] if cmd c else p[2:4] end [cur[0] p[4], cur[1] p[5]] if cmd c else p[4:6] for s in range(1, curve_steps 1): t s / curve_steps mt 1 - t x mt**3*cur[0] 3*mt*mt*t*c1[0] 3*mt*t*t*c2[0] t**3*end[0] y mt**3*cur[1] 3*mt*mt*t*c1[1] 3*mt*t*t*c2[1] t**3*end[1] points.append([round(x, 3), round(y, 3)]) cur end else: i 1 return points def svg_to_geojson(paths): features [] for name, d, height in paths: pts parse_path(d) if len(pts) 3: continue # SVG y 轴向下翻转以适配 ECharts flipped [[p[0], round(height - p[1], 3)] for p in pts] if flipped[0] ! flipped[-1]: flipped.append(flipped[0][:]) features.append({ type: Feature, properties: {name: name}, geometry: {type: Polygon, coordinates: [flipped]} }) return {type: FeatureCollection, features: features}调用的时候把从 SVG 里提取出的path的id和d属性成对喂进去再传一个 viewBox 的高度做翻转基准输出的 JSON 直接写文件即可。脚本用了非局部变量递增读取 token处理隐式重复坐标靠的是循环里遇到数字就按当前命令继续消费这点是解析器必须做对的地方否则节点会丢。4.4 注册地图并接上数据拿到 GeoJSON 之后前端这边非常轻。先注册再配置 geo 和 series。下面这段配置是我大屏项目里的基础骨架可以直接改成你自己的配色。import * as echarts from echarts; import parkMap from ./park-map.json; echarts.registerMap(park, parkMap); const chart echarts.init(document.getElementById(map)); chart.setOption({ geo: { map: park, roam: true, aspectScale: 1, layoutCenter: [50%, 50%], layoutSize: 92%, itemStyle: { areaColor: #0b2440, borderColor: #3aa0ff, borderWidth: 1 }, emphasis: { itemStyle: { areaColor: #164a7d }, label: { show: true, color: #ffffff, fontSize: 14 } } }, series: [ { name: 区域负荷, type: map, geoIndex: 0, data: [ { name: A栋厂房, value: 320 }, { name: B栋仓库, value: 180 }, { name: C区停车场, value: 96 } ] } ], visualMap: { min: 0, max: 400, left: 20, bottom: 20, text: [高, 低], calculable: true, inRange: { color: [#0b2440, #1c6fb8, #4fc3ff] } } });这段配置里aspectScale: 1是关键后面第 5 章细说。layoutCenter和layoutSize用来居中撑满容器比 left、top、width 的组合更稳。5. 大屏实战中的参数调优5.1 aspectScale 到底要不要设成 1ECharts 的aspectScale默认值是 0.75它的设计初衷是配合地理投影让地图在屏幕上看起来比例正常。但如果你用的是 SVG 转出来的自定义底图这个 0.75 反而会把你的图压扁或者拉长因为你的坐标本来就是按图纸比例 1:1 来的。所以自定义底图场景下aspectScale直接设成 1让 x 和 y 等比例缩放图形才不会变形。我一开始没注意这个参数园区的方形厂房被压成了扁的找了好久才定位到它。5.2 zoom、center 和 layoutCenter 怎么配合控制地图在容器里的位置有两条路子。一条是用zoom加centercenter 是地图坐标系里的一个点会移到容器正中zoom 控制缩放倍数。另一条是用layoutCenter加layoutSize前者是个百分比坐标表示地图布局盒的中心在容器中的位置后者是布局盒的尺寸。我的建议是布局用 layoutCenter 和 layoutSize交互缩放交给 roam 或者 zoom。两套参数混用时容易互相打架尤其是开了 roam 之后再设 center用户拖一下就跳回去了体验很糟。roam我一般设 true但对于纯展示大屏会设成scale只允许缩放不允许平移防止观众误触把图拖出可视区。如果底图是横长条或者竖长条layoutSize 可以设成[90%, 70%]这样的双值分别控制宽高占比比等比缩放灵活。5.3 tooltip 自动换行和 labelLine 的细节大屏上文字长是常态tooltip 默认不换行提示框会横着拉出屏幕外。解决办法是给 tooltip 开自动换行extraCssText里加white-space: normal和word-break: break-all再配合 formatter 里手动插br/双保险。下面这个写法我用了好几年长文案和其他标点混排都不会溢出。tooltip: { trigger: item, confine: true, formatter: function (params) { const v params.value null || isNaN(params.value) ? - : params.value; return params.name br/ 负荷指数 v; }, extraCssText: white-space: normal; word-break: break-all; max-width: 260px; line-height: 20px; }饼图或者地图标签用 labelLine 时末尾小圆点的偏移是个高频问题。ECharts 5 里 labelLine 的length控制第一段引线长度length2控制第二段长度smooth控制是否平滑。小圆点其实跟引线末端位置绑定你调 length2 就能把小圆点往里收或者往外推。如果标签太长顶到边缘用label.alignTo: edge让标签自动贴边配合edgeDistance留出安全距离远比手动调数值靠谱。5.4 自适应和 rem 换算失效的问题大屏适配绕不开 rem用 postcss-pxtorem 把 CSS 里的 px 转成 rem 是常规操作。但很多人发现 ECharts 里的字体、间距怎么都不跟着缩原因很简单ECharts 走的是 canvas 渲染尺寸配置写在 JS 里postcss 只处理 CSS 文件管不到 JS 里的 px。所以你得自己写一个换算函数在 setOption 之前把需要的 px 值转成 rem或者在窗口 resize 时整体重设 option。我的做法是封装一个px2rem工具基准值取设计稿宽度的十分之一所有传给 ECharts 的 fontSize、symbolSize、labelLine 长度都过一遍这个函数。同时监听 resize 事件防抖之后调chart.resize()并重新计算一次 layoutSize 的百分比。这套下来从 1920 到 4K 分辨率切换地图和文字基本不会乱。注意chart.resize()只重算画布尺寸不会重新解析 option 里的 px 配置。字体、间距这类需要自己监听 resize 后重新 setOption否则文字大小不跟随变化。6. 踩坑记录与常见问题速查6.1 地图一片空白怎么查空白是最高频的问题我总结了排查顺序。先看 registerMap 注册的名字和 geo.map、series.map 里写的名字是否完全一致大小写、空格都算。再看 GeoJSON 的坐标系是不是正常如果所有点都是 0 或者天文数字说明解析时相对坐标没还原。接着看数据里的 name 和 GeoJSON 的 properties.name 是否对得上地图能显示但区域不填色基本都是这个原因。最后看一眼容器的宽高ECharts 初始化时容器如果高度是 0图上什么都不会画这是 CSS 没给高度导致的。6.2 图形变形、错位、翻转的解法变形通常两个原因。一个是 aspectScale 没设成 1属于配置问题。另一个是 SVG 的 viewBox 起点不是 0 0转换时没减偏移导致整张图位移。翻转问题就是我脚本里处理的那步SVG 的 y 轴朝下ECharts 朝上不做height - y翻转图形会上下颠倒。还有一种隐蔽的错位是 transform 没应用g 标签上的translate(100, 50)会让子路径整体偏移解析时忽略它就全乱套。6.3 性能卡顿的处理思路自定义底图卡顿一般三个来源。一是 path 节点太多几千个点以上的区域 hover 时会明显掉帧用路径简化抽稀。二是区域数量过多一张图上几百个可交互区块ECharts 的事件绑定会吃力这种要考虑按需渲染或者做区域聚合。三是 roam 缩放时重绘压力大可以设progressive和progressiveThreshold做渐进渲染或者在缩放时临时关掉 label。我做过一个上千区块的管廊图最后是拆成几张小图分层渲染才顺过来。6.4 常见问题速查表现象可能原因处理方式地图完全不显示未注册或 map 名不一致检查 registerMap 与 map 字段显示但不填色data 的 name 与 GeoJSON 不匹配对齐 properties.name图形上下颠倒未做 y 轴翻转转换时用 height 减 y图形被压扁aspectScale 默认 0.75设为 1整体偏移出画布viewBox 起点非 0 0规整 viewBox 或减偏移tooltip 横穿屏幕未允许换行extraCssText 加 white-space normal字号不随屏幕缩放postcss 管不到 JSJS 内自行 px 转 rem缩放后点位飘移graphic 与 map 坐标系不一致改用 GeoJSON 方案交互卡顿路径节点或区域过多简化路径、分层、渐进渲染我在项目里越来越倾向于这样一个组合SVG 转 GeoJSON 做区域层点位和流向单独用一个 scatter 或者 lines series 挂到同一个 geo 坐标系上。这样地图的区域交互和点线表达各司其职改需求时互不影响。转换脚本建议单独建个工具目录存着下次换项目换个 SVG 改改高度参数就能复用比每次重新调坐标划算太多。真正让我省心的不是某一个参数而是把底图从哪来、怎么转、转到什么程度这三件事提前想明白动工的时候才不会来回返工。
返回列表