前端3D轨迹可视化渲染
说在前面
项目需要做一个在3D传播轨迹功能,从网上找了一些资料,决定使用echarts来展示,地图是2维世界(包括中国各省份)的地图,如果要使用3D地图也可以。
echarts官网。
效果
为了展示效果,做了个小demo。点击不同的作物,有不同的作物轨迹展示。并且有国外到国内、国内的、总的传播轨迹,可以点击右下方的图例进行切换展示。
普通html页面展示3D轨迹
1.引入echarts和jquery
在< head >标签中引入
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.2.2/echarts.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
2.设置地图所需要的容器
<div id="main" style="height: 100vh;">
</div>
3.js代码
重要js代码中都有注释,注释中的()括号内与注释无关
//全球部分点的坐标,也就是传播路径所需要起点、终点坐标
var geoCoordMap = {
石家庄: [114.4995, 38.1006],
哈尔滨: [127.9688, 45.368],
杭州: [119.5313, 29.8773],
广州: [113.5107, 23.2196],
武汉: [114.3896, 30.6628],
南昌: [116.0046, 28.6633],
长沙: [113.0823, 28.2568],
西安: [109.1162, 34.2004],
上海: [121.4648, 31.2891],
天津: [117.4219, 39.4189],
重庆: [107.7539, 30.1904],
尼日利亚: [-4.388361, 11.186148],
洛杉矶: [-118.24311, 34.052713],
香港: [114.195466, 22.282751],
芝加哥: [-87.801833, 41.870975],
加纳库马西: [-4.62829, 7.72415],
曼彻斯特: [-1.657222, 51.886863],
汉堡: [10.01959, 54.38474],
阿拉木图: [45.326912, 41.101891],
伊尔库茨克: [89.116876, 67.757906],
巴西: [-48.678945, -10.493623],
埃及: [31.815593, 31.418032],
巴塞罗纳: [2.175129, 41.385064],
柬埔寨: [104.88659, 11.545469],
米兰: [9.189948, 45.46623],
蒙得维的亚: [-56.162231, -34.901113],
莫桑比克: [32.608571, -25.893473],
阿尔及尔: [3.054275, 36.753027],
阿联酋迪拜: [55.269441, 25.204514],
布达佩斯: [17.108519, 48.179162],
悉尼: [150.993137, -33.675509],
加州: [-121.910642, 41.38028],
墨尔本: [144.999416, -37.781726],
墨西哥: [-99.094092, 19.365711],
温哥华: [-123.023921, 49.311753]
};
var color =['#a6c84c', '#ffa022', '#46bee9']
//传播轨迹的起点和终点,value可以设置点的涟漪圆圈特效大小
var BJData = [
[{ name: '巴西' },{ name: '广州', value:100}],
[{ name: '广州' }, { name: '上海', value: 95 }],
[{ name: '广州' }, { name: '重庆', value: 90 }],
[{ name: '广州' }, { name: '长沙', value: 80 }],
[{ name: '广州' }, { name: '杭州', value: 70 }],
[{ name: '广州' }, { name: '石家庄', value: 60 }],
[{ name: '广州' }, { name: '哈尔滨', value: 50 }],
[{ name: '广州' }, { name: '南昌', value: 40 }],
[{ name: '广州' }, { name: '天津', value: 30 }],
[{ name: '广州' }, { name: '武汉', value: 20 }],
[{ name: '广州' }, { name: '西安', value: 10 }],
[{ name: '广州' }, { name: '广州', value: 10 }]
];
//数据转换方法,返回起点、终点和两点的经纬度
var convertData = function(data){
var res = [];
for (var i = 0; i < data.length; i++) {
var dataItem = data[i];
// console.log(color[i])
var fromCoord = geoCoordMap[dataItem[0].name];
var toCoord = geoCoordMap[dataItem[1].name];
if (fromCoord && toCoord) {
res.push({
fromName: dataItem[0].name,
toName: dataItem[1].name,
coords: [fromCoord, toCoord],
/* lineStyle:{ //设置单个线样式
color:color[i],
width: 1,
opacity: 0.6, //图像透明度,为0时,线完全透明
curveness: 0.2 //值越大,曲度越大
} */
});
}
}
return res;
};
var series = [];
[
[ ,BJData]
].forEach(function (item, i) {
series.push(
{
name: 'xx作物', //轨迹名称,可用于tooltip显示
type: 'lines', //类型
zlevel: 1,
//symbol 的取值'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'和自定义图片
symbol: ['none', 'arrow'], //线的起点无特效,终点箭头展示
symbolSize: 5, //箭头大小
effect: {
show: true, //是否显示特效
period: 4, // 特效动画的时间
trailLength: 0.3, // 特效尾迹的长度。取从 0 到 1 的值,默认为 0.2,数值越大尾迹越长
color: '#a4f109', //特效轨迹颜色
//'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none',或者照片
symbol: 'arrow', // 特效图形的标记
symbolSize: 8,
loop:true,//是否循环显示特效
},
lineStyle: { //某个类型统一的样式(在下个Vue页面展示)
normal: {
color: color[i], //因为只有一个类型
width: 2,
opacity: 0.6, //图像透明度,为0时,线完全透明
curveness: 0.2 //值越大,曲度越大
}
},
data: convertData(item[1])//data需要起点、终点和两点的经纬度
},
{
type: "effectScatter", //涟漪特效动画的散点
coordinateSystem: "geo",
zlevel: 2,
rippleEffect: {
//涟漪特效
period: 4, //动画时间,值越小速度越快
brushType: "stroke", //波纹绘制方式 stroke, fill
scale: 4
//波纹圆环最大限制,值越大波纹越大
},
label: {
normal: {
show: true,
position: "right", //显示位置
offset: [5, 0], //偏移设置
formatter: "{b}" ,//圆环显示文字
/* textStyle: {
fontSize: 9,
color: '#fff'
} */
},
},
symbol: "circle",
symbolSize: function (val) {//根据value值设置圆圈大小
//console.log(val)//下面data属性中的value
var level = 0 ;
var state= Math.floor(val[2]/40) ;
switch (state)
{
case 0: level=0; break;
case 1: level=1; break;
case 2: level=2; break;
case 3: level=3; break;
case 4: level=4; break;
case 5: level=5; break;
case 6: level=6; break;
case 7: level=7; break;
case 8: level=8; break;
case 9: level=9; break;
default: level=10;
}
return 5+level; //圆环大小
},
data: item[1].map(function (dataItem) {
return {
name: dataItem[1].name, //需要展示在地图上的点。
value: geoCoordMap[dataItem[1].name]
.concat([dataItem[1].value])
};
})
});
});
option = {
//容器背景颜色
backgroundColor: '#6daef5',
//悬浮提示
tooltip: {
trigger: "item",
//悬浮提示框颜色
backgroundColor: "#fff",
borderColor: "#FFFFCC",
showDelay: 0,
hideDelay: 0,
transitionDuration: 0,
//下面的提示框内容可以自己设置
/* formatter: function (params, ticket, callback) {
//根据业务自己拓展要显示的内容
var res = "";
var name = params.name;
var value = params.value[params.seriesIndex + 1];
res = "<span style='color:#fff;'>" + name.toString().split(' ')[0]
+ "</span><br/>作物:" + name.toString().split(' ')[1];
return res;
} */
},
//图例
legend: {
orient: 'vertical',
top: 'bottom',
left: 'right',
//data:['xx作物'] //可以不设置
textStyle: {
color: '#fff'
},
selectedMode: 'single'
},
//effectScatter的颜色渐变
/* color:{
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: 'red' // 0% 处的颜色
}, {
offset: 1, color: 'blue' // 100% 处的颜色
}],
global: false // 缺省为 false
}, */
geo: {
map: "world",
label: {
emphasis: {
show: true,
textStyle : {
fontSize:9,
color: '#fff'
}
}
},
roam: true, //是否允许缩放
layoutCenter: ["50%", "50%"], //地图位置
layoutSize: "180%", //地图大小
itemStyle: {
normal: {
color:'#eef3e4', //地图背景颜色
borderColor: "#5bc1c9" //省市边界线
},
emphasis: {
areaColor: "rgba(37, 43, 61, .5)", //悬浮背景
borderColor:'#fff', //鼠标悬浮边框颜色
borderWidth:3,
}
}
},
series: series
};
$.get('.././world.json',function (jsonData) {//地图geojson格式
echarts.registerMap('world', jsonData);
var chart = echarts.init(document.getElementById('main'));
chart.setOption(option);
});
注意 echarts可以结合百度地图api、也可以使用geojson格式来加载地图
国内geojson:阿里云数据可视化平台,国内各省各市的geojson都可以从阿里云可视化平台下载。全球的可以从官网下载,不过从5.1.0版本开始就没有地图了,如果要下载就要点5.0版本,从里面找到world.js即可,3D地球的也在里面。如果要全球地图结合中国各省的geojson可以私信我(是自己结合world.json和中国各省json结合的)。
或者从下面的方式引入。下面就是官方例子里面的代码,不过好像不行
官方迁徙图例子
官方例子要切换到5.1版本前。
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts@5.0.2/dist/echarts.min.js"></script>
<!-- Uncomment this line if you want to dataTool extension
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts@5.0.2/dist/extension/dataTool.min.js"></script>
-->
<!-- Uncomment this line if you want to use gl extension -->
<!-- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts-gl@2/dist/echarts-gl.min.js"></script>
-->
<!-- Uncomment this line if you want to echarts-stat extension
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts-stat@latest/dist/ecStat.min.js"></script>
-->
<!-- Uncomment this line if you want to use map -->
<!-- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts@5.0.2/map/js/china.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts@5.0.2/map/js/world.js"></script>
-->
<!-- Uncomment these two lines if you want to use bmap extension -->
<!-- <script type="text/javascript" src="https://api.map.baidu.com/api?v=2.0&ak=您的密钥"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts@{{version}}/dist/extension/bmap.min.js"></script>
-->
vue3结合echarts展示3D轨迹
因为项目需求,就将轨迹显示的封装到一个js文件中了。也遇到了一些很奇怪的问题。就是上面的效果图的小demo
1. 引入echarts和jquery
在public下的index.html文件中引入
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.2.2/echarts.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
2.封装的js文件
因为渲染地图需要dom元素,但vue中引入js文件,会先加载js文件,导致获取不到dom元素。所以只是将渲染地图所需要的option属性封装了,然后在所引入的vue页面在进行地图渲染。
//全球部分点的坐标
var geoCoordMap = {
石家庄: [114.4995, 38.1006],
哈尔滨: [127.9688, 45.368],
杭州: [119.5313, 29.8773],
广州: [113.5107, 23.2196],
武汉: [114.3896, 30.6628],
南昌: [116.0046, 28.6633],
长沙: [113.0823, 28.2568],
西安: [109.1162, 34.2004],
上海: [121.4648, 31.2891],
天津: [117.4219, 39.4189],
重庆: [107.7539, 30.1904],
香港: [114.195466, 22.282751],
澳门:[113.549134,22.198751],
昆明:[102.73,25.04],
乌鲁木齐:[87.68,43.77],
宁夏:[106.258754,38.471317],
兰州:[103.826308,36.059421],
成都:[104.075931,30.651651],
北京:[116.407526,39.904030],
阿富汗:[ 64.56,32.12],
巴西: [-48.678945, -10.493623],
秘鲁:[ -76.33,-7.26],
吕宋:[ 121.16,14.24],
印尼:[114.56,-3.06],
墨西哥: [-99.094092, 19.365711],
印度:[78.53,26.10],
开罗:[ 30.05,31.50],
尼日利亚: [-4.388361, 11.186148],
洛杉矶: [-118.24311, 34.052713],
芝加哥: [-87.801833, 41.870975],
加纳库马西: [-4.62829, 7.72415],
曼彻斯特: [-1.657222, 51.886863],
汉堡: [10.01959, 54.38474],
阿拉木图: [45.326912, 41.101891],
伊尔库茨克: [89.116876, 67.757906],
埃及: [31.815593, 31.418032],
巴塞罗纳: [2.175129, 41.385064],
柬埔寨: [104.88659, 11.545469],
米兰: [9.189948, 45.46623],
蒙得维的亚: [-56.162231, -34.901113],
莫桑比克: [32.608571, -25.893473],
阿尔及尔: [3.054275, 36.753027],
阿联酋迪拜: [55.269441, 25.204514],
布达佩斯: [17.108519, 48.179162],
悉尼: [150.993137, -33.675509],
加州: [-121.910642, 41.38028],
墨尔本: [144.999416, -37.781726],
温哥华: [-123.023921, 49.311753]
};
//国内外轨迹颜色
var color = ['#a6c84c', '#ffa022', '#46bee9'];
//作物轨迹的起点和终点
var cropData=[
[{cropName:'菠萝'},{origin:'阿富汗'},{end:'西安'}],
[{cropName:'马铃薯'},{origin:'巴西'},{end:'澳门'}],
[{cropName:'番茄'},{origin:'秘鲁'},{end:'澳门'}],
[{cropName:'红薯'},{origin:'吕宋'},{end:'广州'}],
[{cropName:'茄子'},{origin:'印尼'},{end:'昆明'}],
]
//轨迹+扩散的起点和终点
var cropGlobe=[
];
var cropOversea=[
[{ name: '' },{ name: '', value:100}]
]
var cropInter=[
[{ name: '广州' }, { name: '上海', value: 10}],
[{ name: '广州' }, { name: '重庆', value: 10}],
[{ name: '广州' }, { name: '长沙', value: 10 }],
[{ name: '广州' }, { name: '杭州', value: 10}],
[{ name: '广州' }, { name: '石家庄', value: 10 }],
[{ name: '广州' }, { name: '哈尔滨', value: 10 }],
[{ name: '广州' }, { name: '南昌', value: 10 }],
[{ name: '广州' }, { name: '天津', value: 10 }],
[{ name: '广州' }, { name: '武汉', value: 10 }],
[{ name: '广州' }, { name: '西安', value: 10 }],
[{ name: '广州' }, { name: '香港', value: 10 }],
[{ name: '广州' }, { name: '澳门', value: 10 }],
[{ name: '广州' }, { name: '广州', value: 10 }],
[{ name: '广州' }, { name: '昆明', value: 10 }],
[{ name: '广州' }, { name: '乌鲁木齐', value: 10 }],
[{ name: '广州' }, { name: '宁夏', value: 10 }],
[{ name: '广州' }, { name: '兰州', value: 10 }],
[{ name: '广州' }, { name: '成都', value: 10 }],
[{ name: '广州' }, { name: '北京', value: 10 }],
]
//匹配作物,并设置数据
function matchCrop(cropName){
for(let i=0; i<cropData.length; i++){
let dataItem = cropData[i];
if(dataItem[0].cropName == cropName){
//国外轨迹设置
cropOversea[0][0].name = dataItem[1].origin;
cropOversea[0][1].name = dataItem[2].end;
//console.log(cropOversea)
//国内轨迹设置
cropInter.forEach(function(item, i){
item[0].name = dataItem[2].end
})
// console.log(cropInter)
//全球轨迹设置
cropGlobe = cropOversea.concat(cropInter)
//console.log(cropGlobe)
return;
}
}
}
//将地点转成经纬度(生成轨迹方法)
var convertData = function(data){
var res = [];
for (var i = 0; i < data.length; i++) {
var dataItem = data[i];
var fromCoord = geoCoordMap[dataItem[0].name];
var toCoord = geoCoordMap[dataItem[1].name];
if (fromCoord && toCoord) {
res.push({
fromName: dataItem[0].name,
toName: dataItem[1].name,
coords: [fromCoord, toCoord],
});
}
}
return res;
};
//点效果数据(因为是将所有点渲染出来包括起点和终点)
function pointEffect(items){
let item = items.slice(0,1);
//深拷贝
var obj1 = JSON.stringify(item);
var obj2 = JSON.parse(obj1);
obj2[0][1].name = obj2[0][0].name;
items.unshift(obj2[0])
return items
}
export function mapTrack(cropName){
matchCrop(cropName);//设置数据
var option;
var series =[];
[
['国外',cropOversea],
['国内',cropInter],
['全球',cropGlobe]
].forEach(function(item, i){
series.push(
{
name: item[0]+'-'+cropName,
type: 'lines',
zlevel: 1,
symbol: ['none', 'arrow'],
symbolSize:7,
effect: {
show: true, //是否显示特效
period: 4, // 特效动画的时间
trailLength: 0.3, // 特效尾迹的长度。取从 0 到 1 的值,默认为 0.2,数值越大尾迹越长
color: '#fff', //特效轨迹颜色
//'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none',或者照片
symbol: 'arrow', // 特效图形的标记
symbolSize: 8,
loop:true,//是否循环显示特效
},
lineStyle: {
normal: {
color: color[i],
width: 2,
opacity: 0.6, //图像透明度,为0时,线完全透明
curveness: 0.2 //值越大,曲度越大
}
},
data: convertData(item[1])
},
{
type: "effectScatter",
coordinateSystem: "geo",
zlevel: 2,
rippleEffect: {
//涟漪特效
period: 4, //动画时间,值越小速度越快
brushType: "stroke", //波纹绘制方式 stroke, fill
scale: 4
//波纹圆环最大限制,值越大波纹越大
},
label:{
show:true,
position:"right",
formatter: '{b}',
},
symbol:"circle",
symbolSize: function(val){
var level = 0 ;
var state= Math.floor(val[2]/40) ;
switch (state)
{
case 0: level=0; break;
case 1: level=1; break;
case 2: level=2; break;
case 3: level=3; break;
case 4: level=4; break;
case 5: level=5; break;
case 6: level=6; break;
case 7: level=7; break;
case 8: level=8; break;
case 9: level=9; break;
default: level=10;
}
return 4+level; //圆环大小
},
itemStyle:{
color: color[i]
},
data: pointEffect(item[1]).map(function(dataItem){
return{
name: dataItem[1].name,
value:geoCoordMap[dataItem[1].name].concat([dataItem[1].value])
}
})
}
)
});
option={
//地图外颜色
backgroundColor: '#6daef5',
//悬浮提示
tooltip: {
trigger: 'item',
backgroundColor: "#fff",
borderColor: "#FFFFCC",
showDelay: 0,
hideDelay: 0,
transitionDuration: 0,
},
legend: {
orient: 'vertical',
top: 'bottom',
left: 'right',
textStyle: {
color: '#fff'
},
selectedMode: 'single'
},
geo: {
// show:true,
map: "world",
label: {
emphasis: {
show: true,
textStyle : {
fontSize:9,
color: '#fff'
}
}
},
roam: true, //是否允许缩放
layoutCenter: ["50%", "50%"], //地图位置50,50
layoutSize: "240%",//180
itemStyle: {
normal: {
color:'#eef3e4', //地图背景颜色
borderColor: "#5bc1c9" //省市边界线
},
emphasis: {
areaColor: "rgba(37, 43, 61, .5)", //悬浮背景
borderColor:'#fff', //鼠标悬浮边框颜色
borderWidth:3,
}
}
},
series: series
};
//重新刷新作物数据
cropGlobe=[
];
cropOversea=[
[{ name: '' },{ name: '', value:100}]
]
cropInter=[
[{ name: '广州' }, { name: '上海', value: 10}],
[{ name: '广州' }, { name: '重庆', value: 10}],
[{ name: '广州' }, { name: '长沙', value: 10 }],
[{ name: '广州' }, { name: '杭州', value: 10}],
[{ name: '广州' }, { name: '石家庄', value: 10 }],
[{ name: '广州' }, { name: '哈尔滨', value: 10 }],
[{ name: '广州' }, { name: '南昌', value: 10 }],
[{ name: '广州' }, { name: '天津', value: 10 }],
[{ name: '广州' }, { name: '武汉', value: 10 }],
[{ name: '广州' }, { name: '西安', value: 10 }],
[{ name: '广州' }, { name: '香港', value: 10 }],
[{ name: '广州' }, { name: '澳门', value: 10 }],
[{ name: '广州' }, { name: '广州', value: 10 }],
[{ name: '广州' }, { name: '昆明', value: 10 }],
[{ name: '广州' }, { name: '乌鲁木齐', value: 10 }],
[{ name: '广州' }, { name: '宁夏', value: 10 }],
[{ name: '广州' }, { name: '兰州', value: 10 }],
[{ name: '广州' }, { name: '成都', value: 10 }],
[{ name: '广州' }, { name: '北京', value: 10 }],
]
return option;
}
3.将world.json文件和js文件放入vue项目中
4.vue页面引入js并渲染地图
<template>
<div id="all">
<button @click="setTrack1()">菠萝</button>
<button @click="setTrack2()">马铃薯</button>
<button @click="setTrack3()">番茄</button>
<button @click="setTrack4()">红薯</button>
<button @click="setTrack()">轨迹</button>{{cropName}}
<div id="main" style="height: 90vh;">
</div>
</div>
</template>
<script>
import {mapTrack} from '../../public/MapTrack'
import {ref} from 'vue'
export default {
setup () {
var cropName = ref('作物');
//渲染地图
const setTrack = () =>{
var option = mapTrack(cropName.value)//设置option
$.get('./world.json',function (jsonData) {//json放在同级目录下或者public下,按‘./world.json’引入
echarts.registerMap('world', jsonData);//注册地图
var chart = echarts.init(document.getElementById('main'))
chart.setOption(option);//设置option
});
}
const setTrack1 =()=>{
cropName.value = '菠萝';
}
const setTrack2 =()=>{
cropName.value = '马铃薯';
}
const setTrack3 =()=>{
cropName.value = '番茄';
}
const setTrack4 =()=>{
cropName.value = '红薯';
}
return {
setTrack,
setTrack1,
setTrack2,
setTrack3,
setTrack4,
cropName
}
},
}
</script>
<style>
</style>
遇到的奇怪错误
错误1:Uncaught SyntaxError: Unexpected token < in JSON at position 0
如果遇到这个错误,我是将json文件和js文件放在public下,或者需要引入vue页面的同级目录下,不要放的太深。然后get()中的文件路径按下面的方式写。
$.get('./world.json',function (jsonData) {//json放在同级目录下或者public下,按‘./world.json’引入
echarts.registerMap('world', jsonData);//注册地图
var chart = echarts.init(document.getElementById('main'))
chart.setOption(option);//设置option
});
就很奇怪,明明json文件的路径都不对,也能加载出地图。后来我改成正确的路径反而还报了上面的那个错误。
好了,3D轨迹可视化就说到这,如果有帮助,希望可以点个赞,收藏一波。