news 2026/10/27 17:52:27

ECharts世界地图中国居中:GeoJSON经度平移与断点切环实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ECharts世界地图中国居中:GeoJSON经度平移与断点切环实践

简介:面向前端开发与数据可视化从业者的 ECharts 世界地图数据包,重点提供以中国为地理中心的世界地图脚本,并配套中国省级区域地图数据,适合制作涉华数据分析、全球业务看板与地理关系展示。压缩包共 420 个文件,以 380 个 JSON 地图数据和 40 个 JS 脚本为主,整体约 4.34MB;JSON 便于按区域加载与复用,JS 可直接在页面中引入并注册地图。已有 3155 人学习下载,说明其在 ECharts 地图类资源中具有一定参考价值。读者可据此快速完成地图容器初始化、geo 中心点与缩放平移配置、标签高亮和区域样式调整,并将业务数据绑定到 series 进行渲染,减少自行搜集世界及省级边界数据、处理坐标与名称匹配的工作量,适合搭建中国视角的全球数据可视化页面。

1. 让中国落在画面正中:一份替换即生效的 echart 世界地图数据

大屏做到第三版,业务方指着屏幕说了一句:能不能让中国在中间。默认的 echart 世界地图 是拿本初子午线当中心线的,中国被挤到画面右侧三分之一的位置,太平洋对岸那块大陆横在最左边,怎么看怎么别扭。这份以中国为中心的 world.js 就解决这一件事——它是一份已经registerMap好的世界地图数据,覆盖掉原来的文件,option 几乎不用动,中国就落到画面正中。

它适合正在做大屏、做海外节点分布图、又不想为了一个投影去啃整套 GIS 的前端同学。需要提前说清楚:这是一份数据文件,不是框架,不含任何组件封装;它的价值在于省掉你查投影、算偏移、处理断点那一整天的活。

2. 拆开 world.js:registerMap 一行调用背后的三套坐标

很多人第一次打开这个文件,看到几万行[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是标准 GeoJSON,features里每个元素的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

公开的国家边界 GeoJSON,properties里的字段名五花八门:有的叫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指向你下载的原始 GeoJSON,NAME || 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拖拽与滚轮缩放只读大屏给false

aspectScale默认是0.75,这个值是为中国省级地图准备的,套到世界地图上整个人类居住带都像被踩了一脚。boundingCoords的作用更隐蔽:不写它,用户滚轮放大再一拖,地图能整个飞出容器,回不来的感觉非常糟糕,配了之后视野被夹在给定范围内,怎么作都还在框里。

center值得单独提一句。做了数据级平移之后,中国的经度是 0,所以center: [0, 0]就能让中国居中——不需要再写[105, 35]这种看着像黑魔法的数字。反过来,如果某天你把CENTER_LON改成 150,center也要跟着改,否则会跑偏。这两个值应该从同一个配置项导出,别硬编码两处。

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])转成地图坐标,再反解回真实经纬度才能拿去查库。少了这一步,用户点哪儿都是

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 17:51:58

接入大模型API前必须验证的五件事

1. 项目概述&#xff1a;为什么“接入多模型API前看五件事”不是 checklist&#xff0c;而是生存指南“接入多模型API前我会先看这五件事”——这句话在最近三个月的工程师茶水间、技术群和内部分享会上出现频率高得反常。它不像一句口号&#xff0c;倒像一个刚从火线撤下来的运…

作者头像 李华
网站建设 2026/10/27 17:51:39

终端保密检查工具:Windows安全基线PowerShell核查与复检

简介&#xff1a;终端保密检查工具.zip 是一套面向 Windows 终端安全自查与保密合规场景的轻量工具包&#xff0c;适合单位保密检查、终端风险排查及日常安全巡检人员参考使用。压缩包共 9 个文件&#xff0c;整体约 3.92MB&#xff0c;主要由 4 个 dll 动态库、2 个 ids 标识文…

作者头像 李华
网站建设 2026/10/27 17:50:55

Windows终端保密检查工具安装配置与零命中排查

简介&#xff1a;这款「终端保密检查工具」面向 Windows 平台的终端保密自查与合规检查场景&#xff0c;适合单位保密管理人员、安全运维人员及需要做终端风险排查的读者参考使用&#xff0c;可用于整理检查工具、验证运行环境与了解基础检查流程。压缩包共 9 个文件&#xff0…

作者头像 李华
网站建设 2026/10/27 17:49:55

PC端网页录音实战:麦克风采集、实时波形与分片上传

简介&#xff1a;一套围绕 H5 音频能力的前端实战示例&#xff0c;面向需要实现浏览器端麦克风采集、实时音频流处理与录音上传的开发者&#xff0c;尤其适合 PC 端 Web 项目。内容借助 Web Audio API 与 getUserMedia 取得实时音频流&#xff0c;再用 MediaRecorder 分片录制并…

作者头像 李华
网站建设 2026/10/27 17:49:28

ECharts智慧交通大屏源码实战:从数据模型到性能优化

简介&#xff1a;数据可视化是智能业务系统升级的重要基础&#xff0c;当信息需要承载到指挥中心大屏上时&#xff0c;多图表协同、秒级感知和数据稳定就成了关键。大屏不同于普通报表&#xff0c;它要求前端工程在有限屏幕内高效呈现车流趋势、区域拥堵与状态指标。ECharts作为…

作者头像 李华
网站建设 2026/10/27 17:49:08

深度学习训练-推理不匹配问题与学习率调度优化

1. 问题背景与核心挑战在深度学习模型训练过程中&#xff0c;我们常常会遇到一个棘手的问题&#xff1a;模型在训练集上表现优异&#xff0c;但在实际推理&#xff08;inference&#xff09;阶段却出现性能下降。这种现象被称为"训练-推理不匹配"&#xff08;Trainin…

作者头像 李华