vue3+echarts绘制某省区县地图

vue3+echarts绘制某省区县地图

工作中经常需要画各种各样的图,echarts是使用最多的工具,接近春节,想把之前画的echarts图做一个整合,方便同事和自己随时使用,因此用vue3专门写了个web项目,考虑之后不断完善

其中有这么个需求,需要展示某省各区县的数据,写在vue3项目中,最终展示结果如下:

大体的思路如下:

  1. 在阿里云dataV数据可视化平台获取数据
  2. 整合某省各区县的数据成为一个单独的文件
  3. echarts中注册这个省的地图
  4. echarts画图

主要用的程序语言是JavaScript和Python

下面详细介绍,有些技术细节也是自己经常遇到的,通过这段时间强化训练,感觉对echarts越来越熟练了

一、阿里云dataV地图数据获取

首先上地址,阿里云数据可视化平台,感谢阿里和高德提供如此牛逼的工具

然后选择点击自己所需的省份,比如上面图示的河北

接下来依次点击河北省各地级市,比如我点了石家庄,此时右侧出现了一个json链接,如下图,复制那个链接

如果浏览器装了解析json文件的插件,就会显示这个json文件的数据,如果安装插件,应该会直接把这个json文件下载下来,json数据如下:

接下来,依次去点击河北其他城市的地图,并获取数据,也可以写个爬虫的程序,挺简单的

二、将各地市的数据,整合成一个省的数据

其实思路就是把单个json中的features提取出来,然后整合到一个json文件中去

把上一步下载好的所有文件放到一个目录中,如folder

接下来用Python处理一下

python 复制代码
    features = []  # 初始化 features 列表
    for file in os.listdir(folder):
        filename = os.path.join(folder, file)
        try:
            with open(filename, 'r', encoding='utf-8') as f:
                data = json.load(f)
                features.extend(data.get('features', []))
        except (IOError, json.JSONDecodeError) as e:
            print(f"Error reading JSON file {filename}: {e}")

    json_file = {
        "type": "FeatureCollection",
        "features": features    
    }

    # 导出为 JSON 文件
    output_file_path = 'hebei_combined.json'
    with open(output_file_path, 'w', encoding='utf-8') as output_file:
        json.dump(json_file, output_file, ensure_ascii=False, indent=2)

通过处理后,得到的数据样式如下:

我不太喜欢在vue项目中直接使用json,因为很多情况下都需要异步引入,对于没有后端的项目,写起来比较费劲。更为致命的是,echarts对各种异步的操作非常不友好,经常在等待数据的时候,发现数据还没有返回,就会各种报错。我更倾向于把数据写入到js文件中,然后对外暴露,实际上这个项目我也是这么操作的,我把json里的内容放进同名js文件中,然后按需向外暴露,对象名为hebeiAreas

三、echarts注册地图

echarts中注册地图非常简单,就两步:

  1. 导入地图数据
  2. 注册

体现在程序中如下:

javascript 复制代码
import * as echarts from "echarts";
import { hebeiAreas } from '@/assets/js/areasOfProvince/hebei_combined'
// 注册
echarts.registerMap('proMap', hebeiAreas)

这里的注册需要写在正确的地方,如果只画一个图,写在哪里都无所谓,如果涉及到多个省份的切换,我建议写在切换成功的地方,或者是重绘地图的地方

四、绘图以及踏过的坑

绘图就是正儿八经写代码,我先上完整代码

html 复制代码
<template>
    <div class="container">
        <div class="top">
            <el-select v-model="province" placeholder="请选择省份" @change="choosePro" style="width: 120px">
                <el-option v-for="(item, index) in provinces" :key="index" :label="item.label"
                    :value="item.value"></el-option>
            </el-select>
            <el-button type="primary" style="margin-left: 10px;" @click="changeData">更换数据</el-button>
            <input ref="input" type="file" style="display: none" @change="handleFileChange" />

        </div>
        <div class="proMap" ref="proMap">

        </div>
    </div>
</template>

<script setup>
import { ref, onMounted, h } from 'vue'
import { provinces } from './data/provinceName'
import { ElMessage, ElNotification } from 'element-plus'
import * as echarts from "echarts";
import { areas } from '@/assets/js/areas'
import { hebeiAreas } from '@/assets/js/areasOfProvince/hebei_combined'

const province = ref('hebei')
const provinceZH = ref('河北')
const provinceCode = ref('13')
const proMap = ref()

const drawData = ref([])
const maxData = ref(100)

const getMaxData = () => {
    const arr = []
    arr.push(drawData.value.map(item => item.value))
    maxData.value = Math.max(...arr[0])
}

const getData = () => {
    const areasOfCurrentProvince = areas.filter(item => item.provinceCode == provinceCode.value)
    areasOfCurrentProvince.forEach(item => {
        drawData.value.push({
            name: item.name,
            value: Math.floor(Math.random() * 101)
        })
    });
    getMaxData()
}

