
简介面向前端开发与数据可视化从业者的 ECharts 世界地图数据包重点提供以中国为地理中心的世界地图脚本并配套中国省级区域地图数据适合制作涉华数据分析、全球业务看板与地理关系展示。压缩包共 420 个文件以 380 个 JSON 地图数据和 40 个 JS 脚本为主整体约 4.34MBJSON 便于按区域加载与复用JS 可直接在页面中引入并注册地图。已有 3155 人学习下载说明其在 ECharts 地图类资源中具有一定参考价值。读者可据此快速完成地图容器初始化、geo 中心点与缩放平移配置、标签高亮和区域样式调整并将业务数据绑定到 series 进行渲染减少自行搜集世界及省级边界数据、处理坐标与名称匹配的工作量适合搭建中国视角的全球数据可视化页面。1. 让中国落在画面正中一份替换即生效的 echart 世界地图数据大屏做到第三版业务方指着屏幕说了一句能不能让中国在中间。默认的 echart 世界地图 是拿本初子午线当中心线的中国被挤到画面右侧三分之一的位置太平洋对岸那块大陆横在最左边怎么看怎么别扭。这份以中国为中心的 world.js 就解决这一件事——它是一份已经registerMap好的世界地图数据覆盖掉原来的文件option 几乎不用动中国就落到画面正中。它适合正在做大屏、做海外节点分布图、又不想为了一个投影去啃整套 GIS 的前端同学。需要提前说清楚这是一份数据文件不是框架不含任何组件封装它的价值在于省掉你查投影、算偏移、处理断点那一整天的活。2. 拆开 world.jsregisterMap 一行调用背后的三套坐标很多人第一次打开这个文件看到几万行[116.4, 39.9]这样的数组就关掉了。其实抛掉坐标剩下的骨架不到二十行看懂它就知道这份文件该怎么改、能改成什么样。2.1 文件骨架一个 UMD 包装裹着一份 FeatureCollection把坐标数组整段折叠文件长这样// world.js 骨架coordinates 用 ... 省略 (function (root, factory) { if (typeof define function define.amd) { define([exports], factory); // AMD } else if (typeof exports object typeof module ! undefined) { factory(exports); // CommonJS / webpack } else { factory((root.echarts root.echarts || {})); // 浏览器全局 } })(this, function (exports) { var type world; // 注册名必须和 option 里 geo.map 完全一致 var mapData { type: FeatureCollection, features: [ { type: Feature, properties: { name: China }, // 国家名字段tooltip / visualMap 靠它匹配 geometry: { type: Polygon, coordinates: [ /* 一圈闭合的 [经度, 纬度] */ ] } } // ... 剩下两百多个国家和地区 ] }; // 真正干活的只有这一行把 GeoJSON 塞进 echarts 内部的 mapData 仓库 exports.registerMap(type, mapData); });UMD 那三个分支跟地图本身没关系只是为了让同一个文件在 script 标签、webpack、require.js 下都能跑。真正有意义的只有最后一个registerMap(type, mapData)它把整份 FeatureCollection 挂到 ECharts 内部一个以type为键的仓库里。后面geo: { map: world }或series: [{ type: map, map: world }]取值时就是拿这个名字去仓库里捞。mapData是标准 GeoJSONfeatures里每个元素的geometry.type可能是Polygon单块陆地或MultiPolygon群岛、飞地。properties.name是渲染侧做数据匹配的唯一凭据后面要讲的 visualMap 不上色、tooltip 显示 undefined八成都是这个名字对不上。2.2 一次注册之后经纬度是怎么变成屏幕像素的registerMap只负责把数据放好真正把经纬度变成像素发生在渲染阶段中间要过三道换算这是理解「为什么中国能居中」的关键。第一道是包围盒。ECharts 会扫一遍所有坐标算出最小的[minLon, minLat]到[maxLon, maxLat]矩形如果你在 option 里写了boundingCoords就用你给的跳过自动计算。第二道是投影。ECharts 的 geo 默认不做墨卡托就是把经纬度线性映射到画布矩形上——经度值域[-180, 180]线性拉到横向像素纬度值域[90, -90]线性拉到纵向像素再乘一个aspectScale修正长宽比。正因为是线性的你往前推一步就能得出一个很重要的结论谁落在画面中央完全由经度的值域决定跟center参数写多少没关系。原始数据值域是[-180, 180]中间点就是 0°也就是本初子午线中国自然被推到右边。第三道是图形生成。每个环变成一条闭合 Path填充走itemStyle.areaColor描边走borderColor鼠标命中检测也是按 Path 做的。所以「以中国为中心」这件事本质上就是把经度值域从[-180, 180]改成[-75, 285]之类——让 105°E 落在中间。而改法只有两种改 option 里的center只移动视野两边会露白或者改数据本身把经度整体挪一遍。这份 world.js 用的是后者所以它是「数据级」的居中不存在露白问题。2.3 ECharts 4 和 5 的引入方式不一样别照抄旧教程这一条踩坑的人最多。ECharts 4.x 时代echarts/map/js/world.js是随 npm 包一起发的require(echarts/map/js/world)直接能用。5.0 之后地图数据从主包里剥离了那条路径压根不存在import echarts/map/js/world会直接报 Module not found。新项目里常见做法是两条一是把这份 world.js 拷到src/geo/下当本地模块用副作用引入import ./geo/world.js代价是打包体积多出几百 KB二是只留 GeoJSON运行时fetch回来再手动registerMap加载时机可控还能按需拆包。前者省事后者适合大屏首屏有性能要求的情况。提示不管走哪条路registerMap必须发生在echarts.init()之前否则第一次setOption时仓库还是空的图表直接空白。3. 自己造一份用 Node 脚本把 105°E 搬到画布中央拿到手的 world.js 未必符合你的中心经线要求也不太可能和你的数据字段完全对得上。与其改别人的成品不如花二十分钟写个脚本自己生成后面维护心里有底。3.1 先统一字段源数据里国家名可能不叫 name公开的国家边界 GeoJSONproperties里的字段名五花八门有的叫NAME有的叫ADMIN有的干脆是本地语言。ECharts 默认只认小写name不统一就会在渲染阶段静默失败——地图能画出来但着色、tooltip、点击事件全哑火。我一般会在生成脚本里先做一层规整顺手把用不上的字段丢掉能省不少体积// tools/make-world.js —— 第一步字段规整 const fs require(fs); const path require(path); const SRC path.resolve(__dirname, ../geo/source.geo.json); const raw JSON.parse(fs.readFileSync(SRC, utf8)); const features raw.features.map((f) ({ type: Feature, // 只留一个 name大小写和多语言字段全部收敛到这里 properties: { name: f.properties.NAME || f.properties.name || f.properties.ADMIN }, geometry: f.geometry, }));SRC指向你下载的原始 GeoJSONNAME || name || ADMIN是个兜底链实际数据里通常只有一个字段有值。这一步做完后面所有逻辑都只跟properties.name打交道不用再到处判断。选数据源的时候有个取舍110m 精度那份几十万顶点够大屏用50m 精度细节好但体积翻好几倍roam缩放的时候容易卡。我一般先用 110m 把功能跑通真觉得边缘太糙再换。3.2 经度整体平移让 105°E 变成 0°核心就三行数学。把每个经度减去中心经线超出[-180, 180]的部分做一次回绕纬度不动// tools/make-world.js —— 第二步经度平移 const CENTER_LON 105; // 中国大致落在这条经线上 const OFFSET -CENTER_LON; function wrapLon(lon) { let v lon OFFSET; while (v 180) v - 360; // 回绕到左半边 while (v -180) v 360; // 回绕到右半边 return v; } // 递归处理任意深度的坐标环 function shiftRing(ring) { return ring.map(([lon, lat]) [wrapLon(lon), lat]); } function shiftGeometry(geometry) { const { type, coordinates } geometry; if (type Polygon) { return { type, coordinates: coordinates.map(shiftRing) }; } if (type MultiPolygon) { return { type, coordinates: coordinates.map((poly) poly.map(shiftRing)) }; } if (type LineString) return { type, coordinates: shiftRing(coordinates) }; if (type Point) return { type, coordinates: shiftRing([coordinates])[0] }; return geometry; }CENTER_LON这个值决定版式值得单独说一下给105中国正落在画布中央0 度位置断点在 75°W 附近会在美洲中部切开一块给150复刻的是国内纸质世界地图那种版式——大西洋断开欧非在左、美洲在右中国偏左三分之一处视觉上更符合大多数人看地图的习惯。我在大屏上这两种都试过150那种客户接受度更高因为它「看着像地图」105那种在强调「国内节点 全球辐射」的拓扑图里更直观。脚本里把CENTER_LON提成一个常量就是为了随时切换。shiftGeometry用类型分支而不是无脑递归是因为coordinates的嵌套层数跟type强相关Polygon 是「环的数组」MultiPolygon 是「多边形的数组每个多边形又是环的数组」层数差一层结果就是灾难。显式写分支虽然啰嗦但不会错。3.3 断点切环极区数据为什么会被拉成一条横线平移之后最典型的翻车现象地图上横着一条贯穿全图的细线位置看纬度大概在最底下。原因不复杂。一个环从 179° 跨到 -179°本来是两个挨着的点经度平移加回绕之后这两个点可能变成 179 和 181→-179 这种「一头一尾」的坐标。渲染器不知道它是绕地球一圈只看到两个相距 358 度的点于是直接连一条直线过去——那条横线就是这么来的。极区数据最惨因为它本来就围了一圈切成什么样都难看跨经线的岛国也经常中招。标准的修法是在断点处把环拆开每段自己闭合// tools/make-world.js —— 第三步断点切环 function splitRing(ring) { const parts []; let cur []; for (let i 0; i ring.length - 1; i) { const a ring[i]; const b ring[i 1]; cur.push(a); // 相邻两点的经度差超过 180°说明它们分居断点两侧 if (Math.abs(b[0] - a[0]) 180) { const edge a[0] 0 ? 180 : -180; cur.push([edge, a[1]]); // 在断点上补一个点收口 parts.push(cur); cur [[-edge, b[1]]]; // 新的一段从对侧断点起步 } } cur.push(ring[ring.length - 1]); parts.push(cur); return parts.filter((r) r.length 4); // 少于 4 个点构不成多边形 }判定条件Math.abs(b[0] - a[0]) 180是这段逻辑的全部依据地球一圈 360 度两个相邻顶点在真实地理上不可能相隔超过 180 度超过了必然是回绕造成的假象。补的两个点纬度分别取 a 和 b 的纬度是为了让切口是竖直的视觉上贴着画布边缘不会被看出来。filter(r r.length 4)那行别省切完之后经常产生两三个点的碎片留着会在图上渲染出一堆小毛刺。MultiPolygon 切完可能一个变两个要摊平再拼回去function shiftMultiPolygon(polygons) { const out []; polygons.forEach((poly) { poly.forEach((ring) { splitRing(ring).forEach((piece) out.push([piece])); }); }); return { type: MultiPolygon, coordinates: out }; }这里有个简化把所有环都当外环处理。世界地图的国家边界基本没有内环洞这个简化够用真要换成带湖泊、带飞地的数据得保留「第一个环是外环其余是洞」的约定否则湖会被填成陆地。极区那块我一般直接砍掉生成时判断纬度所有顶点都小于 -60 的 feature 直接丢弃。大屏没人看那块留着只会引来那条横线。3.4 落盘拼成 world.js最后一步把处理完的 GeoJSON 套上注册调用写成文件// tools/make-world.js —— 第四步输出 const geo { type: FeatureCollection, features }; const banner /* generated, center${CENTER_LON}E, do not edit by hand */\n; const body echarts.registerMap(world, ${JSON.stringify(geo)});\n; fs.writeFileSync(path.resolve(__dirname, ../src/geo/world.js), banner body); console.log(features:, features.length, bytes:, (banner body).length);JSON.stringify(geo)会把整份数据压成一行文件打开很难看但浏览器解析最快也省掉格式化带来的几十 KB。真需要看结构源数据还在不用看产物。打进 package.json 的 script 里npm run build:geo一条命令重生成后面换数据源、换中心经线都只动常量。console.log打出来的 feature 数和字节数很有用——特征数突然少了说明某一步过滤太狠字节数暴涨通常是切环切出了碎片。4. 接进项目geo 打底、散点叠加与四个必调参数数据准备好之后剩下的就是让它和业务数据在一个坐标系里说话。这块代码不长但参数没配对就是各种别扭。4.1 最小可跑geo 打底 effectScatter lines先用geo组件铺底图再挂两个 series一个画节点一个画飞线// map-init.js import * as echarts from echarts; import ./geo/world.js; // 副作用引入内部已调用 registerMap import { toMapCoord } from ./geo/project.js; const chart echarts.init(document.getElementById(map)); const NODES [ { name: 国内主节点, lnglat: [116.4, 39.9], value: 120 }, { name: 海外节点, lnglat: [-58.4, -34.6], value: 80 }, ]; chart.setOption({ backgroundColor: #0a1a2b, geo: { map: world, // 与 registerMap 的第一个参数一致 roam: true, zoom: 1.15, aspectScale: 1, // 世界地图必须显式给 1 boundingCoords: [[-180, 90], [180, -90]], itemStyle: { areaColor: #12314f, borderColor: #2c5f88, borderWidth: 0.6 }, emphasis: { itemStyle: { areaColor: #1d4f7c } }, }, series: [ { type: effectScatter, coordinateSystem: geo, // 声明用 geo 的坐标系 data: NODES.map((n) ({ name: n.name, value: [...toMapCoord(n.lnglat), n.value], // 关键和地图走同一套平移 })), symbolSize: (v) Math.max(6, Math.sqrt(v[2]) * 1.8), rippleEffect: { scale: 3, brushType: stroke }, label: { show: true, formatter: {b}, position: right, color: #cfe6ff }, }, { type: lines, coordinateSystem: geo, effect: { show: true, period: 4, trailLength: 0.25, symbol: arrow, symbolSize: 6 }, lineStyle: { color: #39d0d8, width: 1, opacity: 0.55, curveness: 0.22 }, data: [{ coords: [toMapCoord([116.4, 39.9]), toMapCoord([-58.4, -34.6])] }], }, ], });geo和series是两层结构geo只画底图和交互series才是数据。想让底图跟着数据变颜色得把type: map的 series 挂在同一个map上而不是改geo——这两者分管视觉和数据混着用就会出现「改了没反应」。coordinateSystem: geo是 series 能找到坐标系的关键漏了这一行散点会按 canvas 像素去定位全挤在左上角。symbolSize用Math.sqrt(value)而不是value是因为人眼对面积敏感、对半径不敏感直接线性映射会让大值看起来夸张得离谱。这是可视化里的常识但写代码时特别容易忘。4.2 四个必调参数boundingCoords、aspectScale、zoom、center同一份世界地图参数不同视觉效果能差一倍。这张表是我踩完坑之后固化的默认值参数作用世界地图上的建议值aspectScale长宽比修正系数1默认0.75会把高纬地区压扁boundingCoords锁死经纬度可视范围[[-180, 90], [180, -90]]避免 roam 之后跑飞zoom初始缩放0.9 ~ 1.2视容器宽高比微调center视野中心经纬度平移后写[0, 0]就是中国roam拖拽与滚轮缩放只读大屏给falseaspectScale默认是0.75这个值是为中国省级地图准备的套到世界地图上整个人类居住带都像被踩了一脚。boundingCoords的作用更隐蔽不写它用户滚轮放大再一拖地图能整个飞出容器回不来的感觉非常糟糕配了之后视野被夹在给定范围内怎么作都还在框里。center值得单独提一句。做了数据级平移之后中国的经度是 0所以center: [0, 0]就能让中国居中——不需要再写[105, 35]这种看着像黑魔法的数字。反过来如果某天你把CENTER_LON改成 150center也要跟着改否则会跑偏。这两个值应该从同一个配置项导出别硬编码两处。4.3 数据侧也要平移把 project 抽成共享模块这一步是整篇文章最容易被忽略、又最容易出错的地方。地图平移了业务数据没平移散点就会跑到屏幕另一半去。解决办法只有一个把平移逻辑抽成模块生成脚本和渲染代码共用。// geo/project.js —— 生成地图和渲染数据共用这一份 export const CENTER_LON 105; // 改这里生成脚本和页面一起生效 export function toMapCoord([lon, lat]) { let x lon - CENTER_LON; while (x 180) x - 360; while (x -180) x 360; return [x, lat]; } // 反解点击地图拿到坐标后换回真实经纬度 export function fromMapCoord([x, lat]) { let lon x CENTER_LON; while (lon 180) lon - 360; while (lon -180) lon 360; return [lon, lat]; }fromMapCoord是给交互用的。chart.on(click)在 geo 上拿到的是屏幕坐标需要chart.convertFromPixel({ geoIndex: 0 }, [x, y])转成地图坐标再反解回真实经纬度才能拿去查库。少了这一步用户点哪儿都是本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。