把散落各市的物料流向、产量数据和重点园区位置揉进一张四川省地图,是很多工业、物流、环保类项目都会遇到的需求。ECharts 里地图 + 物质流 + 个性化标注的组合,听起来像技术大杂烩,实际拆开并不复杂,核心就是“地图怎么铺、流向怎么画、标签怎么放”三件事。这篇文章会把我自己在类似项目里的完整实现思路、配置文件、踩坑点全部过一遍,适合那些已经有 ECharts 基础、想快速做出带业务含义区域可视化的人。你不需要懂复杂的 WebGIS,一个 page 一个组件就能跑起来。
1. 需求拆解:地图、物质流、标注分别是什么
1.1 从标题反推业务场景
“四川省地图+物质流+个性化标注”这个标题,背后对应的通常是一类区域物质流动分析系统。比如某公司有几条生产线,原材料从上游矿区进到中游加工厂,半成品再流转到下游园区,最终产品运到各个消费地。这类项目要展示的,不是简单的“某个城市某个指标大”,而是“哪个点产出、哪个点消耗、东西往哪流动、流量有多大”。信息密度高,单一柱状图或折线图根本装不下。
这种情况下,地图承担的是空间底图角色,物质流承担的是“动态关系表达”角色,个性化标注则负责把地图上看不出来的业务属性补全——比如某个节点的产能、某个标志性建筑、某个运输通道的容量上限。三种信息叠在同一个二维平面上,必须有主次,否则会糊成一团。
1.2 方案选型:为什么选 ECharts 而不是 WebGIS 引擎
在动手之前,我先对比过几个方向。用 WebGIS 类引擎做,最大的问题是地图底图、投影、坐标转换等概念太多,项目周期短的时候根本来不及调。另一个常见方案是用 SVG 手绘路径,说实话,如果你是资深美工,效果会很好,但执行成本偏高,而且后续想接真实数据还得自己维护拓扑关系。
ECharts 的好处在于,它已经把“地图渲染”和“数据可视化”两件事揉在了一套配置体系里。你只要拿到四川省的 GeoJSON,注册成地图,然后按需求往 series 里塞数据就行。地图样式基于 series 的视觉通道控制,流向用 lines 或 scatter 都能处理,自定义标注可以用 markPoint、graphic 甚至自定义系列的 label 全套搞定。单页就能完成,也不需要额外引入地图服务,离线可用。
1.3 环境准备与注意事项
我这次的实现环境是 Vite + Vue 3 + ECharts 5,但核心代码不依赖框架,拿原生 HTML 也完全能跑。版本上建议 ECharts 5.x 以上,早期版本对地图注册方式和 label 的配置项有区别,旧项目升级时记得看迁移文档。
有个比较容易忽略的细节:四川省级地图里包含了很多市县边界,GeoJSON 文件体积通常不小。如果项目对首屏速度有要求,建议用压缩后的拓扑 JSON,不要直接塞原始的未合并边界数据。还要注意,如果前端会做按市下钻,省 JSON 和市 JSON 的坐标系必须一致,最好都从同一来源获取,避免不同数据源之间边界对不上。
2. 数据准备:三套数据的建模与关联
2.1 四川省地图 GeoJSON 的获取与裁剪
先在公开地图数据仓库找一份四川省省界 GeoJSON。以我的经验,这一步的关键不是找不找得到,而是拿到的文件合不合规。常见问题是:要素结构里只有 polygon 没有 center 坐标;或者属性字段名全是英文缩写,你根本不知道哪个字段对应地市名。为了后面做 map 注册和 label 匹配,最好先跑一个脚本清洗一遍:
// 将 GeoJSON 里 properties 字段统一为 name const geojson = fetchedJson; geojson.features = geojson.features.map(f => ({ ...f, properties: { name: f.properties.name || f.properties.NAME || f.properties.cityName } }));清洗后,一定要在 ECharts 里先空载跑一次,确认各市没有缺失、没有飞地、没有重叠多边形。尤其要注意甘孜、阿坝这类范围较大的地区,边界精细度对物质流路径的效果有直接影响。如果发现某个边界明显变形,尽量回到数据源重新取,不要自己在线上拉点,省下后面一大堆看错位置的时间。
2.2 物质流数据建模:从 Excel 到 JSON
物质流数据的来源五花八门,有数据库导出、有 Excel 整理、甚至还有一堆纸质台账。我最常用的是把这些数据统一整理成下面的 JSON 结构:
{ "nodes": [ { "id": "site_A", "name": "某原料基地", "city": "某市A", "category": "供应", "value": 120 }, { "id": "site_B", "name": "某加工厂", "city": "某市B", "category": "中转", "value": 80 } ], "links": [ { "from": "site_A", "to": "site_B", "value": 45 }, { "from": "site_B", "to": "site_C", "value": 30 } ] }这里有三个细节要提前想好。第一,id 不要用中文,尽量用稳定编码,后面做流量映射时会方便很多。第二,site 的城市字段要跟地图 GeoJSON 的 name 完全一致,比如“某市A”,如果地图里叫“A市”,你就要么改数据,要么改地图属性,统一成一份映射表。第三,value 字段可以单纯指物质重量,也可以拆成“物资量 + 运输车次”,我会尽量保留两个维度,方便后面同时用粗细和颜色表达。
2.3 个性化标注数据的字段设计
标注这块最容易在后期返工,因为需求方往往在第一版上线后才说“我还想标一下这个厂房的坐标”。所以,我给标注数据的字段留得比较宽松:
{ "id": "mark_01", "lat": 30.67, "lng": 104.06, "name": "核心枢纽A", "type": "major", "summary": "年处理能力200万吨,辐射周边三市" }lat 和 lng 用来定位,type 控制样式,summary 用来做 hover 或侧边栏。不要想着把所有展示性文字塞进 name,ECharts 的地图标签在字多的时候会互相遮挡,尽量把它拆成“主标题 + 详情”两层。另外,标注点不要太多,通常一张全屏地图上超过 20 个高亮标注就已经接近视觉极限了,宁可做“点击加载更多”,也不要一次铺满。
3. 地图页面的核心实现
3.1 ECharts 地图注册的两种方式和差别
ECharts 里注册地图有两种常见路径。老写法是echarts.registerMap('sichuan', geoJson),然后series-map里直接指定 map。新写法还有一种是直接把 GeoJSON 塞给geo的map属性。两者用起来看起来差不多,但要注意,geo组件是专门做地理坐标系底图的,它自己不能直接展示数据系列,必须配合series-map或者scatter把数据放在 geo 坐标之上。
我这次的方案是:用geo做底图控制整体视觉,各种物质流 series 通过coordinateSystem: 'geo'隐射到底图上。这个组合灵活度高,既能保留区域边界,又能在上方铺线、铺点、铺自定义图形。如果用series-map单独做,区域样式和 series 数据会耦合得更紧,适合更简单的地图填色场景。
const chart = echarts.init(document.getElementById('map')); echarts.registerMap('sichuan', sichuanGeoJson); chart.setOption({ geo: { map: 'sichuan', roam: true, zoom: 1.2, itemStyle: { areaColor: '#e8e8e8', borderColor: '#999' } }, series: [] });3.2 geo 配置里的关键参数
roam设成 true 之后,地图就可以缩放和平移,这在看物质流细节时几乎是必须的。但缩放之后,标注和飞线的粗细很容易显得比例失调,所以后面我会专门讲 scale 联动问题。
zoom初始值取决于地图数据最后的显示范围,不同来源的 GeoJSON 宽高比不一样,有的从全国裁出来的,四川会显得很扁;有的专门处理过,比例接近视觉预期。我一般先把 zoom 调成 1,看着不行再整 0.1 步进微调,不要一次给太多。
itemStyle里areaColor是底图区域色,borderColor是市界颜色,borderWidth默认 1。如果你想强调某个重点区域,可以在regions配置里单独对某个市设置样式。这个功能很有用,比如把重点产业园区所在市高亮为浅蓝色,其他市保持灰色,视觉上立刻有了层级。
3.3 series 里 map 与 geo 的联动坑
很多第一次写 geo + series 的朋友会遇到一个问题:series 的 type 明明写了 map,配置也放好了,图却渲染不出来了。多半是没搞懂坐标系。series-map默认是独立坐标系,它跟geo是两条路。要让区域数据和 geo 联动,更稳妥的实际是直接用geo配合series的geoIndex,但对于地图填色需求,最直接的做法还是把区域数据做成一个 series-map 并设置geoIndex: 0,这样能利用 geo 的地图边界去展示数据。
series: [{ type: 'map', geoIndex: 0, data: mapData.map(d => ({ name: d.city, value: d.value })), label: { show: true } }]但需要注意,如果同时存在一个 series-map 和一堆散点、线,series 的层级顺序、zlevel 都需要安排好。我通常把区域填色放在最底层,飞线和散点放中间层,标注放最上层,这样才不会出现散点被区域遮盖的问题。
4. 物质流可视化的排版与呈现
4.1 物质流的“节点+连线”表达
物质流在地图上的表达,我分成两个部分:节点和连线。节点是物质产生、中转、消纳的地点,用 scatter 加不同颜色的 symbol 表达;连线是节点之间的物质传输,用 lines 系列表达。这要比单用一个桑基图直观,因为地图本身提供了空间参照。
节点数据先转换成经纬度坐标。怎么转?如果你的数据只有城市名,可以先用一个映射表查城市中心点坐标,再把偏置量加到对应节点上。比如多个节点都在同市,为防止重叠,我会在中心点基础上做小范围随机偏移,并保证偏移范围不超过市级行政边界,否则节点会飘到隔壁市去。
连线部分核心配置如下:
series.push({ name: '物质流', type: 'lines', coordinateSystem: 'geo', zlevel: 5, effect: { show: true, period: 6, trailLength: 0.4, symbol: 'arrow', symbolSize: 6 }, lineStyle: { color: '#ff9800', width: 2, opacity: 0.8, curveness: 0.3 }, data: linkData });4.2 折线曲率、流向箭头与动画节奏的调校
curveness是控制线条弯曲程度的关键参数。0 是直线,1 是接近半圆,0.2 到 0.4 是相对自然的弧线。如果两个节点距离很近,curveness 设太大,线条容易反向绕一圈;如果跨市距离很远,curveness 设太小,线条偏直又不够优美。动态看物质流还要靠effect。把 effect.symbol 设为 arrow,效果上会有一个小箭头沿路径流动,material movement 方向很直观。period影响动画周期,数值越小流得越快,但太快会让人觉得烦躁,我常用 5 到 8 秒一轮。
arrow 效果还有一个细节:它沿着路径跑,但不会改变路径本身粗细。如果想表达“流量越大线条越粗”,就要在 data item 里单独指定 lineStyle.width。我建议按 value 的映射来,基础宽度 1,最大宽度 6,不要无上限放大,否则地区之间会出现夸张粗线把地图都遮住。
4.3 利用颜色和宽度表达物质类别与流量
需求里如果同时有“煤炭流向”和“水泥流向”两种物质,同一个 lines 系列会很难分清。更清晰的做法是每种物质一个 lines series,用lineStyle.color区分,同时配合图例(legend)实现显隐。每种物质内部再按流量映射宽度。
这里我踩过一个坑:如果一类物质有 20 条线,每条的 value 差异很大,直接线性映射会导致小流量线条看不见。我一般先做对数变换或分位数分段。简单做法是把 value 分成 3 到 4 个区间,每个区间给一个固定宽度,代码大约是这样:
const widthMap = v => { if (v < 10) return 1; if (v < 30) return 2.5; if (v < 60) return 4; return 6; };这样视觉上更容易对比,也不会因为极端值而让主流线条淹没其他信息。
4.4 物质流线视角下,地图缩放怎么保持协调
roam开启后,用户会缩放地图。问题是,缩放时线条宽度和箭头大小也会跟着 geo 缩。许多项目第一版上线后,用户反馈“我放大到某市,线条怎么变成乱麻了”。这是因为缩放后,局部线条密集区被放大,原先隐藏的交错全部暴露出来。
我提供的方案是:监听georoam事件,在缩放结束后重新设置 arrow 的 symbolSize 和 lineStyle.width。缩放越大,线条相对越细,箭头相对越小,才不至于把区域盖住。这个逻辑用代码写起来不复杂,但需要配合节流函数,否则缩放动画过程中会频繁 setOption。
chart.on('georoam', () => { // 用 requestAnimationFrame 做节流 requestAnimationFrame(() => { chart.setOption({ series: [{ effect: { symbolSize: currentZoom > 2 ? 3 : 6 } }] }); }); });5. 个性化标注的技巧与玩法
5.1 用 markPoint 做高亮标注,还是用 graphic
个性化标注有两种主流实现:markPoint 和 graphic。
markPoint 属于 series 的一部分,可以基于数据坐标在地图上打点、标文字、标 icon,优点是和 series 数据绑定紧密,适合“从业务数据中挑出重点节点”的场景。graphic 是更底层的图形绘制,自由度极高,可以摆放任意文字、矩形、图片,但不会随数据自动点位,适合做“完全定制化的水印、标题、角落横幅”。
我通常优先用 markPoint,因为它天然支持coord坐标、label文字、symbol图形、以及 tooltip。单独用 graphic 做十几个动态点位,配置量会大很多。
series.push({ type: 'effectScatter', coordinateSystem: 'geo', data: majorSites.map(s => ({ name: s.name, value: [s.lng, s.lat], itemStyle: { color: '#d33' }, label: { show: true, formatter: s.name, position: 'right' } })) });5.2 标签避让:多标注重叠的三种解法
标注一多,文字重叠就是必然的。比较原始的方案是缩小字号,但缩到 10px 以下后根本看不清。我实验过几种方案,各有适用范围。
- 固定偏移:让所有 label 统一在 marker 右侧上下交错排列,适合标注数量较少、分布比较散的情况。
- 分组展示:把同一市内的多个标注点合并成一个“簇标点”,点击后再展开,适合城市内节点密集的情况。
- 层级过滤:地图 zoom 小于某个阈值时只显示关键标注,放大后再显示次级标注,适合省级看宏观流向、市级看细节点位的情况。
第三种在实际业务中最实用。ECharts 里没有内置层级显示逻辑,但可以通过事件监听手动控制 series data 的长度和可见性。比如监听georoam事件后,按照当前 zoom 来重新设置 series 的数据。
5.3 tooltip 与信息面板的联动,怎么让标注不白做
标注不只是视觉上打一个点,用户点击后需要看到详情。我用过的两种交互方式:
第一种是 tooltip。把标注点的tooltip.formatter写成 HTML 模板,做成带表格的悬浮卡片。优点是零额外代码,缺点是信息一多悬浮框会盖住地图,移动端体验也一般。
第二种是旁边加信息面板。点击标注点时,将一个对象传给 Vue 或 React 的组件状态,右侧滑出一个详情栏。这里有良好的扩展性,可以显示图片、关联数据、跳转链接,也可快速集成业务流程。这个方案我更推荐,因为业务标注最终都会希望落到具体台账。
选中态的处理要注意:ECharts 的点击事件拿到的参数里有dataIndex,你可以把该 dataIndex 对应的数据置为高亮,再重新 setOption。如果不清除上一次的高亮,每次点击都会留下一块残影,看着很乱。
6. 常见问题与调优实录
6.1 地图不显示、数据空白、线对不上坐标
新手最常遇到的就是地图白屏。先别急着怀疑代码,按这个顺序排查:
- 检查 GeoJSON 是否成功加载,console 里有没有 registerMap 后的报错。
- 检查 series 或 geo 的 map 名称是否跟注册名称完全一致,包括大小写和空格。
- 检查 series 数据里的经纬度是否反向。很多数据源用的是“纬度、经度”存坐标,而 ECharts 地图要求的是“经度,纬度”。这一条我至少见了四五次。
- 检查数据中是否混入了 null 和 NaN。ECharts 遇到无效坐标不会报错,但会悄悄不画。
如果 resized 之后地图被截断,记得调用chart.resize()。如果是在 Tab 页里渲染,需要在页面可见时再初始化,否则宽度检测会是 0。
6.2 性能优化:大物质流数据怎么推流不卡
当链路数据超过 300 条时,ECharts 渲染依然能撑住,但动态动画会明显掉帧。我有几个实测有效的降载措施:
- 减少
effect的数量,只给流量前 N 的关键路径加飞线箭头。 - 关闭
animation,或把animationDuration调短,避免缩放时大量重绘。 - 利用
large模式的 lines 配置,ECharts 的大数据模式会走画布优化路径,牺牲部分自定义效果换取流畅度。 - 数据聚合:先把同一方向的重复链路合并成一条聚合线,再在 tooltip 里显示详细子链路数量。
6.3 字体、配色与地图风格统一
地图风格和业务大屏的视觉体系最好提前定清楚。我用过比较稳的方案是:底图区域色控制在低饱和度的浅灰或浅蓝绿,边界线用中等灰度;物质流线条用高饱和的主题色,比如橙红或青色;标注点用对比色,比如红色或黄色。三种视觉层级一目了然。
如果背景是大屏深色模式,底图areaColor用深蓝黑,边界色用白色透明度 0.4。如果背景是汇报 PPT 式的白色,底图用浅灰,边界色用深灰。文字标签的颜色一定要跟底图区色有足够对比度,否则一放大就迷失。
还有一点,地图上的市名 label 默认有点大,我一般会设成 10 或 11 号字,并在 zoom 变小或变大时动态改变。省内城市名密集的区域,比如川中一带,如果不调整字号,挤在一起很难看。可以把 label 的show根据 zoom 层级开关,同时开启avoidLabelOverlap: true来自动避让。
7. 扩展方向:从静态图到可交互分析工具
前面写的这套其实已经是一个可交付的地图分析模块。但做成产品的话,还能往两个方向扩展。
第一个方向是时间轴。很多物质流数据是按月度或季度更新的,可以在 ECharts 外面包一层时间滑块,切换时间维度后更新 nodes 和 links。注意切换时需要整体更新 series 的 data,不要让旧数据和新增数据混合。我通常会重写数据数组,而不是在原数组上 push。
第二个方向是联动下钻。省级地图点某一个市,跳转到该市的区县地图并展示该市内部的物质流网络。实现上只需要额外注册对应市的 GeoJSON,点击事件里替换地图和 data 即可。但要注意:GeoJSON 的注册名不能重复,下钻后要重新 registerMap,并处理返回上一级的交互按钮。
这个扩展我目前在模拟项目X里做过一版,用户体验提升非常大。地图从“展示页”变成了“分析工具”,用户会主动去点、去筛、去比。对做工业管理、环保调度或者供应链规划的朋友,这套思路很值得一试。
8. 写在最后的实操心得
我自己在做过几套区域物质流全是洞之后的一个体会:ECharts 的 map 其实不难,真正吃掉大量时间的是数据对齐和样式微调。数据对齐靠前期规范和映射表,样式微调只能靠反复审看。
这里分享一个很实用的小技巧:在开发阶段,给地图容器加上一条淡色网格背景,并用 console 把每个标注点的经纬度实时打印出来。这样当某个点位置不对时,你能很快判断是经纬度反了、还是 GeoJSON 问题,还是偏移量没算对。可视化层级的 bug,很多时候用调试台比用肉眼看更高效。
另外,每次 setOption 前尽量把 option 对象完整构建好,不要零零散散地往原对象里塞字段。尤其是 geo、series 这种嵌套层级很深的配置,直接改原对象很容易改到一半变成 undefined。我比较推荐在函数里重新组装一个 option,再用 setOption 整体覆盖,省心,也不容易留隐藏问题。
这套四川省地图+物质流+个性化标注的组合,改改 geoJSON 和 series data,完全可以平移到任何省级区域。数据梳理清楚之后,真正写配置的时间其实只需要一两天。如果你正好要做类似的区域可视化,按这篇文章的顺序先把数据和事件模型定好,后面实现会顺畅很多。