// 更换省份
const choosePro = () => {
    console.log(province.value)
    if (province.value != 'hebei') {
        ElNotification({
            title: '提醒',
            message: h('i', { style: 'color: teal' }, '省份到区县分块需要处理大量数据,功能待后期完成,现在只做了河北的'),
            duration: 0
        })
        province.value = 'hebei'
    }
}
// 更换数据
// 隐藏输入框的dom
const input = ref()
const changeData = () => {
    ElNotification({
        title: '提醒',
        message: h('i', { style: 'color: teal' }, '请务必使用当前省份下的区县数据,否则无法显示正确的数据'),
        duration: 0
    })

    input.value.click()
}
const handleFileChange = async event => {
    const file = event.target.files[0]
    const reader = new FileReader()
    reader.readAsText(file, "UTF-8")
    reader.onload = async (evt) => {
        const fileString = await evt.target.result
        const count = fileString.trim().split('\n').length
        console.log(count)
        const handleData = []
        for (let i = 0; i < count; i++) {
            const fileline = fileString.split("\n")[i].split('\t')
            handleData.push({ name: fileline[0], value: parseInt(fileline[1]) })
        }
        // 更换数据
        drawData.value = handleData

        getMaxData()
        drawProMap()
    }
}
// 画地图相关
let initMap
const drawProMap = () => {
    echarts.registerMap('proMap', hebeiAreas)
    if (initMap != null && initMap != "" && initMap != undefined) {
        initMap.dispose(); //销毁
    }
    initMap = echarts.init(proMap.value)
    initMap.setOption({
        backgroundColor: "transparent", // 设置背景色透明
        tooltip: {
            show: true,
        },
        
        visualMap: {
            text: ["", ""],
            showLabel: true,
            left: "200",
            bottom: "100",
            min: 0,
            max: maxData.value,
            inRange: {
                color: ["#edfbfb", "#b7d6f3", "#40a9ed", "#3598c1", "#215096"],
            },
            // splitNumber: 5,
            seriesIndex: "0",
        },
        series: [
            {
                type: "map",
                map: 'proMap',
                tooltip: {
                    trigger: 'item',
                    formatter: function (params) {
                        // params 包含了鼠标悬浮时的相关信息
                        return params.name + '<br/>' + '数值: ' + params.value;
                    }
                },
                zoom: 1,
                label: {
                    show: false, // 显示地市名称
                    color: "#000",
                    align: "center",
                },
                top: "10%",
                left: "center",
                aspectScale: 0.75,
                roam: true, // 地图缩放和平移
                itemStyle: {
                    borderColor: "#3ad6ff", // 省分界线颜色  阴影效果的
                    borderWidth: 1,
                    areaColor: "#F5F5F5",
                    opacity: 1,
                },
                // 控制鼠标悬浮上去的效果
                emphasis: {
                    // 聚焦后颜色
                    disabled: false, // 开启高亮
                    label: {
                        align: "center",
                        color: "#ffffff",
                    },
                    itemStyle: {
                        color: "#ffffff",
                        areaColor: "#0075f4", // 阴影效果 鼠标移动上去的颜色
                    },
                },
                z: 2,
                data: drawData.value,
            }
        ]
    })
    window.addEventListener("resize", () => {
        initMap.resize();
    });
}

onMounted(() => {
    getData()
    setTimeout(() => { drawProMap() }, 200)
})
</script>

<style lang="scss" scoped>
.top {
    padding: 5px;
    width: 100%;
    box-shadow: rgba(17, 17, 26, 0.1) 0px 0px 16px;
}

.proMap {
    height: 95%;
    width: 95%;
}
</style>

