背景
公司选用的可视化工具为finereport, 只买了填充报表,但是复杂图表地图,需要单独收费,看了echarts官方提供的地图(看着从2.0到5.0了, 好像是4.x 被Apache看上了),正好符合公司的需求,故有了以下的DEMO地址:
https://wl.juneyaoair.com/airecharts/chart_background/?type=map
有问题的可以与我联系,哈哈哈
项目启动大概是2020年的6月份初,开发时间工作七七八八,对接接口,也有4,5天左右了,事隔9个月,现在再拿出看,想要看懂,还是需要花点时间理解的。
怕时间久了,就忘没有,故重新理解一遍,做个记录。
整体图标分层:
- 第一层:先注册世界地图,
- 第二层:中国地图
- 第三层: 散点图
- 第四点:线图
静态资源下载
需要世界地图经纬度的json, common对象,中国省市区的经纬度,
地图json网盘下载地址:
链接:https://pan.baidu.com/s/1jNZ_YSYAUlMYiBUFq6EV4w
提取码:405q
示例公共配置网盘下载地址:https://pan.baidu.com/s/13jhRFWaAFAiT5rqCtkvswg
提取码:ev8m
前端代码
<template lang="pug">
.wrap-chart
//- 自定义加载组件
c-loading(
:show="loadEcharts",
loadingText="图表加载中,请稍后..."
v-if="loadEcharts"
)
div(
:v-if="!loadEcharts",
id="main",
:class="{'main-province': !!provinceName}",
:style="`width: ${width};height:${height};`"
)
.tips
ul
li(
v-for="(item, index) in tips",
:key="index",
:class="{'return-world': !index}",
@click="setMap"
) {{ item }}
</template>
<script>
// js工具库,[http://lodash.net/docs/4.16.1.html](http://lodash.net/docs/4.16.1.html)
import _ from 'lodash'
// 按需应用echarts的模块,参考官方地址:[https://echarts.apache.org/zh/tutorial.html#%E5%9C%A8%E6%89%93%E5%8C%85%E7%8E%AF%E5%A2%83%E4%B8%AD%E4%BD%BF%E7%94%A8%20ECharts](https://echarts.apache.org/zh/tutorial.html#%E5%9C%A8%E6%89%93%E5%8C%85%E7%8E%AF%E5%A2%83%E4%B8%AD%E4%BD%BF%E7%94%A8%20ECharts)
import common from '~/plugins/common'
import echarts from 'echarts/lib/echarts'
import 'echarts/lib/chart/map'
import 'echarts/lib/chart/heatmap'
import 'echarts/lib/chart/effectScatter'
import 'echarts/lib/chart/lines'
require('echarts/lib/component/geo')
require('echarts/lib/component/graphic')
require('echarts/lib/component/tooltip')
require('echarts/lib/component/visualMap')
import CLoading from '~/components/Loading'
export default {
data () {
return {
// 宽度
width: this.getLens(),
// 高度
height: this.getLens('height'),
// 颜色列表
colorList: _.get(this.$route, 'query.colorList'),
// 主题色
theme: _.get(this.$route, 'query.theme') || 'blue',
// 图表类型, 目前只有一个地图目前用不到,后续可以扩展用
chartType: _.get(this.$route, 'query.type') || 'map',
// ! loading标识, 待删除
loadEcharts: true,
// 配置对象
option: {},
// 站点散点列表
scatterList: [],
// 飞机线条列表
linesList: [],
// 机场列表
airportList: [],
// 层级提示
tips: ['世界'],
// 保持访问的省名称
provinceName: '',
// 计时器
mapChartTimer: '',
// 区域计时器
provinceChartTimer: '',
// 地图缩略图
visualMap: {
type: 'piecewise',
min: 1,
max: 5,
left: '2%',
bottom: 10,
itemWidth: 16,
itemHeight: 10,
orient: 'vertical',
splitNumber: 4,
seriesIndex: 0,
show: true,
inRange: {
color: common.mapColor
},
textStyle: {
color: '#ffffff',
fontSize: 12
},
formatter: function (params) {
const num = params - 1
const categoriesList = [
'华北销售处',
'华南销售处',
'国际销售处',
'上海销售处'
]
return _.nth(categoriesList, num)
}
},
// 经纬度映射关系
geoCoordMap: {},
// 是否显示全屏
fullScreen: false
}
},
components: {
CLoading
},
mounted () {
// 初始化图表对象
this.myChart = echarts.init(document.getElementById('main'))
// 设置图表
this.setChart()
// 下钻地图
this.myChart.on('click', params => {
// 获取省会名称
this.provinceName = params.name
// 限制中文省会名称才能执行下钻,直接return
const pattern = new RegExp('[\u4E00-\u9FA5]+')
if (!pattern.test(this.provinceName)) { return }
// 清理定时器
clearTimeout(this.mapChartTimer)
clearTimeout(this.provinceChartTimer)
// 清除图表
this.myChart.clear()
// 中国地图下钻方法
this.downMap(this.myChart, this.option, this.provinceName)
})
},
methods: {
/**
* 对画布长宽配置
*/
getLens (type = 'width') {
let lenValue = _.get(this.$route, `query.${type}`)
// TODO 此处需要检查 lenValue 是否为数值
if (!lenValue) {
switch (type) {
case 'width':
lenValue = '100%'
break
case 'height':
lenValue = '100%'
break
}
return lenValue
}
return `${lenValue}px`
},
/**
* 设置图表
*/
async setChart () {
// TODO 此处可以根据不同图表类型,接入不同图表(原规划是一个页面根据展示不同图表)
switch (this.chartType) {
case 'map':
// 首次显示加载效果
this.loadEcharts = true
// 获取经纬度配置(返回是有航线在运行的站点经纬度)
await this.getCoordinateConfig()
// 设置地图
this.setMap()
break
}
},
/**
* 地图(航线)转换数据
*/
convertData (data) {
const res = []
_.forEach(data, v => {
let fromCoord = _.get(this.geoCoordMap, v.depName)
let toCoord = _.get(this.geoCoordMap, v.arrName)
if (!!fromCoord && !!toCoord) {
fromCoord = JSON.parse(fromCoord)
toCoord = JSON.parse(toCoord)
res.push({
fromName: v.depName,
toName: v.arrName,
coords: [fromCoord, toCoord],
value: 0,
flightNo: this.$util.addDefaultData(_.get(v, 'flightNo')), // 航班号
bookingNumber: this.$util.addDefaultData(_.get(v, 'bookingNumber')), // 订舱
volume: this.$util.addDefaultData(_.get(v, 'volume')), // 货量
income: this.$util.addDefaultData(_.get(v, 'income')) // 收入
})
}
})
return res
},
/**
* 获取scatter图表的数据
*/
convertSingleData () {
const res = []
let middleObj = {}
_.forEach(this.airportList, v => {
let geoCoord = _.get(this.geoCoordMap, v.airlineName)
if (geoCoord) {
geoCoord = JSON.parse(geoCoord)
middleObj = {
name: v.airlineName,
value: geoCoord.concat(0)
}
middleObj = _.assign({}, middleObj, v)
res.push(middleObj)
}
})
return res
},
/**
* 获取经纬度基础配置
*/
async getCoordinateConfig () {
const data = await this.$api.get(`/map/getAirlineLocationConfig`)
// 配置经纬度
this.geoCoordMap = data
},
/**
* 参数name有值为下钻拉取的数据
*/
async getStationData (name = '') {
const requestUrl = !name ? 'getAirlineCabinData' : 'getAirlineCabinDataToProvinceList'
const data = await this.$api.get(`/map/${requestUrl}`, {
provinceName: name
})
this.$set(this, 'airportList', data)
},
/**
* 参数name有值为下钻拉取的数据
*/
async getLinesData (name = '') {
// 有城市名称和没有城市名称是不同的列表
const requestUrl = !name ? 'getAirlineLegList' : 'getAirlineLegDetailList'
const data = await this.$api.get(`/map/${requestUrl}`, {
provinceName: name
})
return data
},
/**
* 获取站点相关联的城市列表数据
*/
async getCityList (name = '') {
const data = await this.$api.get(`/map/getRelateProvinceList`, {
provinceName: name
})
return data
},
/**
* 获取散点图数据: 站点进出港数据
*/
async getScatterList (name) {
// 获取机场站点数据和进出港数据
await this.getStationData(name)
const scatterList = []
if (_.size(this.airportList)) {
// 散点图数据
scatterList.push(
{
type: 'effectScatter',
coordinateSystem: 'geo',
zlevel: 2,
rippleEffect: {
brushType: 'stroke'
},
// hover效果
tooltip: {
show: true,
formatter: function (params) {
const data = _.get(params, 'data')
return `${params.marker}${params.name}<br/>出港订舱量:${data.outBookingTotal}<br/>出港货量:${data.outVolume}<br/>出港收入:${data.outIncome}<br/>累计班次:${data.outFlightNumber}<br/><br/>进港订舱量:${data.inBookingTotal}<br/>进港货量:${data.inVolume}<br/>进港收入:${data.inIncome}<br/>累计班次:${data.inFlightNumber}`
}
},
label: {
normal: {
show: true,
fontSize: 18,
color: '#ffffff',
position: 'right',
formatter: '{b}'
}
},
symbolSize: function (val) {
return 10
},
// 散点图的颜色值的配置
itemStyle: {
normal: {
// color: '#fa8737'
color: 'rgba(255, 255, 255, 0.7)'
}
},
data: this.convertSingleData()
}
)
}
this.scatterList = scatterList
return scatterList
},
/**
* 获取航线图数据
*/
async getLinesList (name) {
const linesData = await this.getLinesData(name)
// 拉取航线数据
// 航线数据
let i = 0
const linesList = []
if (_.size(linesData)) {
_.forEach(linesData, item => {
linesList.push(
{
type: 'lines',
zlevel: 1,
// 线特效的配置
effect: {
show: true,
// 特效动画的时间,单位为 s
period: 6,
// 特效尾迹的长度
trailLength: 0.7,
color: '#fff',
// 特效标记的大小
symbolSize: 3
},
lineStyle: {
normal: {
color: common.mapColor[i],
width: 0,
curveness: 0.2
}
},
data: this.convertData(item)
},
{
type: 'lines',
zlevel: 2,
symbol: ['none', 'arrow'],
symbolSize: 10,
effect: {
show: true,
// 特效动画的时间,单位为 s
period: 6,
// 特效尾迹的长度
trailLength: 0,
// 特效图形的标记
symbol: common.planePath,
// 特效标记的大小
symbolSize: 15
},
lineStyle: {
normal: {
color: common.planeColor[i],
// color: '#ffffff'
width: 1,
opacity: 0.6,
curveness: 0.2
}
},
data: this.convertData(item),
tooltip: {
formatter: function (params) {
// 航线上的hover
const data = _.get(params, 'data')
return `${data.fromName} - ${data.toName}<br/>航班号:${data.flightNo}<br/>订舱:${data.bookingNumber}KG<br/>货量:${data.volume}KG<br/>收入:${data.income}元`
}
}
}
)
i = i + 1
})
}
this.linesList = linesList
return linesList
},
/**
* 获取城市JSON,关联的地图的JSON合并
*/
async getCityJson (provinceName) {
// 获取当前点击的拼音
const curGeoJson = {
'type': 'FeatureCollection',
'features': [],
'UTF8Encoding': true
}
// 获取相关航线的拼音列表,以方便整理地理JSON
let cityList = await this.getCityList(provinceName)
cityList = [provinceName, ...cityList]
// 转拼音列表
const pinyinList = []
_.forEach(cityList, v => {
pinyinList.push(common.cityToPinyin[v])
})
// 拼接JSON
_.forEach(pinyinList, v => {
if (!v) return
let middleGeo = {}
middleGeo = _.cloneDeep(require(`~/static/json/provinces/${v}.json`))
curGeoJson.features = _.concat(curGeoJson.features, middleGeo.features)
})
return curGeoJson
},
/**
* 设置图表
*/
async setMap () {
this.loadEcharts = true
clearTimeout(this.mapChartTimer)
clearTimeout(this.provinceChartTimer)
this.provinceChartTimer = null
// 显示提示文本
this.tips = ['世界']
// 页面宽度变动
window.onresize = this.myChart.resize
// 世界地图json
const mapJson = require('~/static/json/world.json')
// 注册世界地图
echarts.registerMap('world', mapJson)
// 获取散点数据
await this.getScatterList()
// 获取航线数据
await this.getLinesList()
// 配置项
const option = {
// 地图背景色
backgroundColor: common.bgColor,
// hover效果
tooltip: {
show: true,
formatter: function (params) {
return params.name
}
},
// 地理位置配置, 底图
geo: {
map: 'world',
// 是否可以滚动
roam: true,
zoom: 5,
// 中国中心的经纬度
center: [104.2978515625, 35.8544921875],
// 文本颜色配置
label: {
show: true,
position: ['100%', '50%'],
normal: {
show: true,
fontSize: 14,
textStyle: {
color: '#fff'
},
position: ['100%', '50%'],
formatter: function (params) {
const name = params.name
const pattern = new RegExp('[\u4E00-\u9FA5]+')
const internationalList = {
'Japan': '日本',
'Russia': '俄罗斯',
'Canada': '加拿大',
'Hungary': '匈牙利',
'Indonesia': '印度尼西亚',
'Greece': '希腊',
'Germany': '德国',
'Italy': '意大利',
'Czech Republic': '捷克',
'Slovakia': '斯洛伐克',
'Singapore': '新加坡',
'Cambodia': '柬埔寨',
'France': '法国',
'Thailand': '泰国',
'myanmar': '缅甸',
'Romania': '罗马尼亚',
'Laos': '老挝',
'United Kingdom': '英国',
'Netherlands': '荷兰',
'Philippines': '菲律宾',
'Spain': '西班牙',
'Afghanistan': '阿富汗',
'Korea': '韩国',
'Malaysia': '马来西亚'
}
// 台湾,香港,澳门修正为中国(台湾),中国(香港),中国(澳门)
if (_.includes(common.islandCH, name)) {
return `中国(${name})`
}
// 中文直接显示
if (pattern.test(name)) {
return name
}
// 英文为映射后,再显示
const internationalName = _.get(internationalList, name)
if (internationalName) {
return internationalName
}
return ''
}
},
emphasis: {
textStyle: {
color: '#fff'
}
}
},
itemStyle: {
normal: {
color: 'transparent', // 地图背景色
borderColor: '#516a89', // 省市边界线00fcff 516a89
borderWidth: 1
},
emphasis: {
color: 'rgba(238, 120, 0, 1)' // 悬浮背景
}
},
// 单独对台湾、澳门、香港做了强调标记为红色
regions: [
{
name: '台湾',
emphasis: {
itemStyle: {
areaColor: '#DE2910'
}
},
selected: true
},
{
name: '澳门',
emphasis: {
itemStyle: {
areaColor: '#DE2910'
}
},
selected: true
},
{
name: '香港',
emphasis: {
itemStyle: {
areaColor: '#DE2910'
}
},
selected: true
}
]
},
// 统一几大区的配色,高度依赖common对象
visualMap: this.visualMap,
series: [
// 地图配置
{
name: '中国地图',
type: 'map',
mapType: 'china',
geoIndex: 0,
label: {
normal: {
show: true
},
emphasis: {
show: true
}
},
data: common.allProvinceData, // 默认是省级数据
tooltip: {
show: true,
formatter: function (params, ticket, callback) {
const name = _.get(params, 'name')
// 台湾,香港,澳门修正为中国(台湾),中国(香港),中国(澳门)
if (_.includes(common.islandCH, name)) {
return `中国(${name})`
}
return name
}
}
}
]
}
// 合并地图,线条以及散点图数据
option.series = _.concat([],
// 地图
option.series,
// 散点图
this.scatterList,
// 线图
this.linesList
)
// 设置配置项
this.option = option
// 清空当前选中值
this.provinceName = ''
// 绘制图表
this.myChart.setOption(option, true)
// 隐藏loading
this.loadEcharts = false
const second = _.get(this.$route.query, 'second') || 1800
this.mapChartTimer = setTimeout(() => {
if (!this.provinceName) {
this.setMap()
}
}, second * 1000)
},
/**
* 地图下钻
*/
downMap (myChart, option, provinceName) {
// 清理定时器
clearTimeout(this.mapChartTimer)
clearTimeout(this.provinceChartTimer)
if (!this.provinceName) return
// 拉取地级市的相关联的所有数据
// 拼音
const cityPinyin = common.cityToPinyin[provinceName]
if (provinceName !== 'china' && cityPinyin) {
this.tips = ['世界 > 中国', `> ${provinceName}`]
// 获取当前城市和下钻城市的JSON
this.getCityJson(provinceName).then(curGeoJson => {
// 清除图表
this.myChart.clear()
// 设置下钻地图
echarts.registerMap(provinceName, curGeoJson)
// 设置城市数据
const childOption = _.cloneDeep(option)
childOption.series = [_.nth(childOption.series, 0)]
// 城市数据
childOption.series[0].data = common.allCityData
// 获取散点数据
this.getScatterList(provinceName).then(v => {
if (_.size(v)) {
childOption.series = _.concat([],
// 地图
childOption.series,
// 散点图
v
)
}
// 获取航线数据
this.getLinesList(provinceName).then(v => {
if (_.size(v)) {
childOption.series = _.concat([],
// 地图
childOption.series,
// 散点图
v
)
}
// 设置地图展示中心
childOption.geo.center = _.get(
_.first(_.get(curGeoJson, 'features')
), 'properties.cp')
// 设置比例大小
childOption.geo.zoom = 2
// 重置图表
this.resetOption(this.myChart, childOption, provinceName)
// 循环拉取数据
const second = _.get(this.$route.query, 'second') || 1800
this.provinceChartTimer = setTimeout(() => {
if (!this.provinceName) {
clearTimeout(this.provinceChartTimer)
return
}
this.downMap(this.myChart, option, this.provinceName)
}, second * 1000)
})
})
})
}
},
/**
* 重置图表
*/
resetOption (myChart, option, name) {
// 清除图表
this.myChart.clear()
// 设置图表名称
option.geo.map = name
this.$set(this, 'option', option)
this.$forceUpdate()
// 重置图表
this.myChart.setOption(option, true)
},
/**
* 操作全屏
*/
operateScreen () {
// 跳转至外部
this.$router.push({
name: 'chart',
query: {
type: 'map',
position: 'out'
}
})
this.fullScreen = !this.fullScreen
if (this.fullScreen) {
this.enterScreen()
return
}
this.exitFullScreen()
},
/**
* 退出全屏
*/
exitFullScreen () {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.msExitFullscreen) {
document.msExitFullscreen()
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen()
}
},
/**
* 全屏
*/
enterScreen () {
var element = document.documentElement
if (element.requestFullscreen) {
element.requestFullscreen()
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen()
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen()
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen()
}
}
}
}
</script>
<style lang="stylus">
@import "~assets/css/variable"
.wrap-chart
width 100%
height 100%
background-image url('~assets/img/bg.png')
background-position 100% 100%
background-size cover
background-repeat no-repeat
.main-province
canvas:nth-child(2)
display none
.tips
position absolute
flexAlign()
bottom 7%
right 5%
color $white
font-size 14px
margin 20px 0
padding 15px
background-color #232323
border-radius 100px
box-shadow inset 0 5px 10px -5px #191919, 0 1px 0 0 #444
ul
flexAlign()
flex-direction row
li
display flex
border-bottom 3px solid none
box-sizing border-box
&:first-child
margin-right 5px
.full-screen
bottom 10px
margin-bottom 0px
&:hover
cursor pointer
opacity 0.5
.return-world
&:hover
cursor pointer
opacity 0.5
</style>