做数据可视化大屏的同行,尤其是常跟 ECharts 地图打交道的,应该都碰到过这个场景:中国地图注册好了,数据也 setOption 上去了,整体效果不错,偏偏右下角吊着一块小区域,上面标着“南海诸岛”四个字。业务方过来看了一眼,直接问“这块能去掉吗?跟咱们的数据没关系,摆那儿还占地方”。第一次遇到这需求,我翻了半天 ECharts 官方配置,心想是不是有个 show: false 之类的开关,结果发现根本没有。后来把 china.json 拉下来逐条看了数据结构,才彻底搞明白问题出在哪儿。
这篇文章就把这个问题彻底拆开,从 GeoJSON 的数据结构说起,给出几种能落地的隐藏方案,再把隐藏之后地图漂移、缩放比例异常、tooltip 残留这几个连带问题一起解决掉。适合正在做地图可视化、大屏项目,或者单纯被 ECharts 地图细节坑过的人阅读。
1. 为什么右下角会跟着一个“南海诸岛”
1.1 GeoJSON 里其实藏了一个独立区域
ECharts 本身不内置中国地图数据,需要开发者通过echarts.registerMap('china', geoJson)手动注册。而常见的 china.json 里,features数组并不只是 34 个省级行政区,很多版本的数据里额外塞了一些特殊区域,其中就包括一个name为“南海诸岛”的 feature。
可以把这个 GeoJSON 想象成一本画册:每一页是一个区域,封面上写着“中国地图”,但翻到最后一页,里面夹了一张独立的小插图。ECharts 拿到画册后做的事情很简单——逐页去画,所有features数组里的对象都会渲染到 Canvas 上。它不会判断这张插图要不要放在主图右下角,也不关心这个区域有没有跟主区域连在一起,反正画就完了。
为什么这个 feature 偏偏出现在右下角?因为南海诸岛的经纬度范围落在我国南部和东南部海域,换算到平面坐标后,正好落在地图整体边界框的右下区域。所以它并不是“被放在右下角”,而是它本身的地理坐标决定了它只能出现在那里。
1.2 ECharts 渲染逻辑:有多少 feature 画多少
注册完地图后,无论你用的是geo组件还是series-map系列,ECharts 都会遍历 GeoJSON 的features数组,每个 feature 就是一个可绘制、可绑定数据、可响应事件的区域。区域的样式、名称、数据映射,全都围绕 feature 展开。
弄明白这条逻辑,就清楚为什么官方配置项里没有“隐藏南海诸岛”这个按钮了——ECharts 压根没有针对单个 feature 提供显隐开关。如果只是在series.data里没给“南海诸岛”传数据,地图默认还是会画出区域轮廓,只是没有数据值时显示成空白色块。所以想让右下角彻底消失,要么从数据层面把这块区域摘掉,要么用regions配置强行把它的视觉样式清掉,没有第三条捷径。
这里还要提醒一句:不同来源的 china.json,这个区域的名字不一定都一样。有些叫“南海诸岛”,有些叫“中国南海诸岛”,还有些版本把九段线单独拆成了一个 feature。排查的时候要打开 JSON 看一眼实际的name值,别想当然。
2. 方案一:数据层过滤,从根上解决
2.1 用 Node 脚本预处理地图 JSON
我最推荐的做法,是在项目构建之前就把地图数据处理好,把这份“有问题”的 feature 从 GeoJSON 里过滤掉,生成一份干净的地图数据文件。这样前端代码里不需要写任何过滤逻辑,运行时性能也完全不受影响。
先看一个最基础的 Node 脚本:
const fs = require('fs'); // 读取原始地图 JSON const chinaGeo = JSON.parse( fs.readFileSync('./china.json', 'utf-8') ); // 过滤掉名称为“南海诸岛”的 feature chinaGeo.features = chinaGeo.features.filter( (feature) => feature.properties.name !== '南海诸岛' ); // 输出新的地图 JSON fs.writeFileSync( './china-clean.json', JSON.stringify(chinaGeo) ); console.log('处理完成,剩余 feature 数:', chinaGeo.features.length);这段代码里最关键的就是filter这一行。很多朋友图省事,会写成includes('南海'),结果把名字里带“南海”的其他区域也误删了。我自己就吃过一次亏。所以这里建议用精确匹配,或者至少做成一个名单数组,把各种来源的命名变体都放进去:
const hiddenNames = ['南海诸岛', '中国南海诸岛', '十段线', '九段线']; chinaGeo.features = chinaGeo.features.filter( (feature) => !hiddenNames.includes(feature.properties.name) );脚本跑完之后,生成一个china-clean.json,前端直接引用这个文件,一步到位。
2.2 在页面加载时动态过滤
如果地图数据不是本地静态文件,而是从接口或 CDN 动态拉取的,不方便提前预处理,那就在前端拿到数据之后、注册地图之前做一次过滤,效果一样。
fetch('./china.json') .then((res) => res.json()) .then((geoJson) => { // 在 registerMap 之前过滤数据 const filteredGeo = { ...geoJson, features: geoJson.features.filter( (item) => item.properties.name !== '南海诸岛' ) }; echarts.registerMap('china', filteredGeo); renderMap(); });这种做法灵活,数据源随时可以换,不用维护构建脚本。但要注意别在registerMap已经执行之后再改features数组,那样 ECharts 内部已经存下了引用,改了不一定生效。务必在注册之前完成过滤。
2.3 过滤后必须重设的地图参数
数据过滤只是第一步。真正隐蔽的坑在这里:原始 china.json 里因为有南海诸岛,整个地图的边界范围(bounding box)会被向右下角撑大。一旦去掉这块区域,包围盒明显变小,ECharts 默认会把地图重新适配到整个容器里——表现就是地图突然变大,或者整体位置偏移,甚至比例看起来不对劲。
解决方法是给geo或series-map显式指定layoutCenter和layoutSize,把地图主体固定在你想要的位置和大小。我常用的配置如下:
const option = { geo: { map: 'china', roam: true, // 以容器中心为基准 layoutCenter: ['50%', '50%'], // 控制地图占容器的比例 layoutSize: '90%', itemStyle: { areaColor: '#11204d', borderColor: '#3d7eff' }, emphasis: { itemStyle: { areaColor: '#2a5fd0' } } } };layoutSize的值我一般从 100% 往低调,观察地图在屏幕上的大小,调到合适为止。如果是省级地图或者只需要展示部分区域,可以改用center和zoom来控制视角。
注意:如果同时设置了
layoutCenter和center,ECharts 的坐标系会以layoutCenter为准。两者不要混用,否则定位会出现诡异偏差。
3. 方案二:regions 配置隐藏,适合临时应急
3.1 geo 组件和 series-map 的 regions 写法
如果不想改动地图数据,ECharts 也提供了一种视觉层面的隐藏手段:通过regions配置单独控制某个区域的样式。这个办法适合临时应急,比如图表已经上线了,不改数据文件,只在前端配置里快速处理一下。
geo组件里的写法:
const option = { geo: { map: 'china', roam: true, regions: [ { name: '南海诸岛', itemStyle: { areaColor: 'transparent', borderColor: 'transparent' }, label: { show: false }, emphasis: { itemStyle: { areaColor: 'transparent', borderColor: 'transparent' }, label: { show: false } } } ] } };series-map里的写法几乎一样,只是regions直接放在 series 里:
const option = { series: [ { type: 'map', map: 'china', regions: [ { name: '南海诸岛', itemStyle: { areaColor: 'transparent', borderColor: 'transparent' } } ] } ] };这套配置的核心思路是:区域还在数据里,但所有可见的样式都变成透明,包括悬浮高亮的状态也一并处理掉。这样视觉上就看不到右下角那块了。
3.2 视觉隐藏方案的两个典型坑
这个方案看着方便,实际用的时候有两个坑,我必须说在前面。
第一个坑:areaColor透明了,但区域还是实实在在存在于画布上。如果用户的鼠标刚好移到右下角那片空白区域,ECharts 仍然会触发鼠标事件,某些版本里还会出现高亮色的区域轮廓。要杜绝这种情况,需要把emphasis状态也覆盖成透明,并且最好在geo上把silent设为true。但silent会影响所有区域的事件响应,如果其他省份还需要点击、悬浮效果,这个方法就不合适了。
第二个坑:数据层面的占位问题没有解决。比如地图要绑定series.data,数据里虽然没给南海诸岛传值,但区域的边界仍然占用了包围盒。这就意味着 map 的缩放级别、中心点计算仍然会把这块区域算进去,最终地图整体显示范围还是会被它影响。说到底,这只是把颜色变透明,并没有真正“去掉”这块区域。
所以我对这个方案的态度很明确:只适合调试、只适合临时看效果,不建议放进生产环境长期跑。
4. 方案三:换一份不含该区域的地图数据
4.1 按需拼接省市边界数据
除了在原有数据上做减法和视觉隐藏,还有一种思路是直接从源头上换数据。国内不少公开地图数据源会按省份分别提供 GeoJSON,这种拆分后的数据本身就不存在“南海诸岛”这个独立 feature。
常见做法是循环请求各省份的边界 GeoJSON,然后把所有features拼成一个数组,再注册成一张地图。代码大致长这样:
const adcodeList = [ '110000', '120000', '130000' // 这里放所有需要的省级行政区划代码 ]; const features = await Promise.all( adcodeList.map((code) => fetch(`https://geo.datav.aliyun.com/areas_v3/bound/${code}.json`) .then((res) => res.json()) .then((json) => json.features[0]) ) ); const mergedGeo = { type: 'FeatureCollection', features }; echarts.registerMap('china', mergedGeo);这种方式的优点是特别干净,不需要额外过滤,而且拼接哪个省、不放哪个省,完全由你自己控制。缺点也明显:请求次数多,前端要处理异步加载顺序,数据总大小比单份 JSON 更大。如果你的页面走的是 HTTP/2,并且有 loading 状态兜底,体验上问题不大;但如果是纯静态页面、没有任何加载态,要慎重。
4.2 数据源与量级的取舍建议
选择地图数据源时,要考虑项目本身的定位。如果只做省级地图展示,按需拼接是最灵活的;如果要做全国下钻到市级的完整地图,一次性加载全国 GeoJSON 仍然是更高效的选择,这时候数据过滤方案就更合适。
还有一种常见组合:基础底图用一份不带南海诸岛的全国简化数据,下钻到省的时候再单独请求省市级数据。下钻数据都是从官方或公开渠道拉的,不存在右下角区域问题。这样既避免了初始加载时出现多余区域,也不影响地图交互的下钻体验。我做过一个需要下钻到区县的项目,就是用这个思路落地的,整体很顺。
5. 隐藏之后的地图优化与常见问题排查
5.1 地图整体漂移、缩放比例不对怎么办
这是隐藏“南海诸岛”之后最常遇到的问题。原因前面已经讲过:原始地图的包围盒因为包含南海区域,范围很大。去掉之后,ECharts 默认重新按新包围盒适配画布,导致地图视觉上变大、偏移。
处理方式有两个思路。一个是用layoutCenter和layoutSize固定地图的位置和大小,这也是大屏项目里最常用的方式。另一个思路是手动指定center和zoom,比如:
const option = { geo: { map: 'china', center: [104.0, 35.0], zoom: 1.2 } };center是一个经纬度数组,zoom是缩放级别。这种方式适合你知道地图要聚焦在什么位置的情况,但它跟用户的roam操作会有一些交互上的纠缠,需要自己调几次才能找准手感。相比之下,layoutCenter配合layoutSize对大屏来说更可控,百分比定位也更适配不同分辨率的屏幕。
5.2 过滤后地图仍显示旧区域,多半是缓存
遇到过一种很迷惑的现象:代码里明明已经过滤掉了南海诸岛,刷新页面后地图右下角还是有那块区域。排查半天,最后发现是浏览器缓存了旧的china.json。尤其是本地开发用的静态服务器或者 CDN,默认缓存策略比较宽松,地图数据这种不常变的文件很容易被缓存住。
解决思路很直接:开发阶段在请求地址后面拼一个随机参数,比如china.json?_t=Date.now();生产环境通过构建工具给文件名加 hash。另外还有一个容易忽略的点,就是registerMap多次注册同名的地图。如果项目里通过路由切换页面,每次进入都重新执行registerMap,后一次注册会把前一次的覆盖掉,但图表实例可能还保留旧的地图数据。这时候需要chart.clear()或者chart.dispose()之后重新初始化。
5.3 过滤时误删了其他区域怎么办
过滤逻辑如果写得粗糙,很容易误伤。最常见的是用includes('南海')去匹配,结果把所有名称带“南海”的 feature 全部删掉,比如某个版本数据里把“南海诸岛”和“十段线”做成了两个 feature,甚至还有仅包含边界线段的独立 feature。这些名称里都带“南海”或“诸岛”,全删了倒是省事,但可能会把边界线也删掉,导致地图外轮廓出现缺口。
更稳妥的做法是维护一个明确的名单,同时对 feature 的geometry.type做一次判断。Polygon和MultiPolygon是常规的区域,可以过滤;LineString或MultiLineString是边界线段,是否要删取决于业务需求。如果只想隐藏填充区域,但保留右下角那几段边界线,可以让线段类 feature 继续保留;如果是想完全去掉右下角的视觉元素,就把它们一并过滤。这个判断逻辑写在前面给的 Node 脚本里,只需要扩展几行。
const shouldRemove = (name, geometryType) => { if (geometryType === 'LineString' || geometryType === 'MultiLineString') { // 如果连边界线也想隐藏,取消下面一行的注释 return true; // return false; // 保留边界线的情况 } return ['南海诸岛', '中国南海诸岛'].includes(name); }; chinaGeo.features = chinaGeo.features.filter((feature) => { const name = feature.properties.name; const geometryType = feature.geometry.type; return !shouldRemove(name, geometryType); });5.4 tooltip、高亮状态与数据联动问题
用regions做视觉隐藏时,右下角空白区域还可能出现 tooltip 残留,鼠标移过去会显示一块空白浮层。这种情况只能靠把emphasis样式也设置透明,同时针对那个区域把tooltip的formatter判断掉。但说实话,这属于补丁式写法,补一个漏一个,还是回到数据过滤才是最稳的。
如果用了数据过滤方案,还需要注意数据联动的问题。比如series-map的data数组里如果还有name: '南海诸岛'的数据项,而地图里已经没有这个 feature 了,控制台会报警告,图表顶多忽略掉这个数据项,不影响整体渲染。但如果你的数据是从后端动态生成、并且按照地图所有区域自动填充的,那就要在后端或者前端统一过滤掉这个 name,避免多余数据项。
我自己现在的习惯是:在后端生成地图统计数据时,就对“南海诸岛”这个 key 做排除,前端拿到的就是干净的数据。这样不止 ECharts 地图,包括其他可视化组件、表格展示,都不会再出现这个多余区域,一套逻辑多处复用。
最后再分享一个小技巧:不管用哪个数据源、哪个方案,我建议在地图数据处理的脚本里统一维护一份“隐藏名单”和“过滤规则”,而不是每次临时写一句!== '南海诸岛'。因为地图数据源不应该在业务代码里散落得到处都是,一旦以后换了数据源,或者又要过滤澳门、香港等特殊区域,直接改名单就行,不用翻遍整个项目找过滤逻辑。地图这种基础组件,维护成本低一点,比什么花活都重要。