简介:这份地图数据包面向使用 ECharts 做中国区域可视化的大数据开发者与前端工程师,提供全国省级地图及各省下辖城市地图的 JSON 边界数据,可直接接入 ECharts 的 geo 与 series 配置,用于人口、经济指标等统计数据的区域分布展示。数据以行政区划为单元组织,适合中级前端与数据可视化学习者按需取用。压缩包共 863 个文件,以 610 个 JSON 地图数据和 250 个 JS 脚本为主,另有说明文档与工程配置文件,整体约 12.62MB,按省份与城市分层组织,便于按行政区划检索调用。目前已有 14521 人学习下载。读者可据此快速搭建省—市两级地图底图,将区域名称与数值绑定到地图系列,结合颜色深浅、分段图例、悬浮提示、缩放平移与高亮交互呈现区域差异;JSON 边界坐标和 JS 脚本可直接复用,省去地图数据搜集、格式转换与坐标校对环节,适合学术研究、商业分析、公众信息展示等需要中国区域数据可视化的场景。
1. 从一张全国地图点进某个省:这套东西到底解决什么问题
在一个区域经营看板上,最常见的需求就是这个:默认看到全国各省,点某个省,地图换成这个省的市级行政区划,再点一下返回,回到全国。做数据可视化的人几乎都接过这种活儿。这个项目把 ECharts 的全国省级地图和每个省对应的市级地图做在了一起,一套代码完成两级下钻,数据文件按省拆分,换一个省的地图就是换一个 JSON。它不是那种把几百个特性堆上去的模板,而是把「地图数据怎么组织、下钻状态怎么管、业务名字和边界名字怎么对齐」这三件真正会卡住人的事讲清楚了。正在做区域看板、渠道分布大屏、物流覆盖分析的开发者,拿过去改几个颜色和字段就能用;刚接触 ECharts 地图、想找一个能直接跑起来的骨架的人,也适合从这份代码入门。
2. 技术栈与目录:省级、市级两套边界数据怎么被同一套代码吃下
整套东西没有后端,地图数据全是静态文件,前端只负责加载和渲染。这种结构的好处是随便丢到一个静态服务器上就能跑,原型阶段能省掉一堆接口联调的事;代价是所有地图边界都得自己准备好,而且命名必须严谨,一个字不对就是整块地图空白。先看依赖和目录,这是决定后面好不好改的地基。
2.1 依赖与环境:核心依赖只有 ECharts 一个
工程用 Vite 起,package.json里能看到的运行时依赖就是 ECharts 一个。这个选择很实际:地图系列的边界数据不需要打包工具处理,把它当静态资源请求就行;而 ECharts 本身整包引入体积确实大,但地图 + visualMap + 散点这套组合拳按需引入的收益不高,反而容易漏模块导致type: 'map'不认。
{ "name": "echarts-map-drill", "private": true, "type": "module", "scripts": { "dev": "vite --host", "build": "vite build", "preview": "vite preview" }, "dependencies": { "echarts": "^5.5.0" }, "devDependencies": { "vite": "^5.2.0" } }type: "module"让所有.js文件按 ESM 解析,写 import 不用加.mjs后缀;vite --host是为了让同事用局域网 IP 也能打开预览,地图这种宽屏的东西在大显示器上看效果才准。Node 建议 18 以上,Vite 5 对低版本 Node 不友好,这个不值得省。
真正有讲究的是 GeoJSON 的位置。它不能放在src/下面用import进来,一个省的市级边界动辄几百 KB,全国三十多个省全打进 bundle 会让构建时间翻倍,而且用户打开页面时根本用不到别的省。常见做法是放进public/maps/,运行时拼 URL 去fetch,换地图文件连构建都不需要重新跑。
echarts-map-drill/ ├── public/ │ └── maps/ │ ├── china.json # 全国,feature 是省 │ ├── guangdong.json # 广东,feature 是市 │ ├── zhejiang.json # 浙江,feature 是市 │ └── ... ├── src/ │ ├── maps/ │ │ ├── index.js # 中文省名 -> 注册名 │ │ └── loader.js # 按需 fetch + registerMap │ ├── chart/ │ │ ├── render.js # setOption 封装 │ │ └── drill.js # 下钻状态管理 │ └── main.js ├── index.html └── package.json2.2 目录结构:数据层、注册层、视图层三层分开
上面这棵树看着简单,但三层的职责边界是刻意划开的,混在一起改起来会非常难受。
public/maps/只放数据,文件的命名规则是「注册名.json」,全国固定叫china.json,各省用拼音全拼。这份数据从公开的地图边界数据源下载后基本不用改,唯一要注意的是某些数据源的省级文件里会带上「南海诸岛」这类子区域,布局会偏,删掉或者单独提出来处理。
src/maps/负责「名字到文件」的翻译和注册,它不知道图表长什么样,只管把一段 GeoJSON 变成 ECharts 能认的注册项。这一层是整个项目里最该独立的东西,因为它的错误表现是「地图不显示」而不是「报错」,调试成本极高。
src/chart/往下才是视图,render.js管 option 怎么拼,drill.js管当前在第几层。这层依赖数据层,反过来不成立,所以后期想把「点省下钻」改成「下拉框切省」,只需要动drill.js和main.js。
2.3 注册表:中文省名到 GeoJSON 文件名的映射必须照抄
这是第一个容易翻车的地方。ECharts 的点击回调给你的params.name是 GeoJSON 里properties.name的原值,通常是「广东省」「内蒙古自治区」这种带后缀的全称,而文件名一般是拼音。中间必须有一张表,这张表的键要和china.json里的名字逐字一致。
// src/maps/index.js // 键必须和 china.json 里 feature.properties.name 完全一致 export const PROVINCE_INDEX = { '北京市': 'beijing', '天津市': 'tianjin', '河北省': 'hebei', '山西省': 'shanxi', '内蒙古自治区': 'neimenggu', '辽宁省': 'liaoning', '吉林省': 'jilin', '黑龙江省': 'heilongjiang', '上海市': 'shanghai', '江苏省': 'jiangsu', '浙江省': 'zhejiang', '安徽省': 'anhui', '福建省': 'fujian', '江西省': 'jiangxi', '山东省': 'shandong', '河南省': 'henan', '湖北省': 'hubei', '湖南省': 'hunan', '广东省': 'guangdong', '广西壮族自治区': 'guangxi', '海南省': 'hainan', '重庆市': 'chongqing', '四川省': 'sichuan', '贵州省': 'guizhou', '云南省': 'yunnan', '西藏自治区': 'xizang', '陕西省': 'shaanxi', '甘肃省': 'gansu', '青海省': 'qinghai', '宁夏回族自治区': 'ningxia', '新疆维吾尔自治区': 'xinjiang', '台湾省': 'taiwan', '香港特别行政区': 'xianggang', '澳门特别行政区': 'aomen' }; // 反向表:返回上一级时用注册名找回中文名 export const NAME_BY_KEY = Object.fromEntries( Object.entries(PROVINCE_INDEX).map(([cn, key]) => [key, cn]) );这张表里有个血泪经验:山西和陕西。两个省的拼音在无调拼音里完全一样,很多地图数据源自己也糊涂。工程上必须用shanxi和shaanxi区分,后者是陕西的规范拼写,多一个 a。如果下载来的文件就叫shanxi.json,宁可重命名也别在代码里做特殊判断,那种写法三个月后自己都看不懂。
NAME_BY_KEY这张反向表看着可有可无,但它决定了面包屑和返回按钮的文字显示,靠PROVINCE_INDEX现查会导致每次渲染都要遍历一遍对象的键,虽然量小但不干净。
2.4 加载器:按需请求并防止连点重复下载
地图注册这一步的核心是echarts.registerMap(name, geoJson),第二个参数的结构必须是{ type: 'FeatureCollection', features: [...] }。很多数据源下载下来外层还包了一层{ code: 0, data: {...} },直接塞进去 ECharts 不报错,只是画不出来,这是最坑的一种失败方式。
// src/maps/loader.js import * as echarts from 'echarts'; const inflight = new Map(); // 进行中的请求,防止连点造成重复下载 export function hasMap(registerName) { return Boolean(echarts.getMap(registerName)); } export async function ensureMap(registerName) { if (hasMap(registerName)) return registerName; if (inflight.has(registerName)) return inflight.get(registerName); const task = (async () => { // BASE_URL 保证部署在子路径下也能取到文件 const url = `${import.meta.env.BASE_URL}maps/${registerName}.json`; const res = await fetch(url); if (!res.ok) { throw new Error(`[map] ${registerName} 加载失败,HTTP ${res.status}`); } const raw = await res.json(); // 兼容外层包了一层的写法 const geoJson = raw.data ?? raw; if (!Array.isArray(geoJson.features) || geoJson.features.length === 0) { throw new Error(`[map] ${registerName}.json 结构不对,features 为空`); } echarts.registerMap(registerName, geoJson); inflight.delete(registerName); return registerName; })(); inflight.set(registerName, task); return task; }几个参数值得单独说。echarts.getMap(name)返回的是已注册的 GeoJSON,没注册返回undefined,用它判断能避免重复registerMap,重复注册在 5.x 里会覆盖,但控制台会一直刷警告,噪音很大。
inflight这张表解决的是连点问题。用户手快,在请求还没回来的时候连点两下同一个省,没有这张表就是两次网络请求,加上注册顺序不可控,很容易出现「先回来的后注册」导致画面闪一下。
import.meta.env.BASE_URL是 Vite 注入的部署基路径,如果你的项目部署在/dashboard/下面而不是根路径,不加这个前缀就是一片 404。这个坑在本地开发时完全看不出来,一上测试环境就炸。
提示:任何「地图加载了但显示空白」的排查,第一步都是把
echarts.getMap(name)的返回值打印出来看features.length,而不是盯着 option 反复改。
3. 下钻链路:从全国点到某个省,一次点击里发生了什么
下钻这件事本身的代码量不大,难在状态。全国是一层,省是第二层,用户可能在两层之间来回跳,还要处理返回按钮、面包屑、浏览器前进后退。最常见的失败写法是每次点击都重新init一个图表实例,跑起来看着能用,切几次就开始卡,最后内存里堆着七八个 canvas。
3.1 初始化:一个 DOM 只 init 一次
// src/chart/init.js import * as echarts from 'echarts'; export function getChart(dom) { // 同一个 DOM 重复 init 会出现两层 canvas,这是"残影"的主要来源 const exist = echarts.getInstanceByDom(dom); if (exist) return exist; return echarts.init(dom, null, { renderer: 'canvas', devicePixelRatio: window.devicePixelRatio }); }getInstanceByDom是官方给的兜底方法,有实例就复用,没有才创建。renderer选 canvas 而不是 svg,是因为地图的路径点太多,svg 在低配机器上 hover 会明显掉帧。devicePixelRatio显式传是为了在 2 倍屏上文字不发虚,虽然默认值也是取这个,但写在代码里,换人接手时不用猜。
容器的尺寸必须有明确高度。地图和折线图不一样,父容器高度是auto的时候 ECharts 算出来的画布高度可能是 0,表现就是「代码全跑通了,页面上什么都没有」。
3.2 全国地图的 option:四块配置各管什么
// src/chart/render.js export function buildMapOption(registerName, seriesData = []) { return { tooltip: { trigger: 'item', formatter: (p) => `${p.name}<br/>数值:${p.value ?? '暂无'}` }, visualMap: { min: 0, max: 100, left: 16, bottom: 16, text: ['高', '低'], inRange: { color: ['#e8f3ff', '#1f6feb'] }, calculable: true }, series: [ { type: 'map', map: registerName, roam: true, // 允许缩放和拖动 zoom: 1.15, label: { show: true, fontSize: 10 }, itemStyle: { borderColor: '#ffffff', borderWidth: 0.6, areaColor: '#f2f6fc' }, emphasis: { label: { show: true }, itemStyle: { areaColor: '#ffd666' } }, data: seriesData // [{ name: '广东省', value: 62 }] } ] }; }map字段就是注册时用的名字,写错一个字地图直接空白且不报错。roam: true放开缩放拖动,看大屏的人习惯滚轮放大看细节;但要注意它会让点击热区跟着变,如果同时用了geo组件,两边的roam要一致,不然会出现「地图放大了但散点没动」。
label.fontSize给 10 是因为全国图里省名本身就密,字号大了会互相压。itemStyle.borderColor用白色是为了在深色背景上勾出省界,这个细节决定了截图好不好看。emphasis.itemStyle.areaColor是 hover 高亮色,默认的黄色偏刺眼,换成暖橙更耐看。
data的每一项必须有name字段,且要和 GeoJSON 里的区域名对上,value决定 visualMap 的着色。对不上的项不会报错,只是那块区域保持默认色,这种「部分省份有颜色、部分没有」的现象基本都是名字问题。
3.3 点击事件:params 里真正有用的只有三个字段
// src/main.js import { getChart } from './chart/init.js'; import { buildMapOption } from './chart/render.js'; import { drillTo } from './chart/drill.js'; import { ensureMap } from './maps/loader.js'; const dom = document.getElementById('map'); const chart = getChart(dom); async function boot() { await ensureMap('china'); chart.setOption(buildMapOption('china'), true); } chart.on('click', (params) => { // 只处理地图区域点击,visualMap 的点击也会触发这个回调 if (params.componentType !== 'series') return; drillTo(chart, params.name); }); boot();componentType这个判断非常必要。chart.on('click')是全局事件,用户点 visualMap 的滑块也会走进来,这时候params.name可能是undefined或者图例文字,不拦掉就会触发一次莫名其妙的下钻。params.name是区域全称,params.value是这一项的数据值(没配 data 就是undefined),params.seriesIndex在多系列时用来区分点的是地图还是散点。
3.4 下钻与回退:用一个栈记住"我现在在哪一层"
// src/chart/drill.js import { PROVINCE_INDEX } from '../maps/index.js'; import { ensureMap } from '../maps/loader.js'; import { buildMapOption } from './render.js'; // 栈底永远是全国,栈顶是当前层 const stack = [{ key: 'china', label: '全国' }]; export async function drillTo(chart, clickedName) { const key = PROVINCE_INDEX[clickedName]; if (!key) return; // 点到了没有数据的区域,直接忽略 await ensureMap(key); stack.push({ key, label: clickedName }); chart.setOption(buildMapOption(key), true); syncBreadcrumb(); } export function drillBack(chart) { if (stack.length <= 1) return; // 已经在最顶层,不做任何事 stack.pop(); const top = stack[stack.length - 1]; chart.setOption(buildMapOption(top.key), true); syncBreadcrumb(); } function syncBreadcrumb() { const el = document.getElementById('breadcrumb'); if (!el) return; el.textContent = stack.map((s) => s.label).join(' / '); }栈里存的是{ key, label }而不是单纯的字符串,因为返回时既要重新渲染(需要注册名),又要更新面包屑(需要中文名),分开存省得回头再查表。
if (!key) return这一行挡掉了「南海诸岛」这类在 GeoJSON 里存在、但没有对应省级文件的区域。不拦的话会请求一个 404 的 JSON,Promise 抛错,页面上表现为点一下卡住。
setOption(option, true)的第二个参数notMerge必须传true。这是下钻功能里最容易漏的一行,不传的话 ECharts 会把新旧 series 合并,表现是返回全国之后地图上面还叠着上一个省的轮廓,或者颜色错乱。这个问题不会报错,只能靠眼睛发现,属于典型的「后悔药」型 bug。
drillBack里没有重新fetch,因为ensureMap第一次就注册过了,echarts.getMap里还在,直接setOption就能出图。这也是把注册和渲染拆开带来的好处,返回操作是纯内存操作,响应几乎是瞬时的。
4. 市级地图渲染:业务数据和边界数据对不上的时候怎么办
省级地图的 feature 是各个市,渲染逻辑和全国图一模一样,真正麻烦的是数据。业务库里存的可能是「深圳」「广州」,地图边界给的是「深圳市」「广州市」,还有数据源写「XX市市辖区」这种。三套名字摆在一起,visualMap 不出色是必然的。
4.1 名称归一化:一套 replace 规则对齐两边
// src/maps/normalize.js // 把 "广州市" / "广州" / "广州市市辖区" 统一成 "广州" export function normalize(name = '') { return String(name) .replace(/(特别行政区|自治区|自治州|地区|省|市|盟|县)$/g, '') .trim(); } // 按归一化后的名字把业务值灌到地图 features 上 export function alignSeries(geoJson, rows, valueKey = 'value') { const byName = new Map(rows.map((r) => [normalize(r.name), r[valueKey]])); return geoJson.features.map((f) => { const cn = f.properties.name; return { name: cn, value: byName.get(normalize(cn)) ?? null }; }); }normalize的正则里把「市」放在最后一位处理,是因为「市辖区」这种词如果先命中「市」会剩个「辖区」。「自治州」「地区」「盟」是给地级行政区用的,做省级下钻到市级时用得着。
alignSeries的入参geoJson就是echarts.getMap(registerName)的返回,rows是后端返回的数组,形如[{ name: '深圳市', value: 88 }]。返回的value用?? null而不是?? 0,因为 0 在 visualMap 里是有颜色的,null才会保持默认灰,视觉上能一眼看出哪些城市没数据。
提示:如果某块区域明明有数据却还是灰的,把
geoJson.features.map(f => f.properties.name)打出来和业务数据的 name 做一次差集,问题立刻现形。
4.2 geo 组件和 map series 的分工:散点叠加时不要两个都画地图
只画行政区着色的时候,一个type: 'map'就够了。但只要想在图上叠气泡、飞线或者图标,就会碰到一个经典问题:type: 'scatter'配coordinateSystem: 'geo',但 option 里根本没有geo组件,散点无处安放,直接不显示。
正确的写法是在 option 里声明一个geo组件,然后让地图 series 通过geoIndex复用它,而不是各画各的。
// 地图 + 散点叠加的正确写法 export function buildWithScatter(registerName, seriesData, points) { return { geo: { map: registerName, roam: true, layoutCenter: ['50%', '50%'], layoutSize: '96%', itemStyle: { areaColor: '#f2f6fc', borderColor: '#fff' }, label: { show: true, fontSize: 9 } }, visualMap: { min: 0, max: 100, inRange: { color: ['#e8f3ff', '#1f6feb'] }, left: 16, bottom: 16 }, series: [ { type: 'map', geoIndex: 0, // 关键:复用上面那个 geo,不再自己画一遍 data: seriesData }, { type: 'effectScatter', coordinateSystem: 'geo', geoIndex: 0, symbolSize: (val) => Math.max(8, val[2] / 6), rippleEffect: { scale: 3, brushType: 'stroke' }, data: points // [[113.26, 23.13, 88], ...] } ] }; }geo组件负责画底图(坐标、缩放、标签),mapseries 通过geoIndex: 0挂上去只管着色,effectScatter也用同一个geoIndex找坐标系。三者的roam由geo统一控制,不会出现「地图放大了散点没动」。
散点的坐标顺序是[经度, 纬度, 数值]。写成[纬度, 经度]的话点会跑到境外去,因为经度范围是 ±180、纬度是 ±90,落在国内的点纬度一定是 20~50 之间,反过来会超出经度范围,看起来就是地图外面飘了一堆点。GeoJSON 标准也是[lng, lat],两边保持一致。
4.3 加载策略:按需拉 vs 一次性预加载
省份多了以后要选一种策略。三十多个省的市级 GeoJSON,全部加起来几十 MB,让用户打开页面就全下完显然不现实。
按需加载是默认选择,也是上面ensureMap的做法:点到哪个省才拉哪个,配合浏览器缓存,用户回访同一个省基本是秒开。缺点是第一次点某个省时会有一两百毫秒的空白,体验上能感觉到。想补这个空白,可以在hover的时候预取,而不是等click:
// 鼠标悬停时预取,点击时基本就能直接出图 let hoverTimer = null; chart.on('mouseover', (params) => { if (params.componentType !== 'series') return; const key = PROVINCE_INDEX[params.name]; if (!key) return; clearTimeout(hoverTimer); hoverTimer = setTimeout(() => { ensureMap(key).catch(() => {}); // 预取失败不影响正常流程 }, 120); });120 毫秒的延迟是为了过滤掉鼠标划过地图时的连续触发,不移除的话鼠标横穿全国能同时发起七八个请求。catch(() => {})是必要的,预取失败时不应该在控制台留一堆红字,真正的错误留给drillTo里的ensureMap去抛。
只有当省份数量很少(比如只做华东六省一市)时,一次性预加载才划算,可以在boot()里Promise.all拉完再渲染。判断标准很简单:总文件体积小于 2 MB 就全量,大于就走按需。
5. 避坑与排查:地图空白、名字对不上、状态错乱的五条记录
地图这类东西的失败模式很统一——不报错,只是没画出来或者画错了。下面这五条是我在实际项目里反复撞上的,每条按现象、原因、解决三步记,碰到问题时可以按顺序对着排。
5.1 渲染不出来:先怀疑数据,再怀疑配置
现象一:整块地图空白,控制台干干净净一条错都没有。
原因通常是 GeoJSON 结构不对。registerMap对入参很宽容,你给它一个{ code: 0, data: { features: [...] } }它也照单全收,然后什么都画不出来。另一种常见情况是 fetch 的路径 404,但 SPA 的 history 模式把所有未匹配路径都回退到index.html,res.ok是true,res.json()拿到一段 HTML 直接抛解析错误——如果这个错误被catch吃掉了,表现同样是空白。
解决方式是在ensureMap里加两道断言,一道查res.ok,一道查features是不是非空数组,两道都过不了就抛错。排查时先console.log(echarts.getMap(name)?.features?.length),是undefined就是没注册上,是0就是数据有问题,两者都有值但屏幕还是空的,才去检查 option。
现象二:地图出来了,但位置偏到角落里,或者只有一小块。
原因是数据源给的 GeoJSON 带了额外区域,比如全国图里混进了「南海诸岛」的独立 feature,或者某个省的图里带上了邻近海域的轮廓,boundingCoords被撑大,主体被压缩。解法有两个:一是在下载数据时就把多余 feature 剔掉,二是渲染时用layoutCenter和layoutSize强制约束。我一般选前者,数据干净一次,后面所有页面都省事。
5.2 数据对不上:名字、后缀、编码三处
现象三:visualMap 图例在,部分区域有颜色,另一部分永远是灰的。
原因几乎一定是名字不匹配。可能是业务数据用了简称「深圳」而地图是「深圳市」,也可能是数据源把「XX自治州」写成了「XX州」。还有一类更隐蔽:两份数据一个是 UTF-8 一个是 GBK,看着一样的汉字在字节层面不同,===判断失败。
解决就是把normalize这套函数提到公共模块,业务侧和地图侧都过一遍同一个函数再比对,不要只在一边做转换。排查时打差集最快:把地图的 feature 名集合减去业务数据的 name 集合,剩下的就是没匹配上的。
现象四:点击某个省完全没反应,点别的省都正常。
这种「大部分正常、个别失灵」的问题,九成是那张映射表写错了。比如把「广西壮族自治区」写成了「广西省」,或者「内蒙古自治区」漏了「自治区」三个字。drillTo里if (!key) return会安静地跳过,不报任何错,所以看起来像点击失效。
解决方式是先console.log(params.name)把真实值打出来,照着它建表,不要凭记忆写。这张表建议直接从china.json里提取生成,而不是手敲。
5.3 状态错乱:setOption 和实例管理
现象五:下钻之后返回,全国地图上还叠着上一个省的轮廓,颜色也串了。
原因是setOption的第二个参数没传true。ECharts 默认是合并模式,新的 series 会按索引往旧的上面合并,旧数据残留。地图这种只有单个 series 的场景反而更容易中招,因为你根本想不到它会合。
解决就是把chart.setOption(option, true)里的true当成硬性规矩,地图类图表一律不合并。另外要确保整个页面只init一次实例,用getInstanceByDom兜底;重复init会在同一个容器里叠两层 canvas,缩放时上层不跟随,看起来像残影,实际是两个图表在打架。
窗口尺寸变化后地图不变形,靠的是一个ResizeObserver或者window.onresize里调chart.resize()。少这一句的表现是:侧边栏一折叠,地图还是原来的宽度,右边被裁掉一块。
6. 二次开发:把两级下钻扩成三级,顺带接上真实数据
两级下钻跑通之后,最常见的改造需求是继续往下钻到区县,或者反过来在市级图里点某个城市弹出一个详情面板。这两件事看起来要改很多,实际只要drill.js里的那个栈设计得当,扩展成本很低。
往下加一级的关键在于把「层级」这件事从代码里抽出来,不再写死china -> 省的二元关系。stack里每一项都带上parentKey,点击时判断当前栈顶的 key 是不是china,不是的话就去加载下一级的地图文件,命名规则从guangdong.json变成guangdong-shenzhen.json。
// 三级下钻的命名约定:level2 用 "省-市" 拼接 function childKeyOf(parentKey, childName) { if (parentKey === 'china') { return PROVINCE_INDEX[childName]; // 全国 -> 省 } // 省 -> 市,用拼音拼接,市名要先转拼音或者用市代码 return `${parentKey}-${CITY_CODE[childName]}`; } export async function drillDeeper(chart, clickedName) { const parent = stack[stack.length - 1]; const key = childKeyOf(parent.key, clickedName); if (!key) return; try { await ensureMap(key); } catch (e) { // 没有更下一级数据时,当作普通点击处理,比如打开详情面板 openDetailPanel(clickedName); return; } stack.push({ key, label: clickedName, parentKey: parent.key }); chart.setOption(buildMapOption(key), true); syncBreadcrumb(); }childKeyOf里用市代码而不是市名拼拼音,是因为「苏州」和「宿州」拼音只差一个字母,靠人工维护迟早出错,用行政区划代码更稳。ensureMap抛错时不去弹错误提示,而是兜底打开详情面板,这是一个取舍:区县数据不一定每个市都有,缺数据时给用户一个有用的反馈,比一句「加载失败」体验好得多。
接真实业务数据这块,改动点其实只有一个函数。后端返回的字段名通常不是name和value,中间加一层适配就行:
// 把接口返回适配成 ECharts 认识的格式 function adaptRows(list, nameField = 'areaName', valField = 'totalAmount') { return list.map((row) => ({ name: row[nameField], value: Number(row[valField]) || 0 })); }Number(...) || 0是为了处理接口返回字符串"88.5"和null的情况,Number('88.5')是 88.5 能用,Number(null)是 0,Number('--')是 NaN 会被|| 0兜住。不要用parseInt,它会截断小数,金额类的数据差一点就很明显。
有一点值得提前想清楚:下钻的每一层要不要重新请求数据。全国图的省值和某个省内部各市的值,往往来自不同的接口。我的做法是drillTo里并发发起ensureMap和接口请求,用Promise.all一起等,而不是先等地图再等数据,那样点一次要转两次圈。
验证改造是否成功,我用三个固定动作过一遍:连点同一个省十次,看网络面板里是不是只有一次请求;从全国钻到市再一路返回到全国,看面包屑和地图是否同步;把窗口从 1920 拖到 800 再拖回去,看地图有没有变形或留白。这三个动作覆盖了缓存、状态、自适应三类最容易回归的问题。
从那以后我每次接地图类的需求,setOption的第二个参数和getInstanceByDom这两处都强制走一遍 checklist,宁可多写一行也不省,因为这两处的失败都不报错,全靠肉眼看。希望帮到你。
本文还有配套的精品资源,点击获取