美文网首页网页前端后台技巧(CSS+HTML)后端砖头前端
js 可视化大屏-路径-箭头动画之echarts lines 使

js 可视化大屏-路径-箭头动画之echarts lines 使

作者: 阿巳交不起水电费 | 来源:发表于2022-01-14 16:57 被阅读0次

    先上效果图

    image

    之前在工作中需要给可视化大屏写些动画效果,其中就有上图展示的多段路径效果,写的时候也踩了些坑,避免大家后续工作中遇到相似功能不好下手,这里分享给小伙伴们。

    组件使用如下,可以看到,主要就是在背景图上写的动画:

    image.png

    实现原理:

    使用的是echarts的路径图,也是就是type:‘lines’这个系列。可先看下我发布的这个“基础版本”基础-多段线-路径图,考虑到多个页面会使用到当前效果,因此对“基础版本”封装成了一个比较通用的组件,注意echarts版本为4.4.0及其以上

    使echarts 渲染盒子和背景图片(可以是img标签)宽度高度一致,echarts 渲染盒子的层级z-index高于要写动画的图片,以左下角为原点建立坐标系(这样方便测量坐标),整个坐标系宽高(即xAxis和yAxis的最大值)为图片宽高,然后量好各个点的坐标,结合基础-多段线-路径图实现最终动画。

    image.png
    最后对该组件升级以满足更多需求,如页面缩放时,保证点不错位,如使组件支持多段点分别配置单独的颜色、速度,如下:
    路径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],这才是现在的实际点位。

    image.png 路径4.gif

    可以看到代码中配置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>
    
    

    写在最后

    总算写完了,这是我的第一篇博客,但不会是最后一篇,如果对你有帮助的话请留个关注,谢谢啦。

    相关文章

      网友评论

        本文标题:js 可视化大屏-路径-箭头动画之echarts lines 使

        本文链接:https://www.haomeiwen.com/subject/pfoncrtx.html