ECharts世界地图js资源:GeoJSON注册与飞线配置实战指南
简介面向需要在地图上展示全球数据的前端开发者与数据可视化爱好者这份zip压缩包提供了一套可直接集成的ECharts世界地图资源合计77个文件其中39个JS文件用于图表逻辑与交互控制38个JSON文件保存了GeoJSON格式的国家边界与地理坐标数据整体仅3.42MB轻量易加载。已有1639人学习下载。包内除了核心的echarts.js还包含macarons等经典主题配置开发者可直接引入后渲染交互式世界地图实现缩放、平移、点击提示、visualMap颜色映射等功能无需额外处理复杂的地图数据格式。配套的JSON地理数据覆盖全球主要国家和地区可直接对应配置项的data数组进行数值填充。使用时可参考资源内典型示例快速掌握从echarts.init初始化、series地图类型设置到setOption渲染的完整链路并在此基础之上扩展自定义标注、动态效果等适合从入门图表展示到多维数据地理分布分析等多种应用场景是搭建专业地理数据可视化页面的实用工具包。1. ECharts世界地图js为什么地图数据要自己找做可视化大屏遇到世界地图需求很多人第一反应是打开ECharts官方示例复制一段world地图配置。但到了自己的项目里地图往往不显示或者只显示一片空白。这不是ECharts本身的问题而是从ECharts 5开始官方包内不再内置地图数据世界地图的GeoJSON、JS数据文件需要单独获取和注册。你拿到的这份ECharts世界地图js资源核心就是解决这件事——里面包含可直接引用的世界地图数据、注册方式以及配套的配置示例省去你自己去各个源站翻找、裁剪、适配坐标系的时间和精力。适合正在做大屏报表、需要快速把世界地图跑起来的开发者也适合想搞清楚地图数据加载链路、避免后续维护踩坑的前端工程师。搞清楚数据从哪来、注册到哪、配置怎么写后面再换区域、改样式、接数据都顺理成章。2. 数据源选型GeoJSON与内置JS的差异和取舍2.1 ECharts地图的底层逻辑坐标系与GeoJSON的关系ECharts里画世界地图本质上是在一个笛卡尔平面上根据GeoJSON的坐标串绘制多边形。GeoJSON里用经纬度表示边界点ECharts拿到后通过projection逻辑映射到画布。所以数据源核心必须是一个合法的GeoJSON对象或者一个导出了该对象并调用registerMap注册的JS文件。常见的世界地图数据无非两种形态。第一种是纯GeoJSON文件通常以world.json、world.geo.json命名里面是FeatureCollection结构每个Feature代表一个国家或地区properties里往往带name这类字段。第二种是JS文件形如world.js内部会有一段类似{type:FeatureCollection,...}的结构或者直接调用注册方法。区别在于JSON文件需要你自己fetch或import后再注册JS文件一般是在引入script标签后就自动完成注册或者暴露一个全局变量由你手动注册。我一般建议优先选纯GeoJSON。理由很简单纯数据文件不跟运行时机绑定你在Vue、React、原生JS里都能通过fetch或import加载注册时机自己把控而JS文件如果是ECharts 4时代那种require(echarts)后直接注册的写法放在现代打包环境里很容易报错。这份资源里两种形态通常都覆盖了优先用JSON。2.2 坐标系与归一化为什么有的地图变成一团糊还有一个容易被忽略的坑数据坐标系。绝大多数世界地图GeoJSON用的都是WGS84经纬度坐标这对ECharts来说是最标准的输入。但有些第三方源会给你投影后的坐标或者数据里混着负值、异常大值画出来要么缩成一团要么图形严重变形。判断方法很简单打开GeoJSON文件看坐标数组里第一对值大致在什么范围。WGS84的经度范围是-180到180纬度范围是-90到90。如果看到几百上千的值说明这份数据是投影过的不能直接用需要先做投影反算或者换数据源。另外注意数据是否经过归一化。有的源站会把地图数据裁切到一张固定大小的画布坐标系里比如0到1000跟经纬度差着量级。这种数据即便能显示后续你做散点图、飞线图时经纬度坐标坐标的映射也会完全对不上。所以拿到资源后第一步不是写代码而是先打开检查坐标值域。提示验证数据是否可用最快的办法是直接用JSON.parse读取然后打印features[0].geometry.coordinates[0][0]看一下首对坐标的数值范围。2.3 文件格式选择script标签、import与fetch的适用场景回到这份资源的使用方式。如果你是PC端大屏页面是纯HTML引入script的写法可以直接加载world.js让它自动注册然后马上配置option。但如果你的项目是Vite、Webpack这类工程化环境我更建议用import方式引入JSONimport worldGeoJson from ../assets/map/world.json echarts.registerMap(world, worldGeoJson)这里的关键是registerMap的第一个参数是你自己定的地图名第二个参数必须是GeoJSON对象。很多人在工程化项目里直接script引JS文件结果发现全局变量根本没暴露或者暴露的时机晚于图表初始化就会报“地图未注册”的错误。如果是异步加载就用fetchfetch(./map/world.json) .then(res res.json()) .then(geoJson { echarts.registerMap(world, geoJson) initChart() })这种方式的好处是地图数据不会阻塞首屏渲染适合大屏启动时要先拉登录态、用户信息等接口的场景。坏处是你要处理加载失败的兜底逻辑不然地图区域空白。选择哪种加载方式取决于你的项目结构。旧式多页应用用script标签最省事单页应用用import或fetch更符合模块化管理。这份资源里如果两种文件都有别犹豫工程化项目选JSON那个。3. 注册与渲染从引入文件到屏幕上的世界地图3.1 第一步注册地图与容器初始化拿到数据、确定加载方式后注册很简单但有一个顺序问题必须遵守先注册地图再初始化图表实例最后设置option。反过来会报错或者地图区域什么都不显示。import * as echarts from echarts import worldGeoJson from ../assets/map/world.json // 1. 注册地图名称自己定义后续series和geo都引用这个名称 echarts.registerMap(world, worldGeoJson) // 2. 初始化实例绑定到已经设置好宽高的DOM容器 const chart echarts.init(document.getElementById(mapContainer)) // 3. 配置option const option { geo: { map: world, roam: true, itemStyle: { areaColor: #1a2b4a, borderColor: #4a7dbd } } } chart.setOption(option)在这段代码里registerMap的第二个参数直接是解析后的GeoJSON对象注意不是字符串。如果你用fetch拿到的是纯文本一定要先JSON.parse再传给registerMap这是最常见的报错来源之一。geo组件是ECharts里专门用于绘制地图的独立组件它和series-map的区别在于geo只管底图展示不跟数据绑定series-map则是地图和数据联动每个区域的颜色、数值都由series.data控制。如果只是想先看到一张干净的世界地图用geo就够了。后面要做数值分级渲染再说series。3.2 第二步基础视觉参数与区域命名规则地图显示出来后一般会先调底图样式。常用的参数集中在itemStyle和emphasis里。const option { geo: { map: world, roam: true, // 允许缩放和平移 zoom: 1.2, // 初始缩放倍数 center: [0, 20], // 初始视角中心[经度, 纬度] itemStyle: { areaColor: #0f1f3d, // 区域填充色 borderColor: #3c6ea5, // 边界线颜色 borderWidth: 0.6, // 边界线粗细 shadowColor: rgba(0, 0, 0, 0.5), // 阴影颜色 shadowBlur: 10 }, emphasis: { itemStyle: { areaColor: #2a5f8f // 鼠标悬停时的区域高亮色 }, label: { show: true, color: #fff } } } }roam接受布尔值或者scale、move分别表示是否开启缩放、拖动。大屏场景一般设成true就行但如果你做的是固定视角的静态展示关掉会更稳定避免用户误拖后回不来。关于区域命名要特别说一句geo组件不依赖数据时区域名称来自GeoJSON的properties里的name字段。这份世界地图数据里国家名大概率是英文少数源会带中文name。如果你后续要做数据映射比如data: [{ name: China, value: 88 }]这里的name必须与GeoJSON里的name完全一致大小写、空格都不能差。很多飞线图点了不显示就是国家和名字匹配不上。3.3 第三步tooltip与数据绑定的前期准备地图搭配tooltip展示数值是常规操作。用series-map而不是纯geo是实现数据绑定的关键。const option { tooltip: { trigger: item, formatter: function(params) { return params.name : (params.value || 暂无数据) } }, series: [{ type: map, map: world, roam: true, data: [ { name: China, value: 141.2 }, { name: Russia, value: 144.8 }, { name: Canada, value: 38.2 } ] }] }这时候series-map里的map字段指向你registerMap时定义的名字。tooltip的params.name取的是当前鼠标所在区域的名字value则是data数组里匹配到的对应值。如果某个区域在data里没有对应项value就是undefinedformatter里要做兜底显示。这一步把底图和数据打通之后后续换数据源、加飞线、做涟漪效果都基于这一层结构展开。提示调试阶段可以临时加一行console.log(params)在formatter里看看每个区域返回的字段到底有哪些对排查很有帮助。4. 业务落地浦染地图与飞线可视化的参数配置4.1 数值浦染场景series-map的分级色彩映射纯地图底图没有业务含义真正用在报表里最频繁的是浦染地图按数值大小给不同国家上深浅不同的颜色。ECharts里做这个不需要引入额外库靠visualMap组件就行。const option { tooltip: { trigger: item }, visualMap: { type: piecewise, // 分段型也可换 continuous 连续型 min: 0, max: 200, left: 20, bottom: 20, textStyle: { color: #ccc }, inRange: { color: [#0f1f3d, #1f5f8f, #3fa7d6, #7fd0d9] // 低到高 } }, series: [{ type: map, map: world, data: countryData // [{ name: China, value: 141.2 }, ...] }] }type有两种piecewise适合数据分布离散或者你需要明确分档阈值的场景continuous适合连续渐变。大屏上通常用piecewise因为可以明确标注出“多少以上是高值”视觉上更直观。inRange.color数组的顺序是低值到高值从左到右依次对应不要写反了不然低值反而最刺眼。有个很容易踩的细节visualMap的显示位置如果设置了left和bottom在大屏不同分辨率下位置会漂移。我一般习惯放到图表的grid区域外并且单独用一个div容器绝对定位放图例而不是完全依赖visualMap自带的渲染位置。如果你做的是自适应大屏建议在resize回调里重新计算位置或者干脆不用visualMap的图例组件自己画一个静态图例。4.2 飞线地图场景散点、涟漪与markLine的坐标体系飞线图的业务需求很典型从某个国家出发箭头指向另一个国家中间带一条有动画效果的曲线。这个功能在ECharts里用series-effectScatter画起点和终点、series-lines画飞线最顺手。这里有个必须理解的概念series-lines里每条线的coords坐标数组用的不是国家名而是经纬度。所以你要么自己准备一份国家中心点的经纬度映射表要么在地图初始化后通过ECharts的API取坐标。const option { geo: { map: world, roam: true, itemStyle: { areaColor: #1a2b4a } }, series: [ { type: lines, coordinateSystem: geo, data: [ { coords: [[116.40, 39.90], [-74.00, 40.71]] } ], lineStyle: { color: #ffd700, width: 1.5, opacity: 0.8, curveness: 0.3 // 曲线弯曲程度 }, effect: { show: true, period: 4, // 动画周期秒数 trailLength: 0.4, // 拖尾长度 symbol: arrow, // 箭头样式 color: #fff } }, { type: effectScatter, coordinateSystem: geo, data: [ { name: 北京, value: [116.40, 39.90] }, { name: 纽约, value: [-74.00, 40.71] } ], symbolSize: 10, rippleEffect: { brushType: stroke, scale: 3 } } ] }关键点在这series-lines的coordinateSystem必须设为geo数据里是coords字段里面的两点就是线段的起点和终点顺序不要反。curveness越大线越弯一般0.2到0.4看起来自然太直像直线太弯又显得夸张。effectScatter也是同样的coordinateSystem但数据格式变成value: [经度, 纬度]注意经纬度顺序写反了会跑到非洲去。飞线数据量大时有个性能注意点effect的动画在每一帧都会重新计算路径成百上千条线同时动画会明显卡顿。我一般把不重要的线去掉effect只保留主要干线的动画或者把period调成6到8秒降低刷新频率。4.3 地图与数据更新的联动setOption的正确姿势业务场景里地图底图通常不变但上面的数据会每隔几秒刷新。很多人直接在setOption里整个替换option对象结果地图重新渲染、飞线要重画还会闪屏。正确做法是用merge方式局部更新。// 首次初始化时全量设置 chart.setOption({ geo: { ... }, series: [ { type: map, map: world, data: countryData }, { type: lines, coordinateSystem: geo, data: [] } ] }) // 后续数据更新时只传变化的字段 chart.setOption({ series: [ { data: newCountryData }, { data: newLinesData } ] })注意ECharts的setOption默认就是merge模式不是覆盖模式。所以你第二次setOption时只写series里的data字段它会把原有配置合并更新。这里有个细节值得提醒如果新数据里某个国家消失了旧的国家颜色还会残留因为它没有被覆盖。这种情况你需要将对应数据项置为null或者重新全量设置geo的data。我处理这类问题时一般先chart.clear()再重新setOption毕竟大屏数据量不大重建成本可控反而逻辑更清爽。另外如果地图数据变化后你发现旧的飞线还在多半是lines的data没被正确覆盖。检查一下是否在series里漏传了对应的索引或者为了省代码直接用了同一个变量名结果引用的是同一个数组对象没有生成新数组。这个坑很低级但确实出现频率不低。5. 避坑手册注册无效、坐标跑偏与加载失败的排查5.1 registerMap重复注册导致样式错乱现象页面里有多个图表实例都用了world地图。第一个实例显示正常后面的实例区域颜色不对或者边界错乱甚至部分国家消失。原因同一份GeoJSON被重复注册到同一个地图名下后面的注册覆盖了前面的。如果两次注册的数据源不同比如一份来自本地裁剪后的JSON一份来自CDN默认数据后来者会让你所有实例都跟着变样。解决全局只注册一次把registerMap放到模块初始化位置而不是放进每个组件的初始化函数里。如果实在需要多份不同裁剪范围的地图用不同的名字注册比如world和world-simplified实例分别引用各自的名字。// 在应用入口统一注册 if (window.__worldMapRegistered) return echarts.registerMap(world, worldGeoJson) window.__worldMapRegistered true用模块加载的话直接利用模块缓存特性在map.js文件里导出一个初始化函数内部判断是否已注册过。这个做法在多页面大屏里尤其重要。5.2 地图区域一片空白控制台也没有报错现象option里geo配好了map: world也对图表其他部分正常就是地图区域空无一物。原因大概率是GeoJSON还没加载完成就执行了setOption。尤其常见于在Vue的created钩子里拉了接口数据然后同步去初始化图表但地图JSON是通过fetch异步拿的时序上晚于setOption。解决把初始化动作放到fetch的回调里或者用async/await包裹确保registerMap执行完成后再初始化async function initMap() { const res await fetch(./map/world.json) const geoJson await res.json() echarts.registerMap(world, geoJson) const chart echarts.init(document.getElementById(map)) chart.setOption(option) }还有一个隐蔽情况生成的DOM容器display:none图表初始化时宽度高度计算为0导致地图画不出来。这种问题排查方式很简单临时把容器display改成block看看是不是这个问题。5.3 散点位置跑到海里经纬度对不上现象数据里的坐标明明是北京的纬度39.9、经度116.4但散点跑到俄罗斯或者非洲去了看起来完全是随机分布。原因第一种是经纬度写反。GeoJSON的顺序是[经度, 纬度]但很多接口返回的顺序是纬度在前。第二种是地图数据本身不是WGS84坐标而是做了某种投影变换。解决先固定测试一组已知坐标比如北京就是[116.40, 39.90]如果点位不在预期位置先交换经纬度试试。如果交换后依然不对逐层检查地图JSON坐标范围。我一般会写一小段脚本把GeoJSON里所有坐标的min/max打出来看经度范围是否在-180到180之间。function checkGeoRange(geoJson) { let minLng Infinity, maxLng -Infinity let minLat Infinity, maxLat -Infinity geoJson.features.forEach(f { const coords f.geometry.coordinates // 简单遍历只处理Polygon和MultiPolygon const arr f.geometry.type Polygon ? [coords] : coords arr.forEach(poly poly[0].forEach(([lng, lat]) { if (lng minLng) minLng lng if (lng maxLng) maxLng lng if (lat minLat) minLat lat if (lat maxLat) maxLat lat })) }) console.log(经度范围:, minLng, maxLng, 纬度范围:, minLat, maxLat) }打印结果如果经度范围在0到360那是数据把西经表示成了大于180的值需要做一次换算。如果范围在0到1000之类说明数据不是经纬度坐标趁早换数据源。5.4 飞线不显示或者飞了一半消失现象series-lines配置了coords也有数据但页面上看不到飞线或者线只画了一半。原因lines的终点坐标超出了GeoJSON定义的区域范围或者飞线的首尾落在海面上。还有一种情况是data数组里的coords没写完整少了一对坐标。解决先用effectScatter把coords里的点分别渲染出来确认起点和终点都在陆地上。如果点位正常接着检查lines的data结构确保每项是{ coords: [...] }而不是裸数组。最后看lineStyle里的opacity和width值太小人眼基本看不见。飞线消失的另一个常见坑zlevel设置高于geo组件的层级时飞线会渲染在地图下面被区域颜色盖住。我一般把series里的所有层统一不设zlevel或者设同一个值依靠series的声明顺序决定层级省得出这类玄学问题。5.5 地图在打包后生产环境加载失败现象本地开发环境一切正常npm run build之后放到服务器上地图区域空白Network面板里提示JSON文件404。原因地图JSON被放在src/assets下通过import引入还好但如果是用fetch请求相对路径比如fetch(./map/world.json)打包后文件路径变了或者没有被打进dist目录。解决如果文件小直接import当成模块资源处理。如果文件大把地图JSON放到public目录用绝对路径引用。我通常的做法是把地图JSON放在public/data/world.json代码里写死/data/world.json这样打包后路径不会变CDN部署时也方便。注意文件名不要加哈希因为fetch请求的路径是静态字符串。6. 进阶技巧地图瘦身与热更新地图数据太大影响首屏加载是大屏项目的常见痛点。世界地图的GeoJSON原始文件动辄几百KB实际使用中部分小岛国根本用不上。我一般会写一个随机抽稀脚本把用不到的区域从features里过滤掉再跑一遍数据压缩。// 过滤掉不需要的国家只保留features里name在名单中的区域 const fs require(fs) const geoJson JSON.parse(fs.readFileSync(world.json, utf-8)) const keepList [China, Russia, Canada, United States of America] const filteredFeatures geoJson.features.filter(f { return keepList.includes(f.properties.name) }) const newGeoJson { type: FeatureCollection, features: filteredFeatures } fs.writeFileSync(world-simplified.json, JSON.stringify(newGeoJson))过滤后如果文件还是大考虑用地图抽稀工具对边界坐标做简化把坐标点数量降下来。不过这里注意抽稀力度过大会让国界线失真飞线起飞降落点位落在外观边界之外。我一般把简化后边界与原始数据的偏差控制在10公里以内肉眼看不出来体积却能降一半以上。实时数据联动的场景里我还习惯在setOption前做一次数据合法性检查筛掉null、NaN和超出经纬度范围的脏数据。这个习惯帮我在演示现场避开好几次数据接口异常导致的地图“地震”——所有点都飞到坐标系的角落堆成一团。从那以后每次接新数据源我都强制走一遍坐标范围校验和离线点位抽验地图数据本身没问题了剩下的就是业务数据的常态化治理。希望这些经验对你也有用少踩几个我已经替你踩过的坑。本文还有配套的精品资源点击获取