js 可視化大屏-路徑-箭頭動(dòng)畫之echarts lines 使用第一篇

先上效果圖

image

之前在工作中需要給可視化大屏寫些動(dòng)畫效果,其中就有上圖展示的多段路徑效果,寫的時(shí)候也踩了些坑,避免大家后續(xù)工作中遇到相似功能不好下手,這里分享給小伙伴們。

組件使用如下,可以看到,主要就是在背景圖上寫的動(dòng)畫:

image.png

實(shí)現(xiàn)原理:

使用的是echarts的路徑圖,也是就是type:‘lines’這個(gè)系列??上瓤聪挛野l(fā)布的這個(gè)“基礎(chǔ)版本”基礎(chǔ)-多段線-路徑圖,考慮到多個(gè)頁面會(huì)使用到當(dāng)前效果,因此對(duì)“基礎(chǔ)版本”封裝成了一個(gè)比較通用的組件,注意echarts版本為4.4.0及其以上。

使echarts 渲染盒子和背景圖片(可以是img標(biāo)簽)寬度高度一致,echarts 渲染盒子的層級(jí)z-index高于要寫動(dòng)畫的圖片,以左下角為原點(diǎn)建立坐標(biāo)系(這樣方便測(cè)量坐標(biāo)),整個(gè)坐標(biāo)系寬高(即xAxis和yAxis的最大值)為圖片寬高,然后量好各個(gè)點(diǎn)的坐標(biāo),結(jié)合基礎(chǔ)-多段線-路徑圖實(shí)現(xiàn)最終動(dòng)畫。

image.png

最后對(duì)該組件升級(jí)以滿足更多需求,如頁面縮放時(shí),保證點(diǎn)不錯(cuò)位,如使組件支持多段點(diǎn)分別配置單獨(dú)的顏色、速度,如下:
路徑3.gif

下面進(jìn)行具體實(shí)現(xiàn),分v1.0和v2.0兩個(gè)版本,不想看的可直接翻到最后查看最終實(shí)現(xiàn)代碼

路徑組件v1.0版本開發(fā)要求

1.核心功能就是上面的基礎(chǔ)-多段線-路徑圖
2.因?yàn)槭窃诒尘皥D上(也可以是img標(biāo)簽,只要保證圖片和組件寬高一致即可)寫一層箭頭運(yùn)動(dòng)的動(dòng)畫,就要考慮到圖片拉伸問題,圖片拉伸需要保證動(dòng)畫始終在正確位置上,不會(huì)錯(cuò)位。
3.使用組件時(shí)要方便,配置點(diǎn)位要簡(jiǎn)單。

路徑組件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, // 當(dāng)前這張圖是 882*602的圖
            height: 602
          }
        }
      },
      dotsArr: { // 運(yùn)動(dòng)點(diǎn)集合
        type: Array,
        default(){
          return [
            [ // 這個(gè)括號(hào)里代表的一組數(shù)據(jù)的運(yùn)動(dòng),即從點(diǎn)[205, 275]運(yùn)動(dòng)到點(diǎn)[263, 275]
              [205, 275],
              [263, 275],
            ],
            [ // 這組點(diǎn)里有四個(gè)點(diǎn)
              [206, 267],
              [284, 267],
              [284, 413],
              [295, 413],
            ],
          ]
        }
      },
      speed: { // 轉(zhuǎn)速
        type: Number,
        default: 7
      }
    },
    data () {
      return {
        myChart: '',
        // 注意:因?yàn)閳D片在現(xiàn)實(shí)的時(shí)候可能會(huì)拉伸,所以設(shè)置actualWH和imgWH兩個(gè)變量
        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, // 圖標(biāo)大小
          },
          lineStyle: {
            width: 1,
            normal: {
              opacity: 0,
              curveness: 0.4, // 曲線的彎曲程度
              color: '#3be3ff'
            }
          }
        }
      },
      getOption () {
        // 點(diǎn)合集-在圖片上一個(gè)一個(gè)量的,注意以渲染盒子左下角為原點(diǎn),點(diǎn)取值方法:以圖片左下角為原點(diǎn),量幾個(gè)線段點(diǎn)的(x,y)
        let dotsArr = this.dotsArr

        // 點(diǎn)的處理-量圖上距離轉(zhuǎn)換為在渲染盒子中的距離 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值
          })
        })
        // 點(diǎn)的處理-量圖上距離轉(zhuǎn)換為在渲染盒子中的距離 end

        // 散點(diǎn)圖和lines繪制 start
        let scatterData = []
        let linesData = [] // 默認(rèn)路徑圖點(diǎn)的路徑
        let seriesLines = [] // 路徑圖
        dotsArr.map(item => {
          scatterData = scatterData.concat(item) // 散點(diǎn)圖data
          linesData.push({
            coords: item
          })
        })

        // 默認(rèn)路徑圖
        linesData && linesData.length && seriesLines.push({
          ...this.getLines(),
          data: linesData
        })
        // 散點(diǎn)圖和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()
      },
      // 刷新數(shù)據(jù)
      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>