以上代码有自己踏过的不少坑,我都说明一下,肯定还有其他坑,一句话,echarts全是坑

  1. getData()是生成地图对应数据的方法,我这里用了随机数,数据格式如下:

    javascript 复制代码
    [
        {name: '涞源县', value: 100},
        ....
    ]

    就是由key为name和value对象组成的数组

    getMaxData()是获取上面数组中的value的最大值,这主要是绘图的时候,图例范围的最大值设置

    drawProMap()是绘制地图的方法

  2. 坑1:注意onMounted钩子中的写法:

    javascript 复制代码
    onMounted(() => {
        getData()
        setTimeout(() => { drawProMap() }, 200)
    })

    挂载组件之前,先要获取数据,然后组件出现,就应该有图出现,这里我设置了0.2s的延时画图,原因是需要先等dom渲染完成后再画图,不然会直接报错

  3. 坑2:画图dom的宽和高必须要先设置,看我的样式:

    css 复制代码
    .proMap {
        height: 95%;
        width: 95%;
    }

    这必须写,不然图出不来,还会报警说无法获取dom的宽高

  4. 坑3:地图dom的初始化问题:

    看相关的代码

    javascript 复制代码
    // 画地图相关
    let initMap
    const drawProMap = () => {
        echarts.registerMap('proMap', hebeiAreas)
        if (initMap != null && initMap != "" && initMap != undefined) {
            initMap.dispose(); //销毁
        }
        initMap = echarts.init(proMap.value)
        /*
        省略其他代码
        */
    }

    一般情况下,可能我们会在画图的时候,直接就是:

    javascript 复制代码
    const initMap = echarts.init(proMap.value)

    上来就直接初始化画图的dom,可能的情况是,如果是在相同的dom上重绘echarts图,控制台就会报警(并非报错,效果会正常出现),说这个dom上本来就存在echarts图,所以在初始化之前,正确的操作是判断dom上的echarts图是否占用,占用的话,就销毁,也就是initMap.dispose();

  5. 从上面的代码可以看出,省份是可以选择的,数据也是可以改的

    • 先说改数据。改数据的逻辑是设定一个隐藏的input dom元素,为什么要用input,因为input可以打开文件,如下代码,其中type="file"就是打开文件,change是文件改变的事件

      html 复制代码
      <input ref="input" type="file" style="display: none" @change="handleFileChange" />

      由于这个dom是隐藏的(style="display: none"),所以打开文件应该是由按钮来控制,也就是下面这行代码

      html 复制代码
      <el-button type="primary" style="margin-left: 10px;" @click="changeData">更换数据</el-button>

      它俩的逻辑关系是:

      • 点击按钮,隐藏的input按钮实现点击事件,如下代码:

        javascript 复制代码
        const input = ref()
        const changeData = () => {
            ElNotification({
                title: '提醒',
                message: h('i', { style: 'color: teal' }, '请务必使用当前省份下的区县数据,否则无法显示正确的数据'),
                duration: 0
            })
        
            input.value.click()
        }
      • 接下来就会触发input的文件打开功能,选定文件后,就会执行handleFileChange方法,在这个方法中,使用了处理txt文本文件的方法,需要注意其中的异步操作,并且有处理换行以及按tab分割的逻辑,这里需要根据个人的项目进行适配,处理好数据后,替换画图的数据即可,然后执行获取最大值和画图方法,相关代码如下:

        javascript 复制代码
        const handleFileChange = async event => {
            const file = event.target.files[0]
            const reader = new FileReader()
            reader.readAsText(file, "UTF-8")
            reader.onload = async (evt) => {
                const fileString = await evt.target.result
                const count = fileString.trim().split('\n').length
                console.log(count)
                const handleData = []
                for (let i = 0; i < count; i++) {
                    const fileline = fileString.split("\n")[i].split('\t')
                    handleData.push({ name: fileline[0], value: parseInt(fileline[1]) })
                }
                // 更换数据
                drawData.value = handleData
        
                getMaxData()
                drawProMap()
            }
        }
    • 再说切换省份。我的代码中并没有实现切换省份的逻辑,因为需要大量的数据支撑,也就是说要把全国34个省级行政区划(包括港澳台)的地图文件都获取,切换成功后,异步引入地图文件,并注册地图,然后画图,这一步我在其他地方实现过,同样存在坑

      坑4:异步导入json文件,首次绘图会出现报错,报错如下:

      Error: Invalid geoJson format coordinate.charCodeAt is not a function

      但是刷新页面就正常了,查了一下相关的资料,有人解释是:

      因为 echarts 会绘制解析 json 之后 执行 decode 方法 后 会将其 UTF8Encoding 的值 从 true 改为false,第二次绘制 时如果 为 false 则 不需要走 decode 方法,如果每次都是新引入的 json,那每次都走 decode 就会报错


                              版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
      

      原文链接:https://blog.csdn.net/m0_37805167/article/details/122553278

他建议是 Object.assign({}, json) 拷贝一次,解释是异步获取的数据是只读的,echarts无法更改,所以会报错,需要拷贝一下,确实在某些情况下能解决,但通过路由切换到需要画图的页面上来时,依然会报错,目前还没有找到靠谱的解决方案,可能不用json而是用js会解决这个问题,需要我来确认

相关推荐
光影少年6 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
As977_7 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
susu10830189119 分钟前
vue3 css的样式如果background没有,如何覆盖有background的样式
前端·css
Ocean☾11 分钟前
前端基础-html-注册界面
前端·算法·html
Rattenking11 分钟前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
Dragon Wu13 分钟前
前端 Canvas 绘画 总结
前端
CodeToGym18 分钟前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
~甲壳虫19 分钟前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js
~甲壳虫23 分钟前
说说webpack proxy工作原理?为什么能解决跨域
前端·webpack·node.js
Cwhat24 分钟前
前端性能优化2
前端