基于地图卫星纹理底图+Echarts+vue实现数据可视化
2026/9/16 7:39:20 网站建设 项目流程

话不多说直接看效果

为啥要整这个呢,就是项目有个需求,要求底图改成带有山脉纹理的,但是有不能有除中国以外的地方出现背景(这里只能排除使用各种底图API了),然后我就查阅Echart文档,没有现成的可实现的方法,网上有Three.js + react实现的,可是我自己又不会,咋整呢?当然这个年头了问AI呀,问了,说是使用Ehcart的graphic添加背景图,然后我就网上各种找,要这种除了区划边界之外都是透明的底图,还真被我在GitHub上找到了,是个项目,直接本地运行,根据选择的区划直接下载底图。背景图片找到了,那就是写代码呗
话不多说直接上源码

源码在下边,先听我说完,按照ai的方式给代码写出来了,发现边界数据和图片是两个层级,且对不上,差十万八千里,难受,继续叫他修改。。。。。此时改了很久,终于改好了。
主要解决了,缩放拖拽时底图和区划边界不重合的问题,其实就是每次缩放和拖拽都重新计算一下底图的位置。及有了底图,有兼容了echarts的各种配置,打点,飞行图...不过不支持下钻,想要下钻还需要找到对应地区的底图,然后下钻之后再对应一下底图和区划边界,想想就麻烦,这里就不弄了。

需要自己来取源码,使用的geojson数据来源DataV。

<template>

<div class="china-map-page">

<div ref="mapRef" class="china-map"></div>

</div>

</template>

<script setup lang="ts">

import {

ref,

reactive,

onMounted,

onBeforeUnmount,

nextTick,

computed

} from 'vue'

import * as echarts from 'echarts'

import { geoMercator } from 'd3-geo'

import chinaBg from '@/assets/china.png'

const mapRef = ref<HTMLElement | null>(null)

let chart: echarts.ECharts | null = null

/**

* =========================================================

* 你现在已经调好的参数

* =========================================================

*

* 这几个值会作为「初始状态」保存。

*

* 初始状态:

* scaleX = 1

* scaleY = 1.3

* offsetX = 3

* offsetY = 93

*

* 只要初始状态已经对齐,后面的缩放、拖动都会基于

* 这个状态进行同步,不会重新把纹理强行 fit 到地图。

* (根据自身情况可调整)

*/

const textureConfig = reactive({

scaleX: 1,

scaleY: 1.3,

offsetX: 3,

offsetY: 93

})

/**

* =========================================================

* 中国大致经纬度范围(根据自身情况可调整)

* =========================================================

*/

const CHINA_BOUNDS = {

left: 73.2,

top: 54.1,

right: 134.8,

bottom: 17.8

}

// 根据CHINA_BOUNDS计算出横纵坐标的参考点

const x = computed(() => {

return (CHINA_BOUNDS.left + CHINA_BOUNDS.right) / 2

})

const y = computed(() => {

return (CHINA_BOUNDS.top + CHINA_BOUNDS.bottom) / 2

})

// 这个点主要是为了测量横向缩放。也就是和x ,y 相差一些数值,就可以近似表示地图当前的横向比例。

const bl = [134,20]


/**

* =========================================================

* 固定参考点

* =========================================================

*

* 不再使用:

*

* 当前地图矩形中心

*

* 而是使用固定的地理坐标。

*

* 这样地图发生 roam 后:

*

* 经纬度 -> 当前像素

*

* 可以准确知道地图到底发生了多少缩放和位移。

*/

const GEO_ANCHORS = {

/**

* 中心参考点

*/

center: [x.value, y.value] as [number, number],

/**

* X 方向参考点

*

* 和 center 保持相同纬度。

*/

x: [bl[0], y.value] as [number, number],

/**

* Y 方向参考点

*

* 和 center 保持相同经度。

*/

y: [x.value, bl[1]] as [number, number]

}

/**

* =========================================================

* 初始状态

* =========================================================

*

* 地图第一次渲染完成后,把:

*

* 1. 参考点像素位置

* 2. PNG 的位置

* 3. PNG 的大小

*

* 全部记录下来。

*

* 后面的 roam 都基于这个状态计算。

*/

let initialState: {

anchorCenter: [number, number]

anchorX: [number, number]

anchorY: [number, number]

textureCenter: [number, number]

textureWidth: number

textureHeight: number

} | null = null

/**

* =========================================================

* 初始化地图

* =========================================================

*/