注意上面兩個(gè)變量:imgWH和actualWH,imgWH是在測(cè)量點(diǎn)坐標(biāo)時(shí)的寬高,actualWH是指頁面渲染時(shí)的實(shí)際寬高,初始時(shí)在mounted 中獲取。

    mounted () {
      this.actualWH = { // 渲染盒子的大小
        width: this.$el.clientWidth,
        height: this.$el.clientHeight
      }
      this.myChart = this.$echarts.init(document.getElementById(this.id))
    },

在渲染圖形前先將點(diǎn)位坐標(biāo)根據(jù)比例換算為實(shí)際坐標(biāo)


image.png

結(jié)合下面的option配置,到這里最終實(shí)現(xiàn)了不同大小圖片在初始時(shí)動(dòng)畫能準(zhǔn)確的定位


image.png

不知道小伙伴們看懂沒,這里總結(jié)下這步操作:

首先渲染圖表的盒子和背景圖(可以是img)大小完全一致,然后配置echarts的option的x軸和y軸分別盒子的寬高,注意x,y軸的類型都為"value",然后grid配置上下左右都為0,再設(shè)置containLabel:false排除坐標(biāo)軸的影響,這就實(shí)現(xiàn)了在圖片上建立坐標(biāo)系的完美對(duì)齊。

在測(cè)量點(diǎn)位的時(shí)候,無論是哪個(gè)寬高量的點(diǎn)(量點(diǎn)的時(shí)候也是左下角開始) ,比如下面這個(gè)點(diǎn)的坐標(biāo)就是 [305,76],我量的時(shí)候是按照背景圖1000 * 280(這就是imgWH的值)的大小量的,但頁面實(shí)際渲染時(shí)盒子的大小實(shí)際是800 * 188(這就是actualWH獲取到的值),直接使用點(diǎn)[305,76]肯定是不行的,因此需要按等比縮放計(jì)算出現(xiàn)在的值也就是[800 / 1000 * 305,188 / 280 * 75],這才是現(xiàn)在的實(shí)際點(diǎn)位。

image.png

路徑4.gif

可以看到代碼中配置echarts的option里有scatter這個(gè)系列,按理說這部分代碼完全是多余的,但是實(shí)踐測(cè)試,必須要有這項(xiàng)配置lines才能跑得起來,而且scatter至少要有一個(gè)點(diǎn)。其他代碼沒什么說的,看代碼也能看懂,至此路徑圖簡(jiǎn)陋版v1.0開發(fā)完畢。

路徑組件v2.0版本升級(jí)

1.在1.0版本上加上了頁面resize事件,頁面resize則echarts resize
2.1.0版本配置的顏色、運(yùn)動(dòng)速度等是通用的,這里擴(kuò)展數(shù)據(jù)配置項(xiàng),以支持對(duì)單條路徑的配置,比如:箭頭顏色、運(yùn)行速度等

這里只貼部分關(guān)鍵代碼,完整代碼請(qǐng)移步頁面底部

解決問題1,data中定義timer,然后定義如下方法:

image.png

在頁面初始時(shí)調(diào)用


image.png

離開頁面時(shí)銷毀


image.png

然后優(yōu)化交互
image.png

至此,問題1解決,到這里按住ctr+鼠標(biāo)滾輪縮放頁面時(shí),可實(shí)現(xiàn)適配。

解決問題2:

數(shù)據(jù)更改,向下兼容,第一項(xiàng)為Object時(shí)可配置當(dāng)前這組點(diǎn)的表現(xiàn)行為


image.png

image.png

核心實(shí)現(xiàn),針對(duì)配置項(xiàng)單獨(dú)生成一個(gè)series,這里小伙伴可能有疑問: 不能在一個(gè)series的lines中實(shí)現(xiàn)嗎,為什么要每次單獨(dú)配置一段路徑動(dòng)畫都得push一個(gè)lines?答案是:不能,因?yàn)閑ffect項(xiàng)只能針對(duì)每個(gè)lines。


image.png

至此問題二得到解決。

最后,我的項(xiàng)目是vue開發(fā)的,封裝的vue組件-最終實(shí)現(xiàn)2.0版本-代碼如下,可直接使用。若是react或者其他方式開發(fā)的,可參考代碼自行開發(fā)。

