本文主要介绍了在vue3项目中使用echarts开发geo地图的详细配置。
本文包含了:地图注册,地图贴图,地图切换,散点图·热力图·路径图·柱形图。
直接先上项目github链接,代码注释全面,地图组件可以直接拿来在业务里使用👇
项目在线预览地址👇:
https://vue3_echarts_geo_map-rfka0hh6-kikidoulovemeruriding.4everland.app
第1步,创建好一个vue3+typescript的项目,开始安装相关库
npm i echarts -S
npm i echarts-wordcloud -S
第2步,下载一份地图geo json数据,本文使用山东省的数据,其他地区的json数据可以从阿里的dataV下载:
https://datav.aliyun.com/portal/school/atlas/area_selector
第3步,新建GeoMap.vue,引入依赖、声明props、初始化echarts实例、抛出实例:
<template>
<div
:style="{ height: `${props.height}px` }"
class="map-container"
ref="map"
></div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, withDefaults, watch } from 'vue';
import * as echarts from 'echarts';
import ShandongGeoJson from '../assets/json/shandong.json?raw';
interface Props {
// 地图的大小主要依赖DOM容器的高度
height?: number;
json: string;
// 具体业务数据(散点图 热力图 柱形图等)
series: any;
}
const props = withDefaults(defineProps<Props>(), {
height: 400,
json: ShandongGeoJson,
series: null,
});
// echats挂载的DOM节点
const map = ref<HTMLElement>();
// echarts实例
const chartInstance = ref<echarts.ECharts>();
// 抛出echarts实例,方便父组件使用
defineExpose({
chartInstance,
});
onMounted(() => {
//初始化实例
chartInstance.value = echarts.init(map.value);
}
onUnmounted(() => {
// 释放echarts实例
chartInstance.value?.dispose();
})
</script>
<style lang="less" scoped>
.map-container {
width: 600px; // 为了演示效果,这里固定宽度, 通常给100%, 宽度由父级DOM决定
}
</style>
第4步,分别定义地图注册方法,贴图绘制方法,配置地图方法,并写好watch监听props.series和props.json执行方法:
<template>
<div
:style="{ height: `${props.height}px` }"
class="map-container"
ref="map"
></div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, withDefaults, watch } from 'vue';
import * as echarts from 'echarts';
import ShandongGeoJson from '../assets/json/shandong.json?raw';
interface Props {
// 地图的大小主要依赖DOM容器的高度
height?: number;
json: string;
// 具体业务数据(散点图 热力图 柱形图等)
series: any;
}
const props = withDefaults(defineProps<Props>(), {
height: 400,
json: ShandongGeoJson,
series: null,
});
// echats挂载的DOM节点
const map = ref<HTMLElement>();
// echarts实例
const chartInstance = ref<echarts.ECharts>();
// 抛出echarts实例,方便父组件使用
defineExpose({
chartInstance,
});
// 注册地图
const registerMap = (value?: echarts.SeriesOption) => {
echarts.registerMap('shandong', JSON.parse(props.json));
if (chartInstance.value) {
// 判断options是否为空,为空则执行初始渲染,不为空则执行更新
const options = chartInstance.value.getOption();
if (options) {
options.series = value;
chartInstance.value.setOption(options);
} else {
drawSticker(value);
}
} else {
console.error('echarts实例未初始化,无法渲染地图');
}
};
/**
* 绘制地图
* 绘制地图贴图要早于渲染地图,否则地图贴图会出现黑色贴图的情况
*
*/
const drawSticker = (value?: echarts.SeriesOption) => {
let sticker: HTMLCanvasElement;
const width = map.value?.offsetWidth ?? 1000;
const stickerDom = document.getElementById('sticker') as HTMLCanvasElement;
if (!stickerDom) {
sticker = document.createElement('canvas');
sticker.id = 'sticker';
map.value?.appendChild(sticker);
} else {
sticker = stickerDom;
}
// 贴图的大小,宽高一致,一般情况下小于props.height更为合适,根据实际情况调整
sticker.width = props.height;
sticker.height = props.height;
const stickerCtx = sticker.getContext('2d');
const image = new Image();
image.src = new URL('../assets/image/sticker.jpg', import.meta.url).href;
image.onload = () => {
// 5个参数分别为:图片、x原点、y原点、宽度、高度
stickerCtx?.drawImage(image, 0, 0, width, width);
// 不显示贴图在文档流中
sticker.style.display = 'none';
generateMap(value);
};
};
const generateMap = (value?: echarts.SeriesOption) => {
const echartsOptions = {
// 一般背景一定设置为透明
backgroundColor: 'transparent',
/**
* 这里的tooltip为《地图本身》鼠标移入显示的div,这里设置为false,不会影响散点图 热力图等series设置的tooltip
*/
tooltip: {
show: true,
},
grid: {
top: 0,
bottom: 0,
left: 0,
right: 0,
},
geo: [
{
map: 'shandong',
roam: false,
// 开启多选
selectedMode: 'multiple',
left: '18%',
top: '18%',
// 应用在第一层的tooltip
tooltip: {
show: true,
// 使用自定义的tooltip样式,下面这四个属性要这么设置,避免影响到自己的样式
backgroundColor: 'transparent',
borderWidth: 0,
padding: 0,
extraCssText: 'box-shadow: none',
// className: 'tooltip-style',
// 自定义的tooltip样式,可以在《公共css文件》声明css类在此使用,当然也可以直接使用style行内样式
formatter: (params: any) => {
return `
<div class="tooltip-style">
<p>
${params?.name}
</p>
</div>
`;
},
},
// 可以给某个地区单独设置样式
regions: [
{
name: '青岛市', // 必须提供对应的name
itemStyle: {},
},
],
label: {
show: true,
fontSize: 12,
// fontFamily: 'YSXS',
},
itemStyle: {
borderColor: 'red',
borderWidth: 1,
color: {
image: document.getElementById('sticker'),
repeat: 'no-repeat',
},
},
z: 2,
},
{
map: 'shandong', //注册地图的名字
roam: false, //开启鼠标缩放和平移漫游。默认不开启
left: '18.8%',
top: '18.8%',
label: {
show: false
},
itemStyle: {
shadowColor: '#000',
shadowOffsetX: 6,
shadowOffsetY: 6,
shadowBlur: 3,
color: 'darkgreen', // 背景
borderWidth: 1, // 边框宽度
borderColor: 'red', // 边框颜色
fontSize: 0.1, //
},
z: 1,
},
],
visualMap: {
type: 'continuous',
map: 'shandong',
show: false,
top: 'top',
min: 1,
max: 1 * 100,
// seriesIndex: 0,
splitNumber: 5,
inRange: {
color: ['#d94e5d', '#eac736', '#50a3ba'].reverse(),
},
calculable: true,
},
series: [value],
};
chartInstance.value?.setOption(echartsOptions);
// 重绘一次,不然省会城市会缺失,暂时没有发现BUG问题所在
chartInstance.value?.resize();
};
onMounted(() => {
//初始化实例
chartInstance.value = echarts.init(map.value);
// 监听两个数据
watch(
() => props.series,
(newValue) => {
registerMap(newValue);
},
{
immediate: true,
}
);
// 监听新的地图json数据,渲染地图
watch(
() => props.json,
(newValue) => {
registerMap(newValue);
}
);
}
onUnmounted(() => {
// 释放echarts实例
chartInstance.value?.dispose();
})
</script>
<style lang="less" scoped>
.map-container {
width: 600px; // 为了演示效果,这里固定宽度, 通常给100%, 宽度由父级DOM决定
}
</style>
到这里地图组件已经封装好了,在父级组件开始使用。
第5步,在父级组件引入地图组件使用:
<template>
<div
id="main"
class="main"
>
<div class="btn-list">
<button @click="switchData(scatterSeries)">散点图</button>
<button @click="switchData(heatmapSeries)">热力图</button>
<button @click="switchData(lineseries)">路径图</button>
<button @click="handleMockBarData">柱形图</button>
<button @click="handleChangeMap">
{{ currentJson === ShandongJson ? '切换到青岛市' : '切换到山东省' }}
</button>
</div>
<div id="map-container">
<GeoMap
ref="map"
:series="currentData"
:height="600"
:json="currentJson"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import * as echarts from 'echarts';
// 引入地图JSON用来切换地区
import ShandongJson from './assets/json/shandong.json?raw';
import QindaoJson from './assets/json/qingdao.json?raw';
import GeoMap from './components/GeoMap.vue';
// map子组件的模板引用,我们通过这里可以访问到子组件的echarts实例
const map = ref<any>(null);
// 演示不同数据类型的地图
const currentData = ref();
// 演示不同地区的地图
const currentJson = ref(ShandongJson);
const switchData = (data: any) => {
currentData.value = data;
};
/**
* 地图中心点, 通常地图json中有确定的中心点,实际应用中我们需要根据具体视图UI来自定义中心点
* json数据里的center: [*****, *****]就是中心点的经纬度数组,可以取来改动和使用
* 中心点主要在用在,给不含有经纬度的数据集,通过遍历赋值,添加上我们预设的中心点经纬度
*/
const regionCenterPoints = [
{
name: '青岛市',
value: [120.355173, 36.082982],
},
{
name: '烟台市',
value: [121.391382, 37.539297],
},
{
name: '济南市',
value: [117.000923, 36.675807],
},
];
console.log(regionCenterPoints);
// 父组件把处理好的series传给子组件, 这里展示几种业务常用的图表类型
/**
* 1. 散点图
* 数据格式: [{name: '数据名称', value: [经度, 纬度]}],name是可选项
* 默认散点大小是一样大,即symbolSize为10,自行设置symbolSize可以控制点的大小
* 散点的颜色,如果map子组件里声明了visualMap,将会使用visualMap inrange的第一个颜色,
* 如果要更改,需要在data数据里传入itemStyle更改颜色
* 其他散点图的参数参考 https://echarts.apache.org/zh/option.html#series-scatter
*/
const scatterSeries = ref<echarts.ScatterSeriesOption>({
type: 'scatter',
coordinateSystem: 'geo',
z: 10,
data: [
{
name: '青岛市',
value: [120.355173, 36.082982],
symbolSize: 30,
itemStyle: {
color: 'red',
},
},
{
name: '烟台市',
value: [121.391382, 37.539297],
symbolSize: 80,
},
{
name: '济南市',
value: [117.000923, 36.675807],
},
],
tooltip: {
show: true,
// echarts这个formatter函数参数的类型声明很多没有导出,直接用any
formatter: (params: any) => {
return `<div style="background: red">${params?.data.value[0]}</div>`;
},
},
});
/**
* 2. 热力图
* 数据格式:[[经度,纬度,热力值],...]
* 使用热力图必须要在map子组件里定义visualMap,否则无法显示
* 其他热力图的参数参考 https://echarts.apache.org/zh/option.html#series-heatmap
*
*/
const heatmapSeries = ref<echarts.HeatmapSeriesOption>({
type: 'heatmap',
coordinateSystem: 'geo',
data: [
[120.355173, 36.082982, 100],
[121.391382, 37.539297, 100],
[119.121733, 36.146036, 100],
[119.121733, 36.146037, 100],
[119.121733, 36.146038, 100],
[119.121733, 36.146039, 100],
[119.121733, 36.146031, 100],
[119.121733, 36.146032, 100],
[119.121733, 36.146033, 100],
],
z: 8,
geoIndex: 0,
blurSize: 20,
});
/**
* 3. 路径图
* 数据格式:[{coords: [[经度,纬度],[经度,纬度]], lineStyle: {}}] , coord是二维数组
* coords数组第一项为起始点的经纬度,第二项为终点的经纬度,lineStyle是线的样式可选项
* 其他路径图的参数参考 https://echarts.apache.org/zh/option.html#series-lines
*/
const lineseries = ref<echarts.LinesSeriesOption>({
type: 'lines',
coordinateSystem: 'geo',
data: [
{
coords: [
[120.355173, 36.082982],
[121.391382, 37.539297],
],
lineStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: '#3B80E2',
},
{
offset: 1,
color: '#49BEE5',
},
]),
width: 2,
},
},
{
coords: [
[120.355173, 36.082982],
[117.000923, 36.675807],
],
lineStyle: {
color: 'darkgreen',
width: 2,
},
},
],
lineStyle: {
curveness: 0.3, // 线的曲度,默认为0直线,0-1的取值范围,值越大曲度越大,可以为负数即设置为反向弧度
},
effect: {
show: true, // 是否显示特效
period: 6, // 特效动画时间,单位s
trailLength: 0.9, // 特效尾迹长度,取值0-1,值越大尾迹越长
constantSpeed: 40, // 特效动画速度,值为像素点,越大越快
color: 'lightblue', // 特效标记的颜色
symbol: 'circle', // 特效标记的图形,可以使用图片等
symbolSize: 10, // 特效标记的大小
},
});
/**
* 4. 柱形图
* 柱形图稍微复杂一些,由于coordinateSystem无法设置geo,只能设置为直角坐标系
* 所以在这里要做一下geo经纬度到二维坐标的转换才能使用
* 处理好的数据并不是series数据,而是需要重新执行setOption
* 其他柱形图的参数参考 https://echarts.apache.org/zh/option.html#series-bar
* 通过该示例,其余大部分的二维坐标的图表都可以自行摸索实现
*/
const mockBarData = [
{
name: '青岛市',
value: [120.355173, 36.082982],
count: 100,
},
{
name: '烟台市',
value: [121.391382, 37.539297],
count: 10,
},
{
name: '济南市',
value: [117.000923, 36.675807],
count: 20,
},
];
const barSeries = ref<any>({
xAxis: [],
yAxis: [],
series: [],
grid: [],
});
const handleMockBarData = () => {
mockBarData.forEach((item: any, index: number) => {
const geoCoords = item.value; // 获取经纬度
// 转换后的二维坐标
const coords: any = map.value?.chartInstance?.convertToPixel(
'geo',
geoCoords
);
index += 1;
barSeries.value.xAxis.push({
id: index, // 组件id,在配置中引用标识
gridIndex: index, // x轴所在的grid的索引
gridId: index,
type: 'category', // 设置为主轴
splitArea: {
show: false,
},
splitLine: {
// 是否显示坐标轴在 grid 区域中的分隔线
show: false,
},
axisTick: {
// 是否显示坐标轴刻度
show: false,
},
axisLabel: {
// 是否显示坐标轴刻度标签
show: false,
},
axisLine: {
// 是否显示坐标轴轴线
show: false,
},
z: 100,
// 通常设置一个比较大的数值 确保柱形图在最高层
});
// Y轴配置
barSeries.value.yAxis.push({
id: index, // 组件id,在配置中引用标识
gridIndex: index, // x轴所在的grid的索引
gridId: index,
// 默认柱子的高度是相同的,如果要设置为不同,调整min 和 max
// min: 0.1,
// max: 60,
// interval: 0.1,
splitLine: {
// 坐标轴在 grid 区域中的分隔线
show: false,
},
axisTick: {
// 坐标轴刻度
show: false,
},
axisLabel: {
// 坐标轴刻度标签
show: false,
},
axisLine: {
// 坐标轴轴线
show: false,
},
splitArea: {
show: false,
},
z: 100,
});
// 坐标系配置控制柱子的位置,需要根据实际UI视图来调整 left top
barSeries.value.grid.push({
gridIndex: index, // x轴所在的grid的索引
gridId: index,
id: index, // 组件id,在配置中引用标识
width: 100, // 组件的宽度
height: 100, // 组件的高度
left: coords[0] - 50, // 离容器左侧的距离
top: coords[1] - 105, // 离容器上侧的距离
z: 100,
});
barSeries.value.series.push({
labelLine: {
show: false,
},
emphasis: {
show: false,
disabled: true,
},
z: 100,
id: index, // 组件id,在配置中引用标识
type: 'bar', // 柱状图
xAxisId: index, // 使用的x轴的id
yAxisId: index, // 使用的y轴的id
barGap: 0, // 柱间距离
barWidth: 8,
barCategoryGap: 0, // 同一系列的柱间距离
data: [item.count], // 柱子数据, 可以设置多个数据,显示多个柱子
label: {
show: false,
itemStyle: {
color: 'red',
},
},
itemStyle: {
// 柱子样式
borderRadius: [4, 4, 0, 0],
color: new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{
offset: 0,
color: 'rgba(232, 137, 58, 1)',
},
{
offset: 1,
color: 'rgba(232, 137, 58, 0.2)',
},
],
false
),
},
});
});
// 执行setOption, 通常会和之前的图表合并,如果不想合并,传递第二个参数{replaceMerge: 'series'}
map.value.chartInstance.setOption(barSeries.value, {
replaceMerge: 'series',
});
};
/**
* 关于缩放和拖动的问题,echarts自带的roam问题很多:
* 问题1:地图贴图之后,只有地图本身,表面没有图表的时候,只有在地图空白区域才可以拖动和缩放
* 问题2:地图表面加入图表后,拖动和缩放只对第一层geo生效
* 问题3:通过使用监听事件georoam,对除了第二层geo的地图进行拖动和缩放,会卡顿无比(GeoMap.vue)
* 问题4:缩放和拖动对geo经纬转成二维坐标的图表不生效,例如上面的柱形图
* 综上自己实现一个wheel监听,缩放父级dom元素,算是一个折中的解决方案
*/
let num = 1;
const handleMouseEvent = () => {
const main = document.getElementById('main');
const mapContainer = document.getElementById('map-container');
main?.addEventListener('wheel', (event: WheelEvent) => {
// 该缩放方案tooltip会出现移动位置的问题,所以直接把显示中的tooltip隐藏了
map.value?.chartInstance?.dispatchAction({
type: 'hideTip',
});
event.preventDefault();
if (event.deltaY < 0) {
num += 0.1;
mapContainer!.style.transform = `scale(${num})`;
} else {
if (num < 0.11) {
num = 0.1;
} else {
num -= 0.1;
}
mapContainer!.style.transform = `scale(${num})`;
}
});
};
// 切换地图JSON即可完成切换
const handleChangeMap = () => {
currentJson.value =
currentJson.value === ShandongJson ? QindaoJson : ShandongJson;
};
// 监听地图的点击事件
const handleClickMap = () => {
map.value?.chartInstance?.on('click', (params: any) => {
if (params.componentType === 'geo') {
if (params.name === '青岛市') {
currentJson.value = QindaoJson;
}
}
});
};
// 监听地图的选中事件
const handleSelectMap = () => {
map.value?.chartInstance?.on('geoselectchanged', (params: any) => {
console.log(params.selected, 'selectchanged');
});
};
onMounted(() => {
handleMouseEvent();
handleClickMap();
handleSelectMap();
});
</script>
<style lang="less">
.tooltip-style {
width: 100px;
height: 33px;
padding: 5px;
border-radius: 8px;
background: darkorange;
color: #fff;
line-height: 33px;
p {
margin: 0;
}
}
.main {
width: 1000px;
height: 1000px;
position: relative;
.btn-list {
width: 500px;
display: flex;
position: relative;
z-index: 1000;
justify-content: space-between;
}
#map-container {
position: absolute;
transition: all 0.1s linear;
}
}
</style>
上面代码中已经列出了散点图 热力图 路径图 柱形图 还有地图点击事件和选中事件的回调函数示例。一切顺利的话,你应该已经看到地图了。
其他方面,关于echarts地图的缩放平移建议用监听鼠标事件通过css操作父级dom的方式实现(如上面代码),当然你也可以使用监听echarts缩放事件来实现:
chartInstance.value?.on('georoam', (params: any) => {
const options: any = chartInstance.value?.getOption();
if (params?.zoom) {
// 缩放
options.geo.slice(1)?.forEach((item: any) => {
item.zoom = options?.geo?.[0]['zoom'];
item.center = options?.geo?.[0]['center'];
})
} else {
// 移动
options.geo.slice(1)?.forEach((item: any) => {
item.center = options?.geo?.[0]['center'];
})
}
chartInstance.value?.setOption(options, {
replaceMerge: ['geo'],
});
})
但是效果很卡顿...
关于设置地图最外层边框和地图内部边框不一样的颜色宽度,可以修改GeoMap.vue组件里的generateMap方法如下:
const generateMap = (value?: echarts.SeriesOption) => {
const echartsOptions = {
// 一般背景一定设置为透明
backgroundColor: 'transparent',
/**
* 这里的tooltip为《地图本身》鼠标移入显示的div,这里设置为false,不会影响散点图 热力图等series设置的tooltip
*/
tooltip: {
show: true,
},
grid: {
top: 0,
bottom: 0,
left: 0,
right: 0,
},
geo: [
{
map: 'shandong',
roam: false,
selectedMode: 'multiple',
left: '18%',
top: '18%',
// 应用在第一层的tooltip
tooltip: {
show: true,
// 使用自定义的tooltip样式,下面这四个属性要这么设置,避免影响到自己的样式
backgroundColor: 'transparent',
borderWidth: 0,
padding: 0,
extraCssText: 'box-shadow: none',
// className: 'tooltip-style',
// 自定义的tooltip样式,可以在《公共css文件》声明css类在此使用,当然也可以直接使用style行内样式
formatter: (params: any) => {
return `
<div class="tooltip-style">
<p>
${params?.name}
</p>
</div>
`;
},
},
// 可以给某个地区单独设置样式
regions: [
{
name: '青岛市', // 必须提供对应的name
itemStyle: {},
},
],
label: {
show: true,
fontSize: 12,
// fontFamily: 'YSXS',
},
itemStyle: {
borderColor: 'red',
borderWidth: 1,
color: {
image: document.getElementById('sticker'),
repeat: 'no-repeat',
},
},
z: 2,
},
{
map: 'shandong', //注册地图的名字
roam: false, //开启鼠标缩放和平移漫游。默认不开启
left: '18.8%',
top: '18.8%',
label: {
show: false
},
itemStyle: {
shadowColor: '#000',
shadowOffsetX: 6,
shadowOffsetY: 6,
shadowBlur: 3,
color: 'darkgreen', // 背景
borderWidth: 1, // 边框宽度
borderColor: 'red', // 边框颜色
fontSize: 0.1, //
},
z: 1,
},
],
visualMap: {
type: 'continuous',
map: 'shandong',
show: false,
top: 'top',
min: 1,
max: 1 * 100,
// seriesIndex: 0,
splitNumber: 5,
inRange: {
color: ['#d94e5d', '#eac736', '#50a3ba'].reverse(),
},
calculable: true,
},
/**
* 需要设置最外层和内部的边框不同颜色时,在series这里声明一层map,把第一层geo配置全部给到这里,
* 纹理贴图需要设置到areaColor那里
* 第一层的borderWidth 大于 这里的borderWidth 即可实现最外层和内部的边框样式不同的效果
*/
series: [
{
type: 'map',
map: 'shandong',
itemStyle: {
borderWidth: 1,
areaColor: document.getElementById('sticker'),
},
}
]
};
chartInstance.value?.setOption(echartsOptions);
// 重绘一次,不然省会城市会缺失,暂时没有发现BUG问题所在
chartInstance.value?.resize();
};
关于地图的立体感, 这需要看你的UI设计稿的复杂度,不是很复杂的设置一层geo添加阴影即可,复杂的就在geo数组里多加几层,堆叠出想要的效果,配置的时候,z这个参数越大的越在上层。
代码注释里有很多细节说明,这里就不一一赘述了,各位clone一下代码跑起来看看,实践一下。有任何不懂得问题可以在评论区回复,我看到了一定会回复的。see you!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。