async function initMap() {

if (!mapRef.value) return

chart = echarts.init(mapRef.value)

/**

* 加载中国 GeoJSON

*/

const chinaGeoJson = await fetch(

'https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json'

).then(res => {

if (!res.ok) {

throw new Error(`加载中国地图失败:${res.status}`)

}

return res.json()

})

/**

* 注册地图

*/

echarts.registerMap('china', chinaGeoJson)

/**

* =======================================================

* d3 Mercator(使用第三方的投影坐标可以更好的让底图和区域边界重合)

* =======================================================

*/

const projection = geoMercator()

/**

* 先给一个中心

*/

projection.center([104, 35])

/**

* fitSize 只负责初始化地图。

*

* 注意:

*

* 后面不会再使用这个东西去重新计算 PNG。

*/

projection.fitSize([1000, 700], chinaGeoJson)

/**

* =======================================================

* ECharts option

* =======================================================

*/

const option: echarts.EChartsOption = {

backgroundColor: '#000',

geo: {

map: 'china',

/**

* 开启拖动 + 缩放

*/

roam: true,

/**

* 初始缩放如果初始不是1,那就需要重新调整textureConfig和CHINA_BOUNDS

*/

zoom: 1,

/**

* 地图层级

*/

z: 2,

/**

* 使用 d3 的 Mercator 投影

*/

projection: {

project: (point) => {

return projection(point as [number, number])

},

unproject: (point) => {

return projection.invert(

point as [number, number]

)!

}

},

/**

* 地图区域透明

*/

itemStyle: {

areaColor: 'rgba(0, 0, 0, 0)',

borderColor: '#00ffff',

borderWidth: 2

},

/**

* 鼠标移入

*/

emphasis: {

itemStyle: {

areaColor: 'rgba(0, 150, 255, 0.2)'

},

label:{

color:'#fff'

}

}

},

/**

* 初始不放 graphic

*

* 后面 updateChinaTexture 再添加。

*/

graphic: []

}

chart.setOption(option)

await nextTick()

/**

* 等 ECharts 真正完成第一次布局

*/

requestAnimationFrame(() => {

requestAnimationFrame(() => {

/**

* 第一次创建纹理。

*

* 这里会记录「初始状态」。

*/

initTextureState()

})

})

/**

* =======================================================

* 地图缩放 / 拖动

* =======================================================

*/

chart.on('georoam', () => {

updateChinaTexture()

})

}

/**

* =========================================================

* 经纬度 -> 当前 ECharts 像素坐标

* =========================================================

*/

function geoToPixel(

point: [number, number]

): [number, number] | null {

if (!chart) return null

const result = chart.convertToPixel(

{

geoIndex: 0

},

point

) as number[]

if (!result || result.length < 2) {

return null

}

if (

!Number.isFinite(result[0]) ||

!Number.isFinite(result[1])

) {

return null

}

return [

result[0],

result[1]

]

}

/**

* =========================================================

* 计算初始纹理

* =========================================================

*

* 注意:

*

* 这里仍然使用你之前的算法。

*

* 所以:

*

* scaleX = 1

* scaleY = 1.3

* offsetX = 3

* offsetY = 93

*

* 得到的初始效果不会被改变。

*/

function initTextureState() {

if (!chart) return

/**

* 获取三个固定参考点

*/

const anchorCenter = geoToPixel(GEO_ANCHORS.center)

const anchorX = geoToPixel(GEO_ANCHORS.x)

const anchorY = geoToPixel(GEO_ANCHORS.y)

if (

!anchorCenter ||

!anchorX ||

!anchorY

) {

return

}

/**

* =======================================================

* 获取当前中国地图矩形

* =======================================================

*/

const leftTop = geoToPixel([

CHINA_BOUNDS.left,

CHINA_BOUNDS.top

])

const rightBottom = geoToPixel([

CHINA_BOUNDS.right,

CHINA_BOUNDS.bottom

])

if (

!leftTop ||

!rightBottom

) {

return

}

const baseLeft = leftTop[0]

const baseTop = leftTop[1]

const baseWidth =

rightBottom[0] - leftTop[0]

const baseHeight =

rightBottom[1] - leftTop[1]

if (

baseWidth <= 0 ||

baseHeight <= 0

) {

return

}

/**

* =======================================================

* 根据你原来的参数计算 PNG 大小

* =======================================================

*/

const textureWidth =

baseWidth * textureConfig.scaleX

const textureHeight =

baseHeight * textureConfig.scaleY

/**

* 原来的中心算法

*/

const centerX =

baseLeft +

baseWidth / 2

const centerY =

baseTop +

baseHeight / 2

/**

* 原来的 offset

*/

const textureCenterX =

centerX +

textureConfig.offsetX

const textureCenterY =

centerY +

textureConfig.offsetY

/**

* =======================================================

* 保存初始状态

* =======================================================

*/

initialState = {

anchorCenter: [

anchorCenter[0],

anchorCenter[1]

],

anchorX: [

anchorX[0],

anchorX[1]

],

anchorY: [

anchorY[0],

anchorY[1]

],

textureCenter: [

textureCenterX,

textureCenterY

],

textureWidth,

textureHeight

}

/**

* 第一次直接绘制

*/

renderTexture(

textureCenterX,

textureCenterY,

textureWidth,

textureHeight

)

}

