ECharts自定义地图绘制从GeoJSON配置到省份轮廓校准实战教程 解决地图数据缺失坐标偏移显示异常等常见问题
大家好,我是Agnes。今天咱们来聊聊ECharts自定义地图这件事。我敢拍着胸脯说,这可能是你见过最实用、最接地气的教程了——没有废话,没有套话,只有实打实的代码和踩坑经验。
做过地图可视化的人都知道,ECharts默认的地图配置看似简单,但一旦遇到数据缺失、坐标偏移、轮廓不准这些问题,真的会让人抓狂。我见过太多人在这里栽跟头,所以今天把这些问题一次性给你讲透。
一、为什么我们总需要自定义地图
先说个故事。去年有个做数据可视化的朋友找我帮忙,他的项目需要展示中国各省份的疫情数据。他用的是ECharts自带的中国地图,结果发现某些省份的数据死活渲染不出来。
“是不是配置错了?”他问。
“不,”我说,”是你的数据有问题。”
他查了半天才发现,某个省份在JSON里的名称叫”新疆维吾尔自治区”,但他的数据里写的是”新疆”。就这么一个细微差别,整条数据链就断了。
这就是为什么我们总需要自定义地图的原因——官方提供的地图数据往往不够精准,或者不符合你的业务需求。你需要的是:
- 特定区域的精细地图(比如某个省、某个市)
- 自定义的行政区划边界
- 数据与地图的精确匹配
二、GeoJSON是什么?为什么它这么重要
GeoJSON是一种基于JSON格式的地理空间数据交换格式。你可以把它想象成一张”数字地图”,里面记录了每个地区的边界坐标、名称、面积等信息。
让我用一个小例子说明一下GeoJSON的结构:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "北京市",
"adcode": "110000",
"center": [116.407526, 39.90403]
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[116.407526, 39.90403],
[116.41, 39.91],
[116.42, 39.905],
...
]
]
]
}
}
]
}
看到这个coordinates数组了吗?这就是地图轮廓的核心——它记录了一系列经纬度坐标,把这些点连起来,就形成了省份的边界。
关键点来了:ECharts的地图功能就是依赖GeoJSON数据来绘制地图轮廓的。所以,搞清楚了GeoJSON,你就搞清楚了自定义地图的半壁江山。
三、获取GeoJSON数据的几种方式
方式一:使用阿里云DataV.GeoAtlas
这是目前国内最常用的开源地理数据源。网址是 http://datav.aliyun.com/portal/school/atlas/area_selector
在这个网站上,你可以下载中国各个省份的GeoJSON数据。下载后你会得到一个.json文件,里面包含了各个省份的边界坐标。
方式二:使用高德地图开放平台
高德地图提供了强大的地理编码API,你可以通过他们的服务获取特定区域的GeoJSON数据。这种方式适合需要更精细控制的情况。
方式三:使用Mapbox或者OpenStreetMap
如果你需要全球的地图数据,Mapbox和OpenStreetMap是很好的选择。不过这些数据的粒度可能比较粗,不适合需要做省级精细地图的场景。
四、ECharts注册自定义地图的基础代码
好,拿到了GeoJSON数据,接下来就是把注册到ECharts里。这一步其实很简单,但很多人第一次做的时候总是踩坑。
// 引入ECharts
import * as echarts from 'echarts';
// 获取GeoJSON数据(假设已经下载到本地)
fetch('/china.json')
.then(response => response.json())
.then(geoJson => {
// 注册自定义地图
echarts.registerMap('china', geoJson);
// 初始化图表
const chart = echarts.init(document.getElementById('main'));
// 配置项
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}'
},
visualMap: {
min: 0,
max: 1000,
left: 'left',
top: 'bottom',
text: ['高', '低'],
calculable: true,
inRange: {
color: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']
}
},
series: [{
name: '数据地图',
type: 'map',
map: 'china', // 这里对应的就是registerMap的第一个参数
roam: true,
zoom: 1.2,
label: {
show: true,
fontSize: 10
},
data: [
{ name: '北京市', value: 100 },
{ name: '上海市', value: 200 },
{ name: '广东省', value: 300 },
// ... 更多数据
]
}]
};
chart.setOption(option);
});
这段代码看起来很简单对吧?但问题就出在那些”看似简单”的地方。
五、常见问题一:地图数据显示不完整
这是最常见的问题。你精心配置好了,结果地图只显示了一部分省份,其他的地方全是空白。
为什么会这样?
原因分析:
GeoJSON数据本身不完整:你下载的GeoJSON可能缺少某些省份的数据。比如有些公开的GeoJSON数据只包含省级边界,不包含县级边界。
省份名称不匹配:这是最坑的地方。GeoJSON里记录的省份名称可能和你的数据名称不一致。
让我用代码来演示这个问题:
// 假设这是GeoJSON里记录的省份名称
const geoNames = [
'北京市', '天津市', '河北省', '山西省',
'内蒙古自治区', '辽宁省', '吉林省', '黑龙江省',
'上海市', '江苏省', '浙江省', '安徽省',
'福建省', '江西省', '山东省', '河南省',
'湖北省', '湖南省', '广东省', '广西壮族自治区',
'海南省', '重庆市', '四川省', '贵州省',
'云南省', '西藏自治区', '陕西省', '甘肃省',
'青海省', '宁夏回族自治区', '新疆维吾尔自治区',
'台湾省', '香港特别行政区', '澳门特别行政区'
];
// 假设这是你的数据中的省份名称
const dataNames = [
'北京', '天津', '河北', '山西',
'内蒙古', '辽宁', '吉林', '黑龙江',
'上海', '江苏', '浙江', '安徽',
'福建', '江西', '山东', '河南',
'湖北', '湖南', '广东', '广东',
'海南', '重庆', '四川', '贵州',
'云南', '西藏', '陕西', '甘肃',
'青海', '宁夏', '新疆',
'台湾', '香港', '澳门'
];
// 对比一下,你能发现什么问题吗?
// 很多名称是不匹配的!
解决方案:
我们需要建立一个映射关系:
// 建立名称映射
const nameMapping = {
'北京': '北京市',
'天津': '天津市',
'河北': '河北省',
'山西': '山西省',
'内蒙古': '内蒙古自治区',
'辽宁': '辽宁省',
'吉林': '吉林省',
'黑龙江': '黑龙江省',
'上海': '上海市',
'江苏': '江苏省',
'浙江': '浙江省',
'安徽': '安徽省',
'福建': '福建省',
'江西': '江西省',
'山东': '山东省',
'河南': '河南省',
'湖北': '湖北省',
'湖南': '湖南省',
'广东': '广东省',
'海南': '海南省',
'重庆': '重庆市',
'四川': '四川省',
'贵州': '贵州省',
'云南': '云南省',
'西藏': '西藏自治区',
'陕西': '陕西省',
'甘肃': '甘肃省',
'青海': '青海省',
'宁夏': '宁夏回族自治区',
'新疆': '新疆维吾尔自治区',
'台湾': '台湾省',
'香港': '香港特别行政区',
'澳门': '澳门特别行政区'
};
// 转换数据
function transformData(rawData) {
return rawData.map(item => {
// 如果原始数据中的名称在映射表中,就替换
if (nameMapping[item.name]) {
return {
...item,
name: nameMapping[item.name]
};
}
return item;
});
}
六、常见问题二:坐标偏移问题
这个问题更麻烦。你的地图位置看起来是歪的,或者某个省份的位置完全不对。
为什么会发生坐标偏移?
这是因为中国的地图数据涉及到底图安全和坐标系统的问题。在中国,我们需要使用GCJ-02坐标系,而不是标准的WGS-84坐标系。
// 这是一个简单的坐标转换示例
// 将WGS-84坐标转换为GCJ-02坐标
function wgs84ToGcj02(lng, lat) {
const a = 6378245.0; // 长半轴
const ee = 0.00669342162296594323; // 偏心率平方
let dLng = transformLat(lng - 105.0, lat - 35.0);
let dLat = transformLng(lng - 105.0, lat - 35.0);
const radLat = lat / 180.0 * Math.PI;
let magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
const sqrtMagic = Math.sqrt(magic);
dLng = (dLng * 180.0) / ((a / sqrtMagic) * Math.cos(radLat) * Math.PI);
dLat = (dLat * 180.0) / (((1 - ee) / (magic * sqrtMagic)) * a * Math.PI);
return {
lng: lng + dLng,
lat: lat + dLat
};
}
function transformLat(x, y) {
let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y +
0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin(y / 3.0 * Math.PI)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * Math.PI) + 320 * Math.sin(y * Math.PI / 30.0)) * 2.0 / 3.0;
return ret;
}
function transformLng(x, y) {
let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin(x / 3.0 * Math.PI)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * Math.PI) + 300.0 * Math.sin(x / 30.0 * Math.PI)) * 2.0 / 3.0;
return ret;
}
不过说实话,在实际开发中,我们很少手动做坐标转换。更好的做法是:
使用专门的工具库:比如
coord-transform这个npm包,它可以帮你完成各种坐标系统的转换。使用已经处理好的GeoJSON数据:很多第三方服务会提供已经转换好的GCJ-02坐标系的GeoJSON数据。
# 安装坐标转换库
npm install coord-transform
import coordTransform from 'coord-transform';
// 使用示例
const wgs84Point = [116.397428, 39.90923]; // 经度, 纬度
const gcj02Point = coordTransform.wgs84togcj02(wgs84Point[0], wgs84Point[1]);
console.log(gcj02Point); // [116.397688, 39.909021]
七、常见问题三:地图轮廓不准确
这是最让设计师头疼的问题。你导入的地图,轮廓看起来怪怪的,和实际地理形状不符。
原因分析:
数据来源不靠谱:有些公开的GeoJSON数据是几十年前测绘的,和现在的行政区划已经不一样了。
简化过度:为了减小文件体积,很多GeoJSON数据会对边界进行简化,这会导致轮廓失真。
坐标系不匹配:如果你的数据是WGS-84坐标系,而ECharts期望的是GCJ-02坐标系,那么地图就会偏。
解决方案:
首先,你需要检查你的GeoJSON数据质量。可以用一些工具来可视化你的GeoJSON,看看轮廓是否正确。
这里推荐一个在线工具:https://geojson.io
你可以把你的GeoJSON文件上传上去,然后在地图上查看轮廓是否正确。
如果轮廓有问题,你有几个选择:
选择一:换数据源
// 使用阿里云DataV.GeoAtlas的数据
// 下载地址:http://datav.aliyun.com/portal/school/atlas/area_selector
// 选择你需要的区域,下载GeoJSON
选择二:使用专业的地图数据服务
国内比较靠谱的地图数据服务有:
- 阿里云DataV.GeoAtlas(免费)
- 高德地图开放平台(部分免费)
- 腾讯地图开放平台(部分免费)
选择三:手动编辑GeoJSON
如果某个省份的轮廓有问题,你可以用GIS软件(比如QGIS)来手动编辑GeoJSON,修正错误的边界。
八、进阶:如何优化地图性能
当你的地图数据量很大时,ECharts的渲染性能可能会下降。这里有几个优化技巧:
1. 简化GeoJSON数据
GeoJSON文件越大,渲染越慢。你可以使用简化工具来减小文件体积。
# 使用mapshaper简化GeoJSON
npx mapshaper input.json -simplify 0.1 -o output.json
2. 按需加载
不要一次性加载所有数据。可以根据用户的操作,按需加载特定区域的地图数据。
// 按需加载地图数据
async function loadRegionMap(region) {
const response = await fetch(`/maps/${region}.json`);
const geoJson = await response.json();
echarts.registerMap(region, geoJson);
return region;
}
// 使用示例
loadRegionMap('beijing').then(name => {
// 使用注册的地图
chart.setOption({
series: [{
type: 'map',
map: name
}]
});
});
3. 使用WebGL渲染
对于大量数据点的地图,可以考虑使用ECharts的GL版本,它使用WebGL来渲染,性能会好很多。
import * as echarts from 'echarts/extension';
// 使用GL版本
const chart = echarts.init(document.getElementById('main'), null, {
renderer: 'webgl'
});
九、实战案例:做一个省份数据对比地图
让我们把前面讲的所有知识点整合起来,做一个实战案例。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>ECharts自定义地图实战</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 20px;
}
#mapChart {
width: 100%;
height: 600px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.legend {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 20px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-color {
width: 20px;
height: 20px;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="container">
<h1>2024年各省份GDP数据地图</h1>
<div id="mapChart"></div>
<div class="legend">
<div class="legend-item">
<div class="legend-color" style="background: #1e3a5f;"></div>
<span>高GDP</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #4a90d9;"></div>
<span>中GDP</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #a8d5ba;"></div>
<span>低GDP</span>
</div>
</div>
</div>
<script>
// 省份名称映射
const nameMapping = {
'北京': '北京市',
'天津': '天津市',
'河北': '河北省',
'山西': '山西省',
'内蒙古': '内蒙古自治区',
'辽宁': '辽宁省',
'吉林': '吉林省',
'黑龙江': '黑龙江省',
'上海': '上海市',
'江苏': '江苏省',
'浙江': '浙江省',
'安徽': '安徽省',
'福建': '福建省',
'江西': '江西省',
'山东': '山东省',
'河南': '河南省',
'湖北': '湖北省',
'湖南': '湖南省',
'广东': '广东省',
'广西': '广西壮族自治区',
'海南': '海南省',
'重庆': '重庆市',
'四川': '四川省',
'贵州': '贵州省',
'云南': '云南省',
'西藏': '西藏自治区',
'陕西': '陕西省',
'甘肃': '甘肃省',
'青海': '青海省',
'宁夏': '宁夏回族自治区',
'新疆': '新疆维吾尔自治区',
'台湾': '台湾省',
'香港': '香港特别行政区',
'澳门': '澳门特别行政区'
};
// 2024年各省份GDP数据(单位:亿元)
const gdpData = [
{ name: '广东', value: 135673 },
{ name: '江苏', value: 128222 },
{ name: '山东', value: 92292 },
{ name: '浙江', value: 82553 },
{ name: '河南', value: 61029 },
{ name: '四川', value: 60132 },
{ name: '湖北', value: 58238 },
{ name: '福建', value: 54355 },
{ name: '湖南', value: 50012 },
{ name: '安徽', value: 47051 },
{ name: '上海', value: 47219 },
{ name: '河北', value: 43944 },
{ name: '北京', value: 43761 },
{ name: '陕西', value: 33786 },
{ name: '江西', value: 32075 },
{ name: '辽宁', value: 30209 },
{ name: '云南', value: 28954 },
{ name: '广西', value: 27202 },
{ name: '山西', value: 25672 },
{ name: '内蒙古', value: 24523 },
{ name: '贵州', value: 21017 },
{ name: '重庆', value: 30145 },
{ name: '吉林', value: 20115 },
{ name: '黑龙江', value: 15884 },
{ name: '新疆', value: 19126 },
{ name: '甘肃', value: 11864 },
{ name: '海南', value: 7551 },
{ name: '宁夏', value: 5315 },
{ name: '青海', value: 3941 },
{ name: '西藏', value: 2397 }
];
// 转换数据名称
function transformData(data) {
return data.map(item => ({
...item,
name: nameMapping[item.name] || item.name
}));
}
// 初始化图表
const chart = echarts.init(document.getElementById('mapChart'));
// 显示加载状态
chart.showLoading();
// 加载中国地图数据
fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json')
.then(response => response.json())
.then(geoJson => {
// 注册地图
echarts.registerMap('china', geoJson);
// 隐藏加载状态
chart.hideLoading();
// 配置项
const option = {
tooltip: {
trigger: 'item',
formatter: function(params) {
if (params.data) {
return `${params.name}<br/>GDP: ${params.data.value} 亿元`;
}
return params.name;
}
},
visualMap: {
min: 0,
max: 140000,
left: 'left',
top: 'bottom',
text: ['高GDP', '低GDP'],
calculable: true,
inRange: {
color: ['#1e3a5f', '#2d5a8f', '#4a90d9', '#7ab5e8', '#a8d5ba']
},
textStyle: {
color: '#333'
}
},
series: [
{
name: 'GDP地图',
type: 'map',
map: 'china',
roam: true,
zoom: 1.2,
label: {
show: true,
fontSize: 10,
color: '#333'
},
emphasis: {
label: {
show: true,
fontSize: 12,
fontWeight: 'bold'
},
itemStyle: {
areaColor: '#ffd700',
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.3)'
}
},
select: {
disabled: true
},
data: transformData(gdpData)
}
]
};
chart.setOption(option);
})
.catch(error => {
chart.hideLoading();
console.error('加载地图数据失败:', error);
// 显示错误提示
chart.setOption({
title: {
text: '地图数据加载失败,请稍后重试',
left: 'center',
top: 'center',
textStyle: {
color: '#999',
fontSize: 16
}
}
});
});
// 响应式处理
window.addEventListener('resize', () => {
chart.resize();
});
</script>
</body>
</html>
这个实战案例展示了:
- 如何使用阿里云DataV的GeoJSON数据
- 如何处理省份名称映射问题
- 如何配置visualMap来显示数据颜色
- 如何处理加载错误的情况
- 如何响应式调整地图大小
十、总结与最佳实践
好了,说了这么多,让我来总结一下今天的内容。
核心要点:
GeoJSON是地图的基础:搞清楚GeoJSON的结构和来源,是做好自定义地图的第一步。
名称映射是关键:大部分数据显示问题都是因为名称不匹配。建立一个完善的名称映射表,可以解决80%的问题。
坐标系统要注意:在中国做地图开发,一定要用GCJ-02坐标系,否则会出现位置偏移。
数据质量要检查:用在线工具检查你的GeoJSON数据是否正确,避免轮廓异常。
性能优化不能少:当数据量大时,要考虑简化数据和按需加载。
最佳实践建议:
- 优先使用阿里云DataV.GeoAtlas的数据源,质量比较可靠
- 建立完善的省份名称映射表,并且定期更新
- 对于关键项目,建议购买专业的地图数据服务
- 做好错误处理,地图加载失败时给用户友好的提示
- 定期测试地图在不同浏览器和设备上的显示效果
希望这篇教程对你有帮助。如果你在实践中遇到其他问题,欢迎随时问我。记住,地图可视化这条路,踩坑是难免的,但每一次踩坑都是一次成长的机会。
加油!
