
前阵子接了个数据可视化大屏的需求首页必须放一张中国地图鼠标移上去要显示各省的业务数据点击省份还得到地市一级。我最初以为这事很常规——毕竟ECharts是开源库地图组件属于基础功能结果真动手才发现ECharts 5之后官方不再内置中国地图的GeoJSON数据网上能找到的要么缺南海诸岛要么坐标偏了要么省名和业务数据对不上。我前后踩了不少坑把地图从空白页一点点调到能看、能点、能下钻顺带把地图旁边折线图、饼图、tooltip缩放大屏这类配套问题也理了一遍。这篇文章就从地图数据准备开始把整个实现过程、关键配置和实战中真正会踩到的坑完整写出来。1. 地图数据是第一步GeoJSON从哪里找为什么ECharts自己不带中国地图1.1 ECharts 5之后的变化与GeoJSON来源如果你用的是ECharts 4以前的版本会发现在示例页面里直接写map: china就能出图因为旧版本把中国地图的JSON打包进了内置地图资源。但从ECharts 5开始官方把内置地图全部移除理由是地图数据更新频率高、体积又大不适合随主包分发。这个改动对于经常做国内项目的人来说影响不小因为第一反应往往是去官方示例里复制代码结果发现地图渲染出来是空白控制台还会提示找不到china这个地图。GeoJSON的推荐来源主要有几个阿里云DataV GeoAtlashttps://geo.datav.aliyun.com/areas_v3/bound/100000_full.json这是最常用的全国数据地址带南海诸岛按省/市/区划分清晰可以直接用fetch加载。其他开源GitHub仓库里的china.json很多是早年整理的可能没有更新到最新行政区划比如某些地区代码已经调整地图上还是旧名字。有条件的团队直接采购或使用业务部门提供的地理信息数据。我个人的选择是DataV GeoAtlas原因很简单数据全、更新及时、支持按adcode取省市级JSON做下钻时非常方便。比如要下钻到广东省直接请求https://geo.datav.aliyun.com/areas_v3/bound/440000_full.json返回的就是广东省的GeoJSON连市级边界都包含在里面。1.2 registerMap的注册流程和使用边界拿到GeoJSON之后不是直接塞进series配置而是先要通过echarts.registerMap接口注册。很多第一次写的人会忽略这一步以为配置了map: chinaECharts就能自己识别结果白屏半天。注册方法很简单fetch(https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json) .then(response response.json()) .then(geoJson { echarts.registerMap(china, geoJson); // 注册完才能初始化图表 initChart(); });这里的china是给这个地图起的名字可以随便取但必须和后续配置项map: china保持一致。在实际项目里我通常会把省份编码拼进去比如registerMap(china_440000, geoJson)避免多次注册时名字冲突。注册接口还有一个可选参数specialAreas用于对地图局部区域做缩放或偏移但日常项目里基本用不到。真正需要注意的是registerMap需要在setOption之前完成所以当GeoJSON是通过异步请求拿到的一定要把图表的初始化和setOption放进then回调里或者先给用户展示loading状态否则就会出现经典的首页刷新地图空白等一会儿手动resize又出来了这种诡异现象。1.3 数据质量自查南海诸岛、边界和name字段从DataV拉取的地图数据基本没问题但如果你用的是别人打包好的china.json一定要检查三件事。第一南海诸岛是否完整。新闻里经常出现的九段线在地图上是一块独立的区域很多精简版JSON直接把它去掉了虽然主体数据没影响但放在正式大屏上非常明显。第二GeoJSON中的properties.name字段必须和你的业务数据name字段严格一致。ECharts的map系列做数据匹配时是把series.data里的name和地图JSON里的name做字符串匹配并不关心adcode或id。这就意味着如果你的数据里有内蒙古而GeoJSON里写的是内蒙匹配不上地图上对应省份就没有颜色。更隐蔽的是广西壮族自治区和广西这种差异看起来差不多实际就是匹配不上。第三文件体积。全国级别的GeoJSON动辄几百KB到几MB如果页面首次加载全部地图数据白屏时间会很长。我做过的一个项目里100000_full.json加载了将近900ms放在业务数据接口较慢的环境里体验很差。比较好的做法是先展示全国骨架后续按需加载下钻数据这个后面会细说。2. 一个能跑的中国地图最小实现与地图定位着色2.1 最小可运行示例注册、初始化、setOption先把最简版本写出来让地图先跑起来。下面是完整可运行的HTML假设你已经引入了ECharts 5的CDN!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleECharts中国地图最小实现/title script srchttps://cdn.jsdelivr.net/npm/echarts5.5.0/dist/echarts.min.js/script style #chart { width: 1000px; height: 700px; } /style /head body div idchart/div script fetch(https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json) .then(response response.json()) .then(geoJson { echarts.registerMap(china, geoJson); const chart echarts.init(document.getElementById(chart)); const provinceData [ { name: 广东, value: 1280 }, { name: 江苏, value: 1170 }, { name: 浙江, value: 980 }, { name: 四川, value: 760 }, { name: 山东, value: 645 } ]; chart.setOption({ tooltip: { trigger: item, formatter: function(params) { if (params.value) { return params.name br/销售额 params.value 万; } return params.name br/暂无数据; } }, visualMap: { min: 0, max: 1500, left: 20, bottom: 20, inRange: { color: [#e0f3f8, #abd9e9, #74add1, #4575b4, #313695] } }, series: [{ type: map, map: china, roam: true, label: { show: false }, data: provinceData }] }); window.addEventListener(resize, function() { chart.resize(); }); }); /script /body /html这套代码是地图项目的地基。需要注意几个点tooltip的formatter里我先判断了params.value是否存在这是处理有些省份没数据的标准姿势不然hover到没有数据的省份时界面上会显示一个赤裸裸的undefined非常掉价。visualMap的min和max如果业务上不好预估可以先写死后续用setOption动态调整。2.2 地图定位与缩放roam、center、layoutCenter地图不是一定要显示全中国有时候只显示某个区域或者希望地图大屏打开时自动定位到数据最集中的省份。这就要用到center、zoom和layoutCenter。roam控制用户是否能交互缩放和拖拽可选值有true、false、scale和move。做展示型大屏时我一般设成false如果还需要交互下钻则设成true。center和zoom在map系列里看起来可以用但实际效果受投影方式和地图尺寸影响不太直观。我踩过坑之后发现更可控的是配合geo组件统一管理geo: { map: china, roam: true, center: [104.0, 37.5], zoom: 1.2, // 默认放大级别 layoutCenter: [50%, 50%], layoutSize: 90% }, series: [{ type: map, map: china, geoIndex: 0, // 让map系列挂载到已注册的geo组件上 data: provinceData }]这里比较关键的理解是geo组件负责地图的坐标定位、缩放、平移这些底层几何能力series.map负责数据渲染。当同时声明geo和map系列时map系列可以设置geoIndex: 0共享geo的几何状态这样地图定位和缩放就变得可控了。layoutCenter和layoutSize是用百分比控制地图在整个容器里的位置和大小比写死center坐标更适配不同分辨率的大屏。2.3 visualMap连续与分段数据是如何变成颜色的visualMap是地图上色的核心组件它把数值映射成颜色。ECharts的visualMap组件支持两种形态连续型type: continuous和分段型type: piecewise。连续型适合数值分布均匀的场景就是我上面示例里那种写法通过inRange.color指定一段色带。这里最需要注意的问题是颜色深浅和数值要对应浅色代表低值、深色代表高值色带最好选同一个色系比如浅蓝到深蓝。色系跨度太大的话比如红到绿视觉上容易出现误判非专业人士会觉得这块区域大小和颜色完全不搭。分段型适合把数切成几个档位比如0到100、100到500、500以上。这种在大屏里展示评级时特别常见visualMap: { type: piecewise, pieces: [ { min: 1000, label: 1000万以上, color: #b80000 }, { min: 500, max: 1000, label: 500万-1000万, color: #eb6700 }, { min: 100, max: 500, label: 100万-500万, color: #f3c300 }, { min: 0, max: 100, label: 0-100万, color: #a3d8f4 } ], left: 20, bottom: 20 }还有一点必须提醒当地图数据更新时visualMap的min和max不会自动跟随数据变化。如果接口返回的数据最大值变了图例和颜色映射仍然停留在旧范围导致颜色看起来不对。我习惯在拿到数据后先遍历一次计算真实的最大最小值再通过setOption整体重置visualMap范围。3. 地图上的信息层散点热点、自定义图标与省份下钻3.1 用effectScatter标出城市热点的配置方法地图只做省份着色有些单薄大屏项目里通常还要在地图上叠加城市级别的散点比如重点销售城市分布门店数量Top10城市。这里推荐用effectScatter它在普通散点的基础上加了涟漪动画视觉上很有冲击力。要让effectScatter显示在地图上需要让它和geo组件建立坐标系关联series: [ { type: map, map: china, geoIndex: 0, data: provinceData }, { type: effectScatter, coordinateSystem: geo, // 关键用geo的坐标系 data: cityData, // [{name: 北京, value: [116.4, 39.9, 98]}] symbolSize: function(val) { return Math.max(val[2] / 20, 5); }, rippleEffect: { brushType: stroke, scale: 4 }, label: { show: true, formatter: {b}, position: right }, itemStyle: { color: #ff6600 } } ]这里data里的value是三个元素[经度, 纬度, 数值]前两个必须是标准的经纬度坐标第三个是散点大小映射。城市经纬度数据通常需要业务方提供或者自己维护一张城市坐标表。用symbolSize函数根据数值动态调整散点大小视觉效果更自然。如果散点之间靠得太近数值大的城市会挡住小的我一般会把symbolSize的映射系数调小并且给低值的点设置一个最低大小保证每个点都能被看到。3.2 柱状图标和自定义图片地图大屏里伪柱状图的替代方案搜echarts柱状图柱子可以用自定义图片显示不的人多半是想在地图上或者地图下面做出更活泼的柱子效果。先回答这个问题ECharts的柱状图柱子完全可以用图片替代做法是把symbol设为image://协议。series: [{ type: bar, data: [120, 200, 150, 80], barWidth: 30, itemStyle: { color: { image: document.getElementById(barImg), // 或 image://data:image/png;base64,xxx repeat: stretch } } }]如果柱子的背景图是PNG直接设置image: image://./img/bar-bg.png也行。不过在实际大屏项目里直接在地图上放真正的bar系列并不是好选择。ECharts的bar需要笛卡尔坐标系直接叠加在地图上会很生硬而且柱子的宽度是像素单位地图一缩放柱子就失真了。我更推荐的做法是用symbol为图片的scatter系列模拟柱子散点位置放在省份中心。用effectScatter做气泡排名。用markPoint在地图上标记最大值省份。这个思路也回答了echarts 柱状图柱子可以用自定义图片显示不的深层需求如果只是想增强视觉效果symbol自定义图片是最灵活的方案而且不破坏地图的坐标系。3.3 点击省份下钻到地市的联动实现点击下钻是很多地图大屏的刚需。实现逻辑并不复杂监听地图点击事件拿到省份名称然后加载对应省份的GeoJSON重新注册后更新series。const mapStack [china]; chart.on(click, function(params) { const provinceName params.name; if (!provinceName || mapStack[mapStack.length - 1] ! china) { return; } const provinceAdcode provinceAdcodeMap[provinceName]; // 省份名的adcode映射表 if (!provinceAdcode) return; fetch(https://geo.datav.aliyun.com/areas_v3/bound/ provinceAdcode _full.json) .then(response response.json()) .then(geoJson { echarts.registerMap(province, geoJson); mapStack.push(provinceName); chart.setOption({ geo: { map: province, center: geoJson.center || undefined, zoom: 6 }, series: [{ map: province, geoIndex: 0, data: cityData[provinceName] || [] }] }); }); });这里的核心是provinceAdcodeMap一份把广东映射到440000的对照表。DataV的接口规则是{adcode}_full.json全国是100000_full.json各个省级单位分别是对应的行政区划代码。下钻后通常还要提供一个返回全国的按钮实现就是维护一个mapStack点击返回时出栈并重新注册上一级地图。踩坑提醒下钻之后地图的center和zoom不重置的话会停留在上一次的缩放位置用户会以为页面卡住了。从GeoJSON的center字段取中心点是一个省事的办法实际使用中部分省份的center并不完全在几何中心建议结合省份实际形状微调一下。另外重复点击同一个省份时会反复请求JSON最好加一个请求锁或者在请求前判断当前地图是不是已经在这个省份。4. 地图旁边那些图表的坑折线图刻度、tooltip换行、饼图labelLine4.1 折线图x轴刻度不显示或显示不全的排查方向地图大屏很少只有一张地图几乎都会配折线图展示趋势。最容易翻车的是x轴刻度数据一多刻度标签要么叠在一起要么直接不显示。几个排查方向按优先级排第一axisLabel.interval。类目轴默认可能会自动跳过一些标签想让刻度全部显示就设interval: 0。xAxis: { type: category, data: timeList, axisLabel: { interval: 0, rotate: 45, // 文案长时旋转避免重叠 fontSize: 12 } }第二boundaryGap。如果你发现第一个和最后一个刻度离坐标轴起点终点太近或者太远检查一下这个配置。折线图通常设boundaryGap: false让折线从坐标轴起点开始柱状图则设true。第三时间类x轴最好用type: time而不是全部塞成类目字符串。用time类型时ECharts会自动按时间间隔和容器宽度决定显示哪几个刻度显示不全是正常的。你需要做的是在axisLabel.formatter里按需格式化文案xAxis: { type: time, axisLabel: { formatter: function(value) { return echarts.format.formatTime(MM-dd, value); } } }之前有个项目里折线图x轴刻度明明有数据却不显示排查到最后发现是容器的宽度被父级压缩成了0ECharts初始化时拿到了错误的宽度。这个属于resize问题后面第5章再展开。4.2 tooltip自动换行formatter与extraCssTextecharts tooltip自动换行这个搜索词背后是很多人在tooltip里塞了一长串数据默认样式下全部挤在一行丑到没法看。tooltip换行有两个层面。第一层是内容换行。tooltip的formatter返回HTML字符串时用br/换行即可tooltip: { trigger: item, formatter: function(params) { return [ div stylefont-weight:bold;margin-bottom:4px; params.name /div, 数值 (params.value || -), 占比 (params.data params.data.percent ? params.data.percent % : -) ].join(br/); } }第二层是容器宽度限制。如果tooltip内容本身很长光有换行还不够还要防止tooltip把整个弹层撑得太宽。ECharts的tooltip支持extraCssText可以往里追加CSS样式tooltip: { extraCssText: max-width:320px;white-space:normal;word-break:break-all; }这里white-space:normal是让文本可以自动换行的关键。我见过不少人以为只要formatter里写了br/就完事结果遇到没有换行符的长文本依然溢出。两个配置配合使用才能保证tooltip无论是主动换行还是被动换行都正常。4.3 饼图labelLine末端小圆点偏移的修复方法地图项目的右下角或侧边栏经常放饼图做占比分析所以饼图 labelLine末尾小圆点偏移这类问题也算地图大屏的高频坑。先解释现象ECharts饼图的label默认只有一条引导线没有小圆点。很多人为了做引导线末端带小圆点的视觉效果会在label的formatter里返回一段带span的HTML比如label: { formatter: function(params) { return span styledisplay:inline-block;width:8px;height:8px;border-radius:50%;background: params.color ;margin-right:6px;/span params.name params.percent %; } }这样确实出现了小圆点但经常会发现圆点和引导线的末端不是对齐的甚至偏移到上一行或下一行。根本原因是span是行内元素在tooltip或label的HTML渲染中会受到vertical-align和line-height影响基线对不齐。修复方案我习惯用两种。第一种在span上加上vertical-align:middle并且设置line-height一致span styledisplay:inline-block;width:8px;height:8px;border-radius:50%;background: params.color ;margin-right:6px;vertical-align:middle;/span第二种放弃HTML模拟圆点直接不画点只显示文本和百分比。说实话在正式大屏里引导线末端的小圆点并不是不可或缺的视觉元素如果它总在对齐上出问题就删掉。ECharts自带的引导线已经足够清晰没必要为了小圆点增加复杂度。顺手说一句饼图labelLine本身有两个长度配置length是引导线靠近饼图的那一段length2是靠近文字的那一段。文字位置偏移时先把这两个值调小再调整label.padding和label.margin比硬调vertical-align更稳定。5. 大屏项目里ECharts地图的真实工程问题5.1 vue3项目里pxtorem对ECharts不生效的真相大屏项目常配合rem适配方案vue3里很多团队会用postcss-pxtorem自动把px换算成rem。但你很快会发现一个诡异现象页面上其他DOM都跟着rem正常缩放了唯独ECharts图表里的文字、散点大小、图形尺寸纹丝不动甚至整个canvas容器都缩了图却不缩。原因其实不难理解postcss-pxtorem是在编译时把CSS文件里的px转成rem它只处理样式表中写得出来的东西。ECharts内部的所有绘图尺寸、字体大小、symbol大小是在JavaScript里用数字px计算后直接绘制到canvas上的根本不经过CSS。也就是说ECharts不认识rem也不知道当前根元素的font-size是多少。所以在大屏项目中不能指望pxtorem自动适配ECharts内部尺寸。我常用的解决方案有三种。第一种用transform: scale整体缩放整个大屏容器。这是最简单粗暴也最稳定的方式把设计稿按1920宽度做然后动态计算浏览器实际宽度和设计稿宽度的比例把整个页面容器做scale。这样ECharts的canvas会跟着容器的transform一起缩放不需要改任何代码线条和文字也不会因为被缩放而模糊。第二种在配置ECharts时先算出rem换算比例把字体和symbolSize都套一层函数const rem2px function(px) { const baseWidth 1920; const scale document.documentElement.clientWidth / baseWidth; return px * scale; }; // 使用时 label: { fontSize: rem2px(14) }, symbolSize: rem2px(20)这种方式适合对细节控制要求高的项目但要记得resize时重新setOption。第三种干脆不用rem做ECharts容器的适配只让外部布局用flex或其他自适应方案ECharts本身监听resize后调用chart.resize()让canvas自动铺满容器。这种方法最贴近ECharts的设计思路我在地图大屏项目里用得最多。5.2 原生JSjQueryAjaxECharts的数据对接套路有些老项目或者运营后台没有前端框架还是原生JS配合jQuery开发。这类环境里做ECharts地图很多人的困惑是前端代码怎么和后端接口对接数据格式怎么转。其实ECharts本身不关心你是用jQuery还是fetch它只认setOption里传入的data结构。用jQuery的$.ajax请求后端接口再在回调里拼装数据即可$.ajax({ url: /api/biz/province, type: GET, dataType: json, success: function(resp) { if (!resp || resp.code ! 200) { return; } const mapData resp.data.map(function(item) { return { name: item.provinceName, value: item.totalValue }; }); chart.setOption({ series: [{ type: map, map: china, geoIndex: 0, data: mapData }] }); }, error: function(xhr, textStatus) { console.error(省份数据加载失败, textStatus); } });这里最需要强调的是数据清洗。后端返回的字段名几乎不可能正好是name和value必须map一层。同时后端返回的省份名称常常是广东省这种全称而GeoJSON里用的可能是广东此时要不就是后端统一口径要不就是前端做一次映射转换。我建议在前端维护一个映射函数把全称转成简称const provinceShortNames { 北京市: 北京, 广东省: 广东, // 其余省份同理 };另外不要只在success里写逻辑error回调里至少要打个日志。地图大屏项目调试困难控制台没有报错信息时会非常折磨人。5.3 异步加载、resize监听和地图性能的三个建议最后给三个项目级建议都是实操验证过的。第一地图JSON异步加载的顺序问题。地图数据是异步的图表容器可能这时候还没渲染完成。最佳实践是先chart.showLoading()等JSON加载完并注册完地图后再hideLoading()并setOption。等到地图真的渲染出来再关loading用户不会面对无休止的空白大屏。chart.showLoading({ text: 地图加载中, color: #409EFF, maskColor: rgba(255, 255, 255, 0.8) });第二resize监听。常规写法是window.addEventListener(resize, () chart.resize())但在iframe嵌入、多标签页切换、侧边栏折叠这类场景下window的resize不可靠。我习惯用ResizeObserver监听ECharts容器的尺寸变化const resizeObserver new ResizeObserver(function() { chart.resize(); }); resizeObserver.observe(document.getElementById(chart));第三地图重绘性能。GeoJSON一旦注册好尽量复用不要每次setOption都重新fetch。在地图和多个图表联动的大屏里频繁注册大体积地图JSON会造成明显的卡顿。解决思路是维护一个mapCache对象key是adcodevalue是已注册的地图实例名需要展示某块区域时直接查缓存没有才去请求。关于是否需要真正3D柱状图或3D地图这件事我也补充一句ECharts本身不具备真正的3D渲染能力要做3D柱状图或3D地图需要额外引入echarts-gl库并且会带来投影、光照、性能三方面的额外复杂度。如果项目没有强烈的视觉要求先用2D地图把数据讲清楚远比硬上3D稳妥。按我个人的经验地图可视化大屏最容易翻车的地方从来不是配置项背不下来的问题而是数据链路没理清地图JSON异步加载导致白屏、业务字段和地图name对不上、resize后图表变形、rem适配缩放出错。只要把这几条主线想清楚中国地图这块基本就稳了。