<!--
路徑圖組件,針對(duì)圖片上點(diǎn)需要有路徑動(dòng)畫的情況
若圖片有變化:
   1.修改 imgWH 的寬高為最新圖片的寬高
   2.重新在原圖上量出點(diǎn)合集并賦值給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, // 當(dāng)前這張圖是 882*602的圖
            height: 602
          }
        }
      },
      dotsArr: {
        type: Array,
        default(){
          return [
//  eg:           [
//                  [140,338], // 點(diǎn)運(yùn)動(dòng)起點(diǎn) -- [x,y]
//                  [202,338], // 點(diǎn)運(yùn)動(dòng)終點(diǎn)
//                ]
            // 左上點(diǎn)合集
            [
              { // 第一項(xiàng)可為對(duì)象,是當(dāng)前這組點(diǎn)的配置
                color: 'red', // 顏色
                symbol:'rect', // 類型-'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'
                speed: 3 // 運(yùn)動(dòng)時(shí)間
              },
              [140, 338],
              [202, 338],
              [202, 329],
            ],
            [
              [141, 227],
              [160, 227],
              [196, 100],
              [202, 100],
              [202, 107],
            ],

            // 上中點(diǎn)
            [
              [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],
            ],

            // 底部點(diǎn)合集
            [
              [113, 123],
              [113, 59],
              [625, 59],
            ],
            [
              [677, 59],
              [727, 59],
              [727, 67],
              [813, 67],
            ]

          ]
        }
      },
      speed: { // 速度
        type: Number,
        default: 7
      }
    },
    data () {
      return {
        myChart: '',
        // 注意:因?yàn)閳D片在現(xiàn)實(shí)的時(shí)候可能會(huì)拉伸,所以設(shè)置actualWH和imgWH兩個(gè)變量
        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, // 圖標(biāo)大小
          },
          lineStyle: {
            width: 1,
            normal: {
              opacity: 0,
              curveness: 0.4, // 曲線的彎曲程度
              color: '#3be3ff'
            }
          },
        }
      },
      getOption () {
        // 點(diǎn)合集-在圖片上一個(gè)一個(gè)量的,注意以渲染盒子左下角為原點(diǎn),點(diǎn)取值方法:以圖片左下角為原點(diǎn),量幾個(gè)線段點(diǎn)的(x,y)
        let dotsArr = this.dotsArr

        // 點(diǎn)的處理-量圖上距離轉(zhuǎn)換為在渲染盒子中的距離 start
        dotsArr.map(item => {
          item.map(sub => {
            if (Object.prototype.toString.call(sub) !== '[object Object]') { // item可能配置了當(dāng)前這組點(diǎn)的運(yùn)動(dòng)時(shí)間
              sub[0] = (this.actualWH.width / this.imgWH.width) * sub[0] // x值
              sub[1] = (this.actualWH.height / this.imgWH.height) * sub[1] // y值
            }
          })
        })
        // 點(diǎn)的處理-量圖上距離轉(zhuǎn)換為在渲染盒子中的距離 end

        // 散點(diǎn)圖和lines繪制 start
        let scatterData = []
        let linesData = [] // 默認(rèn)路徑圖點(diǎn)的路徑
        let seriesLines = [] // 路徑圖
        dotsArr.map(item => {
          if (Object.prototype.toString.call(item[0]) === '[object Object]') { // 單獨(dú)配置路徑
            let cArr = item.slice(1)
            if (!cArr.length) return // 無數(shù)據(jù)跳過
            scatterData = scatterData.concat(cArr) // 散點(diǎn)圖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'])

            // 可以更改成下面這種-傳入配置項(xiàng)
            // opt = merge(opt,item[0])
            seriesLines.push(opt)
          } else { // 使用默認(rèn)路徑配置
            scatterData = scatterData.concat(item) // 散點(diǎn)圖data
            linesData.push({
              coords: item
            })
          }

        })

        // 默認(rèn)路徑圖
        linesData && linesData.length && seriesLines.push({
          ...this.getLines(),
          data: linesData
        })
        // 散點(diǎn)圖和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: [
            // 多段點(diǎn)
            {
              zlevel: 2,
              symbolSize: 0,
              data: scatterData,
              type: 'scatter'
            },
            ...seriesLines
          ]
        };
        return option
      },
      // 繪制圖表
      draw () {
        this.myChart.clear()
        this.resetChartData()
      },
      // 刷新數(shù)據(jù)
      resetChartData () {
        this.myChart.setOption(this.getOption(), true)
      },

      // 。。。。。 resize 相關(guān)優(yōu)化 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)
        }
      },
      // 優(yōu)化-添加resize
      _eventHandle(){
        this.clearTimer()
        this.timer = setTimeout(() => {
          this.clearTimer();
          this.$nextTick(() => {
            this.myChart && this.myChart.resize()
          })
        }, 500)
      },
      // 。。。。。 resize 相關(guān)優(yōu)化 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>

頁面報(bào)錯(cuò)

如果有的小伙伴引入報(bào)錯(cuò),看下是不是echarts未引入。注意我代碼中是用的this.$echarts,因?yàn)槲业膃charts是全局引入的,如果你要每次都引入可改為let echarts = require("echarts")或者 import * as echarts from 'echarts'; 然后下面的this.$echarts 改為echarts即可。還有echarts版本是否為4.4.0及其以上。

寫在最后

總算寫完了,這是我的第一篇博客,但不會(huì)是最后一篇,如果對(duì)你有幫助的話請(qǐng)留個(gè)關(guān)注,謝謝啦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容