说实话,刚开始搞自定义地图的时候,我也踩过不少坑。那种看着 GeoJSON 数据明明有,但在地图上就是显示不出来,或者轮廓歪得亲妈都不认识的尴尬,你应该也懂。今天我们就把 ECharts 地图绘制的底裤都扒干净,从数据源头到尾端渲染,一步步带你搞定专属区域地图。
先搞明白:GeoJSON 到底是什么鬼?
在你动手写代码之前,得先理解地图数据的本质。GeoJSON 是一种基于 JSON 格式的地理空间数据交换格式,简单说就是把地图上的地块翻译成代码能读懂的语言。
一个标准的 GeoJSON 长这样:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "朝阳区",
"adcode": "110105"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[116.30, 39.96],
[116.35, 39.96],
[116.35, 39.99],
[116.30, 39.99],
[116.30, 39.96]
]
]
}
}
]
}
关键点来了:type 字段决定数据类型,Polygon 是多边形(地块),MultiPolygon 是多个多边形组合(比如一个行政区由好几块不相连的地组成)。coordinates 数组里的每一对 [经度, 纬度] 就是地图上的一个顶点。
如果你发现地图缺块,十有八九是某个区域的 coordinates 数组漏了,或者嵌套层级不对。
数据来源:去哪找靠谱的 GeoJSON?
地图画得歪不歪,数据源是罪魁祸首。我推荐几个经过实战检验的来源:
- 自然资源部标准地图服务 - 官方权威,轮廓最准,但下载流程有点繁琐
- 阿里云 DataV.GeoAtlas - 这个是真的香!提供省、市、区县三级数据,还能在线裁剪,API 调用也方便
- Natural Earth - 国际开源数据,适合做世界地图或大区域地图
- 高德/百度地图开放平台 - 适合获取特定 POI 周边的地理数据
拿阿里云 DataV 举例,访问 http://datav.aliyun.com/portal/school/atlas/area_selector,选择你要的区域(比如北京市朝阳区),下载 GeoJSON 文件。这个数据质量比网上随便搜的高多了。
ECharts 注册地图:最基础的代码骨架
把 GeoJSON 数据保存到 beijing.json(假设你已经下载好了),然后这样初始化:
// 引入 ECharts
import * as echarts from 'echarts';
// 异步获取 GeoJSON 数据
fetch('./beijing.json')
.then(response => response.json())
.then(featureCollection => {
// 注册地图名称为 'beijing'
echarts.registerMap('beijing', featureCollection);
// 初始化图表
const chart = echarts.init(document.getElementById('mapContainer'));
// 配置项
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}' // 显示区域名称
},
visualMap: {
min: 0,
max: 1000,
left: 'left',
top: 'bottom',
text: ['高', '低'],
calculable: true,
inRange: {
color: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']
}
},
series: [{
name: '北京区域数据',
type: 'map',
map: 'beijing', // 对应 registerMap 的第一个参数
roam: true, // 允许缩放和平移
zoom: 1.2,
label: {
show: true,
fontSize: 10,
color: '#333'
},
// 区域高亮样式
emphasis: {
label: {
fontSize: 12,
fontWeight: 'bold'
},
itemStyle: {
areaColor: '#f6d770',
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.3)'
}
},
// 普通状态样式
itemStyle: {
areaColor: '#eee',
borderColor: '#999',
borderWidth: 1
},
// 绑定数据
data: [
{ name: '朝阳区', value: 850 },
{ name: '海淀区', value: 720 },
{ name: '西城区', value: 630 },
{ name: '东城区', value: 580 },
{ name: '丰台区', value: 490 },
{ name: '石景山区', value: 320 }
]
}]
};
chart.setOption(option);
})
.catch(error => console.error('地图加载失败:', error));
这段代码跑起来,你应该能看到北京各区的地图了。但别高兴太早,这里埋着一个巨大的坑——名称匹配问题。
核心痛点一:为什么地图缺块?名称匹配失败!
这是新手最容易栽跟头的地方。你数据里的 name 和 GeoJSON 里的 properties.name 可能长得不一样:
- GeoJSON 里叫 “朝阳区”,你数据里写成了 “朝阳”
- 或者多了一个空格:”朝阳区 “ vs “朝阳区”
- 甚至编码问题导致乱码
解决方案:先打印出来对一遍
fetch('./beijing.json')
.then(response => response.json())
.then(featureCollection => {
// 先把所有区域名称打印出来,方便核对
const geoNames = featureCollection.features.map(f => f.properties.name);
console.log('GeoJSON 中的区域名称:', geoNames);
// 你的数据名称
const dataNames = ['朝阳区', '海淀区', '西城区', '东城区', '丰台区', '石景山区'];
// 检查哪些缺失
const missing = dataNames.filter(name => !geoNames.includes(name));
if (missing.length > 0) {
console.warn('以下区域在地图中找不到:', missing);
console.log('GeoJSON 中相似名称:', geoNames.filter(n =>
n.includes(missing[0].slice(0, 2))
));
}
echarts.registerMap('beijing', featureCollection);
// ...后续代码
});
如果你发现确实少了几个区,别慌,可能有两种情况:
- 数据源本身就不包含这些小区域 - 比如你下载的区县数据只到区级,某些新城还没来得及划分
- 名称确实对不上 - 用上面的代码找相似名称,手动映射
核心痛点二:轮廓歪得离谱?坐标系统一问题!
ECharts 默认使用 WGS84 (EPSG:4326) 坐标系,但国内很多数据源用的是 GCJ-02(火星坐标系) 或者 BD-09(百度坐标系)。如果用错了,地图不仅位置偏移,轮廓还会变形。
判断方法:把地图放到全国范围内看,如果整个地图偏到海里面去了,那就是坐标系问题。
解决方案:坐标转换
如果你拿到的是 GCJ-02 坐标的 GeoJSON,需要转换成 WGS84。有个现成的库叫 coordtransform:
npm install coordtransform
import coordtransform from 'coordtransform';
function convertCoordTransform(gcjPoint) {
// gcjPoint 格式:[经度, 纬度]
return coordtransform.gcj2wgs(gcjPoint[0], gcjPoint[1]);
}
function transformGeoJSON(geojson) {
// 递归遍历所有坐标点
function transformCoords(coords) {
if (typeof coords[0] === 'number') {
// 单个点
const [lng, lat] = coords;
return coordtransform.gcj2wgs(lng, lat);
} else {
// 嵌套数组,递归处理
return coords.map(transformCoords);
}
}
// 深拷贝避免修改原数据
const transformed = JSON.parse(JSON.stringify(geojson));
transformed.features.forEach(feature => {
if (feature.geometry && feature.geometry.coordinates) {
feature.geometry.coordinates = transformCoords(
feature.geometry.coordinates
);
}
});
return transformed;
}
// 使用
fetch('./beijing_gcj.json')
.then(res => res.json())
.then(gcjGeoJSON => {
const wgs84GeoJSON = transformGeoJSON(gcjGeoJSON);
echarts.registerMap('beijing', wgs84GeoJSON);
// ...
});
如果是百度坐标系(BD-09),用 bd2wgs 方法。这个转换必须在 registerMap 之前完成。
进阶技巧:动态交互与数据更新
地图画出来只是第一步,让数据动起来才是王道。
1. 点击区域触发事件
chart.on('click', function(params) {
// params.name 是区域名称
// params.value 是绑定的数据值
console.log('点击了:', params.name, ',数据值:', params.value);
// 弹出详情面板
showDetailPanel(params.name, params.value);
});
2. 动态更新数据(不用重新加载地图)
function updateMapData(newData) {
// newData 格式:[{name: '朝阳区', value: 900}, ...]
// 只更新 series.data,地图几何数据不变
chart.setOption({
series: [{
data: newData,
// 更新颜色映射范围
visualMap: {
max: Math.max(...newData.map(d => d.value)),
min: Math.min(...newData.map(d => d.value))
}
}]
});
}
// 模拟数据变化
setInterval(() => {
const randomData = [
{ name: '朝阳区', value: Math.floor(Math.random() * 1000) },
{ name: '海淀区', value: Math.floor(Math.random() * 1000) },
{ name: '西城区', value: Math.floor(Math.random() * 1000) },
{ name: '东城区', value: Math.floor(Math.random() * 1000) },
{ name: '丰台区', value: Math.floor(Math.random() * 1000) },
{ name: '石景山区', value: Math.floor(Math.random() * 1000) }
];
updateMapData(randomData);
}, 3000);
3. 多地图切换(省-市-区县三级联动)
const mapLevels = {
province: 'china', // 中国地图(ECharts 内置)
city: 'beijing', // 北京市
district: 'chaoyang' // 朝阳区
};
let currentLevel = 'province';
// 点击省份进入市级
chart.on('click', function(params) {
if (currentLevel === 'province' && params.name === '北京市') {
loadDistrictMap('beijing');
currentLevel = 'city';
}
});
async function loadDistrictMap(mapName) {
// 动态加载下一级 GeoJSON
const response = await fetch(`./maps/${mapName}.json`);
const geoJSON = await response.json();
// 注册新地图
echarts.registerMap(mapName, geoJSON);
// 更新配置
chart.setOption({
series: [{
map: mapName,
data: generateMockData(geoJSON) // 生成对应数据
}]
});
}
性能优化:数据量大了怎么办?
如果你要做全国所有区县(2800+),GeoJSON 文件可能几十 MB,加载会很慢。
1. 按需加载(LOD - Level of Detail)
// 根据缩放级别加载不同精度的数据
let currentZoom = 1;
chart.on('viewportchange', function() {
const newZoom = chart.getModel().getComponent('geo').zoom;
if (Math.abs(newZoom - currentZoom) > 0.3) {
currentZoom = newZoom;
if (currentZoom < 1.5) {
// 放大时加载高精度数据
loadHighPrecisionMap();
} else if (currentZoom > 3) {
// 缩小时加载简化数据
loadLowPrecisionMap();
}
}
});
2. 坐标抽稀
// Douglas-Peucker 算法简化坐标
function simplifyCoords(coords, tolerance = 0.01) {
if (coords.length < 3) return coords;
let maxDist = 0;
let maxIndex = 0;
const start = coords[0];
const end = coords[coords.length - 1];
// 找距离线段最远的点
for (let i = 1; i < coords.length - 1; i++) {
const dist = perpendicularDistance(coords[i], start, end);
if (dist > maxDist) {
maxDist = dist;
maxIndex = i;
}
}
if (maxDist > tolerance) {
// 递归简化
const left = simplifyCoords(coords.slice(0, maxIndex + 1), tolerance);
const right = simplifyCoords(coords.slice(maxIndex), tolerance);
return left.concat(right.slice(1));
}
return [start, end];
}
function perpendicularDistance(point, lineStart, lineEnd) {
const dx = lineEnd[0] - lineStart[0];
const dy = lineEnd[1] - lineStart[1];
const mag = Math.sqrt(dx * dx + dy * dy);
if (mag === 0) return Math.sqrt(
Math.pow(point[0] - lineStart[0], 2) +
Math.pow(point[1] - lineStart[1], 2)
);
const u = ((point[0] - lineStart[0]) * dx +
(point[1] - lineStart[1]) * dy) / (mag * mag);
const x = lineStart[0] + u * dx;
const y = lineStart[1] + u * dy;
return Math.sqrt(
Math.pow(point[0] - x, 2) +
Math.pow(point[1] - y, 2)
);
}
3. Web Worker 离线处理
// worker.js
self.onmessage = function(e) {
const { geojson, tolerance } = e.data;
// 在 worker 中进行坐标简化
const simplified = simplifyGeoJSON(geojson, tolerance);
self.postMessage(simplified);
};
// 主线程
const worker = new Worker('worker.js');
worker.onmessage = function(e) {
const simplifiedGeoJSON = e.data;
echarts.registerMap('beijing', simplifiedGeoJSON);
chart.setOption({ /* 配置 */ });
};
worker.postMessage({
geojson: rawGeoJSON,
tolerance: 0.005
});
调试利器:如何快速定位问题
地图不显示?轮廓歪?数据对不上?先用这些调试方法:
1. 检查 GeoJSON 合法性
# 使用 geojsonlint 在线工具验证
# 或者本地安装验证工具
npm install -g geojsonlint
geojsonlint beijing.json
常见的错误类型:
Polygon的坐标首尾不闭合MultiPolygon嵌套层级错误- 坐标值超出正常范围(纬度 -90~90,经度 -180~180)
2. 在地图上显示调试信息
// 开启 ECharts 的调试模式
const chart = echarts.init(document.getElementById('mapContainer'), null, {
renderer: 'canvas', // 尝试切换渲染器
devicePixelRatio: window.devicePixelRatio // 高清屏适配
});
// 在 tooltip 中显示原始坐标
tooltip: {
formatter: function(params) {
if (params.componentType === 'series') {
return `
<div>区域:${params.name}</div>
<div>数据值:${params.value}</div>
<div style="font-size:10px;color:#999;margin-top:5px;">
centroid: ${params.data.centroid?.join(',')}
</div>
`;
}
return params.name;
}
}
3. 使用浏览器开发者工具
打开 DevTools 的 Network 面板,查看 GeoJSON 请求是否成功,响应内容是否完整。在 Console 里打印 featureCollection.features.length,确认数据是否加载。
完整实战案例:打造企业级区域数据大屏
最后给你一个完整的、可直接运行的案例代码:
”`html <!DOCTYPE html>