/**

* =========================================================

* 根据地图当前状态更新 PNG

* =========================================================

*

* 这是整个方案最关键的地方。

*

* 不再:

*

* 当前地图矩形 -> 重新计算 PNG

*

* 而是:

*

* 初始地图

* ↓

* 当前地图

* ↓

* 计算实际 X/Y 缩放

* ↓

* 计算实际位移

* ↓

* 同样的 transform 应用到 PNG

*/

function updateChinaTexture() {

if (!chart) return

if (!initialState) {

return

}

/**

* =======================================================

* 获取当前三个参考点

* =======================================================

*/

const currentCenter = geoToPixel(

GEO_ANCHORS.center

)

const currentX = geoToPixel(

GEO_ANCHORS.x

)

const currentY = geoToPixel(

GEO_ANCHORS.y

)

if (

!currentCenter ||

!currentX ||

!currentY

) {

return

}

/**

* =======================================================

* 计算初始参考距离

* =======================================================

*/

/**

* X 方向初始距离

*/

const initialDistanceX =

initialState.anchorX[0] -

initialState.anchorCenter[0]

/**

* Y 方向初始距离

*/

const initialDistanceY =

initialState.anchorY[1] -

initialState.anchorCenter[1]

if (

Math.abs(initialDistanceX) < 0.0001 ||

Math.abs(initialDistanceY) < 0.0001

) {

return

}

/**

* =======================================================

* 当前参考距离

* =======================================================

*/

const currentDistanceX =

currentX[0] -

currentCenter[0]

const currentDistanceY =

currentY[1] -

currentCenter[1]

/**

* =======================================================

* 得到地图真正的 X / Y 缩放比例

* =======================================================

*/

const scaleX =

currentDistanceX /

initialDistanceX

const scaleY =

currentDistanceY /

initialDistanceY

/**

* 防止异常值

*/

if (

!Number.isFinite(scaleX) ||

!Number.isFinite(scaleY) ||

scaleX <= 0 ||

scaleY <= 0

) {

return

}

/**

* =======================================================

* 计算纹理相对于初始锚点的位置

* =======================================================

*/

const textureOffsetX =

initialState.textureCenter[0] -

initialState.anchorCenter[0]

const textureOffsetY =

initialState.textureCenter[1] -

initialState.anchorCenter[1]

/**

* =======================================================

* 地图缩放后:

*

* 初始:

*

* Anchor -------- Texture

*

* 缩放:

*

* Anchor ---------------- Texture

*

* 所以纹理相对于 Anchor 的距离也需要一起缩放。

* =======================================================

*/

const newTextureCenterX =

currentCenter[0] +

textureOffsetX * scaleX

const newTextureCenterY =

currentCenter[1] +

textureOffsetY * scaleY

/**

* =======================================================

* PNG 本身也跟着缩放

* =======================================================

*/

const newTextureWidth =

initialState.textureWidth *

scaleX

const newTextureHeight =

initialState.textureHeight *

scaleY

/**

* 绘制

*/

renderTexture(

newTextureCenterX,

newTextureCenterY,

newTextureWidth,

newTextureHeight

)

}

/**

* =========================================================

* 真正绘制 PNG

* =========================================================

*/

function renderTexture(

centerX: number,

centerY: number,

width: number,

height: number

) {

if (!chart) return

/**

* 根据中心点算左上角

*/

const left =

centerX -

width / 2

const top =

centerY -

height / 2

chart.setOption({

graphic: {

type: 'image',

id: 'china-texture',

left,

top,

/**

* PNG 在下面

*

* geo 在上面。

*

* 这样青色边界可以覆盖 PNG。

*/

z: 1,

silent: true,

style: {

image: chinaBg,

width,

height,

opacity: 1

}

}

})

}

/**

* =========================================================

* 浏览器尺寸变化

* =========================================================

*/

function handleResize() {

if (!chart) return

chart.resize()

/**

* resize 后:

*

* 地图像素位置变了,

* 但当前地图的 roam 状态仍然存在。

*

* 所以不能重新初始化 initialState。

*

* 直接按照当前地图状态重新计算纹理。

*/

requestAnimationFrame(() => {

updateChinaTexture()

})

}

/**

* =========================================================

* mounted

* =========================================================

*/

onMounted(async () => {

await nextTick()

await initMap()

window.addEventListener(

'resize',

handleResize

)

})

/**

* =========================================================

* beforeUnmount

* =========================================================

*/

onBeforeUnmount(() => {

window.removeEventListener(

'resize',

handleResize

)

if (chart) {

chart.off('georoam')

chart.dispose()

chart = null

}

initialState = null

})

</script>

<style scoped>

.china-map-page {

width: 100%;

height: 100%;

min-height: 600px;

overflow: hidden;

background: #000;

position: relative;

}

.china-map {

width: 100%;

height: 100%;

}

</style>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询