js 可视化大屏-路径-箭头动画之echarts lines 使
先上效果图
image之前在工作中需要给可视化大屏写些动画效果,其中就有上图展示的多段路径效果,写的时候也踩了些坑,避免大家后续工作中遇到相似功能不好下手,这里分享给小伙伴们。
组件使用如下,可以看到,主要就是在背景图上写的动画:
实现原理:
使用的是echarts的路径图,也是就是type:‘lines’这个系列。可先看下我发布的这个“基础版本”基础-多段线-路径图,考虑到多个页面会使用到当前效果,因此对“基础版本”封装成了一个比较通用的组件,注意echarts版本为4.4.0及其以上。
使echarts 渲染盒子和背景图片(可以是img标签)宽度高度一致,echarts 渲染盒子的层级z-index高于要写动画的图片,以左下角为原点建立坐标系(这样方便测量坐标),整个坐标系宽高(即xAxis和yAxis的最大值)为图片宽高,然后量好各个点的坐标,结合基础-多段线-路径图实现最终动画。
最后对该组件升级以满足更多需求,如页面缩放时,保证点不错位,如使组件支持多段点分别配置单独的颜色、速度,如下:
路径3.gif
下面进行具体实现,分v1.0和v2.0两个版本,不想看的可直接翻到最后查看最终实现代码
路径组件v1.0版本开发要求
1.核心功能就是上面的基础-多段线-路径图
2.因为是在背景图上(也可以是img标签,只要保证图片和组件宽高一致即可)写一层箭头运动的动画,就要考虑到图片拉伸问题,图片拉伸需要保证动画始终在正确位置上,不会错位。
3.使用组件时要方便,配置点位要简单。
路径组件1.0版本-代码如下:
<template>
<div class="chart-box" :id="id"></div>
</template>
<script>
export default {
name: 'linesChartAnimate',
props: {
id: {
type: String,
default: 'ChartBox'
},
imgWH: {
type: Object,
default(){
return {
width: 882, // 当前这张图是 882*602的图
height: 602
}
}
},
dotsArr: { // 运动点集合
type: Array,
default(){
return [
[ // 这个括号里代表的一组数据的运动,即从点[205, 275]运动到点[263, 275]
[205, 275],
[263, 275],
],
[ // 这组点里有四个点
[206, 267],
[284, 267],
[284, 413],
[295, 413],
],
]
}
},
speed: { // 转速
type: Number,
default: 7
}
},
data () {
return {
myChart: '',
// 注意:因为图片在现实的时候可能会拉伸,所以设置actualWH和imgWH两个变量
actualWH: {
width: 0,
height: 0
}
}
},
mounted () {
this.actualWH = { // 渲染盒子的大小
width: this.$el.clientWidth,
height: this.$el.clientHeight
}
this.myChart = this.$echarts.init(document.getElementById(this.id))
this.draw()
},
methods: {
getLines(){
return {
type: 'lines',
coordinateSystem: 'cartesian2d',
// symbol:'arrow',
zlevel: 1,
symbol: ['none', 'none'],
polyline: true,
silent: true,
effect: {
symbol: 'arrow',
show: true,
period: this.speed, // 箭头指向速度,值越小速度越快
trailLength: 0.01, // 特效尾迹长度[0,1]值越大,尾迹越长重
symbolSize: 5, // 图标大小
},
lineStyle: {
width: 1,
normal: {
opacity: 0,
curveness: 0.4, // 曲线的弯曲程度
color: '#3be3ff'
}
}
}
},
getOption () {
// 点合集-在图片上一个一个量的,注意以渲染盒子左下角为原点,点取值方法:以图片左下角为原点,量几个线段点的(x,y)
let dotsArr = this.dotsArr
// 点的处理-量图上距离转换为在渲染盒子中的距离 start
dotsArr.map(item => {
item.map(sub => {
sub[0] = (this.actualWH.width / this.imgWH.width) * sub[0] // x值
sub[1] = (this.actualWH.height / this.imgWH.height) * sub[1] // y值
})
})
// 点的处理-量图上距离转换为在渲染盒子中的距离 end
// 散点图和lines绘制 start
let scatterData = []
let linesData = [] // 默认路径图点的路径
let seriesLines = [] // 路径图
dotsArr.map(item => {
scatterData = scatterData.concat(item) // 散点图data
linesData.push({
coords: item
})
})
// 默认路径图
linesData && linesData.length && seriesLines.push({
...this.getLines(),
data: linesData
})
// 散点图和lines绘制 end
let option = {
backgroundColor: 'transparent',
xAxis: {
// type: 'category',
type: 'value',
show: false,
min: 0,
max: this.actualWH.width,
axisLine: {
lineStyle: {
color: 'red'
}
},
splitLine: {
lineStyle: {
color: 'red'
}
}
// data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value',
show: false,
min: 0,
max: this.actualWH.height,
axisLine: {
lineStyle: {
color: 'red'
}
},
splitLine: {
lineStyle: {
color: 'red'
}
}
// type: 'category'
},
grid: {
left: '0%',
right: '0%',
top: '0%',
bottom: '0%',
containLabel: false
},
series: [
{
zlevel: 2,
symbolSize: 0,
data: scatterData,
type: 'scatter'
},
...seriesLines
]
};
return option
},
// 绘制图表
draw () {
this.myChart.clear()
this.resetChartData()
},
// 刷新数据
resetChartData () {
this.myChart.setOption(this.getOption(), true)
}
},
}
</script>
<style scoped>
.chart-box {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
</style>
注意上面两个变量:imgWH和actualWH,imgWH是在测量点坐标时的宽高,actualWH是指页面渲染时的实际宽高,初始时在mounted 中获取。
mounted () {
this.actualWH = { // 渲染盒子的大小
width: this.$el.clientWidth,
height: this.$el.clientHeight
}
this.myChart = this.$echarts.init(document.getElementById(this.id))
},
在渲染图形前先将点位坐标根据比例换算为实际坐标
image.png
结合下面的option配置,到这里最终实现了不同大小图片在初始时动画能准确的定位
image.png
不知道小伙伴们看懂没,这里总结下这步操作:
首先渲染图表的盒子和背景图(可以是img)大小完全一致,然后配置echarts的option的x轴和y轴分别盒子的宽高,注意x,y轴的类型都为"value",然后grid配置上下左右都为0,再设置containLabel:false排除坐标轴的影响,这就实现了在图片上建立坐标系的完美对齐。
在测量点位的时候,无论是哪个宽高量的点(量点的时候也是左下角开始) ,比如下面这个点的坐标就是 [305,76],我量的时候是按照背景图1000 * 280(这就是imgWH的值)的大小量的,但页面实际渲染时盒子的大小实际是800 * 188(这就是actualWH获取到的值),直接使用点[305,76]肯定是不行的,因此需要按等比缩放计算出现在的值也就是[800 / 1000 * 305,188 / 280 * 75],这才是现在的实际点位。
可以看到代码中配置echarts的option里有scatter这个系列,按理说这部分代码完全是多余的,但是实践测试,必须要有这项配置lines才能跑得起来,而且scatter至少要有一个点。其他代码没什么说的,看代码也能看懂,至此路径图简陋版v1.0开发完毕。
路径组件v2.0版本升级
1.在1.0版本上加上了页面resize事件,页面resize则echarts resize
2.1.0版本配置的颜色、运动速度等是通用的,这里扩展数据配置项,以支持对单条路径的配置,比如:箭头颜色、运行速度等
这里只贴部分关键代码,完整代码请移步页面底部
解决问题1,data中定义timer,然后定义如下方法:
image.png在页面初始时调用
image.png
离开页面时销毁
image.png
然后优化交互
image.png
至此,问题1解决,到这里按住ctr+鼠标滚轮缩放页面时,可实现适配。
解决问题2:
数据更改,向下兼容,第一项为Object时可配置当前这组点的表现行为
image.png
image.png
核心实现,针对配置项单独生成一个series,这里小伙伴可能有疑问: 不能在一个series的lines中实现吗,为什么要每次单独配置一段路径动画都得push一个lines?答案是:不能,因为effect项只能针对每个lines。
image.png
至此问题二得到解决。
最后,我的项目是vue开发的,封装的vue组件-最终实现2.0版本-代码如下,可直接使用。若是react或者其他方式开发的,可参考代码自行开发。
<!--
路径图组件,针对图片上点需要有路径动画的情况
若图片有变化:
1.修改 imgWH 的宽高为最新图片的宽高
2.重新在原图上量出点合集并赋值给dotsArr
-->
<template>
<div class="chart-box" :id="id" v-show="!this.timer"></div>
</template>
<script>
// const merge = require('webpack-merge');
export default {
name: 'linesChartAnimate',
props: {
id: {
type: String,
default: 'ChartBox'
},
imgWH: {
type: Object,
default(){
return {
width: 882, // 当前这张图是 882*602的图
height: 602
}
}
},
dotsArr: {
type: Array,
default(){
return [
// eg: [
// [140,338], // 点运动起点 -- [x,y]
// [202,338], // 点运动终点
// ]
// 左上点合集
[
{ // 第一项可为对象,是当前这组点的配置
color: 'red', // 颜色
symbol:'rect', // 类型-'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'
speed: 3 // 运动时间
},
[140, 338],
[202, 338],
[202, 329],
],
[
[141, 227],
[160, 227],
[196, 100],
[202, 100],
[202, 107],
],
// 上中点
[
[205, 275],
[263, 275],
],
[
[206, 267],
[284, 267],
[284, 413],
[295, 413],
],
[
[208, 257],
[605, 257],
[605, 262],
],
[
[486, 272],
[582, 272],
[582, 307],
],
[
[563, 486],
[582, 486],
[582, 440],
],
// 底部点合集
[
[113, 123],
[113, 59],
[625, 59],
],
[
[677, 59],
[727, 59],
[727, 67],
[813, 67],
]
]
}
},
speed: { // 速度
type: Number,
default: 7
}
},
data () {
return {
myChart: '',
// 注意:因为图片在现实的时候可能会拉伸,所以设置actualWH和imgWH两个变量
actualWH: {
width: 0,
height: 0
},
timer: null
}
},
mounted () {
this.actualWH = { // 渲染盒子的大小
width: this.$el.clientWidth,
height: this.$el.clientHeight
}
this.myChart = this.$echarts.init(document.getElementById(this.id))
this.draw()
this.eventListener(true)
},
methods: {
getLines(){
return {
type: 'lines',
coordinateSystem: 'cartesian2d',
// symbol:'arrow',
zlevel: 1,
symbol: ['none', 'none'],
polyline: true,
silent: true,
effect: {
symbol: 'arrow',
show: true,
period: this.speed, // 箭头指向速度,值越小速度越快
trailLength: 0.01, // 特效尾迹长度[0,1]值越大,尾迹越长重
symbolSize: 5, // 图标大小
},
lineStyle: {
width: 1,
normal: {
opacity: 0,
curveness: 0.4, // 曲线的弯曲程度
color: '#3be3ff'
}
},
}
},
getOption () {
// 点合集-在图片上一个一个量的,注意以渲染盒子左下角为原点,点取值方法:以图片左下角为原点,量几个线段点的(x,y)
let dotsArr = this.dotsArr
// 点的处理-量图上距离转换为在渲染盒子中的距离 start
dotsArr.map(item => {
item.map(sub => {
if (Object.prototype.toString.call(sub) !== '[object Object]') { // item可能配置了当前这组点的运动时间
sub[0] = (this.actualWH.width / this.imgWH.width) * sub[0] // x值
sub[1] = (this.actualWH.height / this.imgWH.height) * sub[1] // y值
}
})
})
// 点的处理-量图上距离转换为在渲染盒子中的距离 end
// 散点图和lines绘制 start
let scatterData = []
let linesData = [] // 默认路径图点的路径
let seriesLines = [] // 路径图
dotsArr.map(item => {
if (Object.prototype.toString.call(item[0]) === '[object Object]') { // 单独配置路径
let cArr = item.slice(1)
if (!cArr.length) return // 无数据跳过
scatterData = scatterData.concat(cArr) // 散点图data
let opt = {
...this.getLines(),
zlevel: 2,
data: [{
coords: cArr
}]
}
// 配置
item[0]['symbol'] && (opt.effect.symbol = item[0]['symbol'])
item[0]['speed'] && (opt.effect.period = item[0]['speed'])
item[0]['color'] && (opt.lineStyle.normal.color = item[0]['color'])
// 可以更改成下面这种-传入配置项
// opt = merge(opt,item[0])
seriesLines.push(opt)
} else { // 使用默认路径配置
scatterData = scatterData.concat(item) // 散点图data
linesData.push({
coords: item
})
}
})
// 默认路径图
linesData && linesData.length && seriesLines.push({
...this.getLines(),
data: linesData
})
// 散点图和lines绘制 end
let option = {
backgroundColor: 'transparent',
xAxis: {
type: 'value',
show: false,
min: 0,
max: this.actualWH.width,
axisLine: {
lineStyle: {
color: 'red'
}
},
splitLine: {
lineStyle: {
color: 'red'
}
}
},
yAxis: {
type: 'value',
show: false,
min: 0,
max: this.actualWH.height,
axisLine: {
lineStyle: {
color: 'red'
}
},
splitLine: {
lineStyle: {
color: 'red'
}
}
// type: 'category'
},
grid: {
left: '0%',
right: '0%',
top: '0%',
bottom: '0%',
containLabel: false
},
series: [
// 多段点
{
zlevel: 2,
symbolSize: 0,
data: scatterData,
type: 'scatter'
},
...seriesLines
]
};
return option
},
// 绘制图表
draw () {
this.myChart.clear()
this.resetChartData()
},
// 刷新数据
resetChartData () {
this.myChart.setOption(this.getOption(), true)
},
// 。。。。。 resize 相关优化 start 。。。。。。
clearTimer(){
this.timer && clearTimeout(this.timer)
this.timer = null
},
eventListener(bool){
if (!bool) { // 销毁
window.removeEventListener('resize', this._eventHandle)
this.clearTimer()
} else {
window.addEventListener('resize', this._eventHandle, false)
}
},
// 优化-添加resize
_eventHandle(){
this.clearTimer()
this.timer = setTimeout(() => {
this.clearTimer();
this.$nextTick(() => {
this.myChart && this.myChart.resize()
})
}, 500)
},
// 。。。。。 resize 相关优化 end 。。。。。。
},
beforeDestroy () {
this.myChart && this.myChart.dispose()
this.eventListener() // 销毁
}
}
</script>
<style scoped>
.chart-box {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
</style>
写在最后
总算写完了,这是我的第一篇博客,但不会是最后一篇,如果对你有帮助的话请留个关注,谢谢啦。