canvas学习瀑布图组件(vue2)

101 阅读3分钟
  • 框架: vue2
  • 通信: websocket
  • 数据格式:

成品

image.png

// 瀑布图封装

<template>
  <div ref="Heatmap" class="chart" />
</template>

<script>
import { chartColor } from '@/views/common/js/colorDefine'
export default {
  name: 'Heatmap',
  props: {
    option: {
      type: Object,
      default: () => { }
    }
  },
  data() {
    return {
      config: { // 值域等配置项
        y_domain: [-40, 100], // y轴值域  [min,max]
        y_Initialize: [-20, 80], // y轴初始化显示值域 [startValue, endValue]
      },
      grid: { left: 30, right: 25, top: 25, bottom: 18, width: 0, height: 0 },
      xAxis: { name: 'MHz' },

      chartTimer: null, // 延时器
      renderData: { xAxis: [], yAxis: [], series: {}}, // 数据存储
      mousePosition: { x: '', y: '' }, // 鼠标位置存储
      elementBlock: { b_w: '', b_h: '' }, // 元素块宽高
      eventType: '', // 事件类型
      chartName: '', // chart图名称
      canRender: true, 
    }
  },
  mounted() {
    this.creatChart()
  },
  activated() {
    if (this.getChart()) {
      this.resize()
      this.echartsResize()
    }
    this.canRender = true
  },
  deactivated() {
    this.getChart() && this.resize('remove')
    this.canRender = false
  },
  beforeDestroy() {
    this.getChart() && this.resize('remove')
    this.canRender = false
  },
  methods: {
    // 创建chart 并插入dom
    creatChart() {
      var obj = this.option
      if (obj.config) this.config = Object.assign(this.config, obj.config)
      if (obj.grid) this.grid = Object.assign(this.grid, obj.grid)
      if (obj.xAxis) this.xAxis = Object.assign(this.xAxis, obj.xAxis)
      if (obj.chartName) this.chartName = obj.chartName

      var myCanvas = document.createElement('canvas')
      this.getDom().appendChild(myCanvas)

      this.initChart()
      // 添加tooltip
      this.createTooltip()
      // 窗口监听
      this.resize()
      // 添加监听
      this.addMonitor()

      this.$emit('chartFun', this.$CONST_MI_COMMAND.MI_echarts_created, this)
    },

    // 创建chart 仅渲染grid框
    initChart() {
      var myCanvas = this.getChart()

      var ctx = myCanvas.getContext('2d')
      var canvasBox = this.getDom()
      var s = canvasBox.getBoundingClientRect()

      this.grid.width = parseInt(s.width) - 5
      this.grid.height = parseInt(s.height) - 6

      const { left, right, top, bottom, width, height } = this.grid

      myCanvas.width = width
      myCanvas.height = height
      ctx.fillStyle = chartColor.COLOR_BACK_GROUND
      ctx.fillRect(0, 0, width, height)
      ctx.clearRect(0, 0, width, height)

      // 创建grid
      ctx.beginPath()
      ctx.strokeStyle = chartColor.COLOR_GRID
      ctx.moveTo(left, top)
      ctx.lineTo(width - right, top)
      ctx.lineTo(width - right, height - bottom)
      ctx.lineTo(left, height - bottom)
      ctx.lineTo(left, top)
      ctx.closePath()
      ctx.stroke()

      this.createVisualMap()
      this.addChartAttr()

      // 清空数据
      this.renderData = { xAxis: [], yAxis: [], series: {}}
      // this.syncShowX();
      this.renderChart(JSON.parse(JSON.stringify(this.renderData)))
    },
    // 创建视觉映射组件
    createVisualMap() {
      const min = this.config.y_Initialize[0]
      const max = this.config.y_Initialize[1]
      var myCanvas = this.getChart()
      var ctx = myCanvas.getContext('2d')
      const { left, right, top, bottom, width, height } = this.grid
      ctx.clearRect(width - right + 7, top, 18, height - bottom - top)
      var grd = ctx.createLinearGradient(width - right + 7, top, width - right + 7, height - bottom)
      grd.addColorStop(0, this.blockColor(max))
      grd.addColorStop(0.5, this.blockColor((max + min) / 2))
      grd.addColorStop(1, this.blockColor(min))
      ctx.fillStyle = grd
      ctx.fillRect(width - right + 7, top, 18, height - bottom - top)

      ctx.font = '9px sans-serif'
      ctx.fillStyle = chartColor.COLOR_CURSOR_TEXT
      ctx.fillText(min, computedLeft(min), height - bottom - 3)
      ctx.fillText(max, computedLeft(max), top + 10)

      function computedLeft(val) {
        const a = String(val).length
        return width - right + (a === 3 ? 8 : a === 2 ? 10 : 14)
      }
    },
    // 根据配置初始化其他属性
    addChartAttr() {
      var ctx = this.getChart().getContext('2d')
      const { left, right, top, bottom, width, height } = this.grid
      // 上箭头
      ctx.beginPath()
      ctx.strokeStyle = chartColor.COLOR_CURSOR_TEXT
      ctx.moveTo(width - right + 16, top - 4)
      ctx.lineTo(width - right + 16, top - 16)
      ctx.lineTo(width - right + 10, top - 10)
      ctx.lineTo(width - right + 16, top - 20)
      ctx.lineTo(width - right + 22, top - 10)
      ctx.lineTo(width - right + 16, top - 16)
      ctx.closePath()
      ctx.stroke()
      ctx.fill()

      // 下箭头
      ctx.beginPath()
      ctx.moveTo(width - right + 16, height - bottom + 4)
      ctx.lineTo(width - right + 16, height - bottom + 16)
      ctx.lineTo(width - right + 10, height - bottom + 10)
      ctx.lineTo(width - right + 16, height - bottom + 20)
      ctx.lineTo(width - right + 22, height - bottom + 10)
      ctx.lineTo(width - right + 16, height - bottom + 16)
      ctx.closePath()
      ctx.stroke()
      ctx.fill()

      // 放大
      ctx.beginPath()
      ctx.arc(width - right - 34, top - 14, 6, 0, 2 * Math.PI)
      ctx.moveTo(width - right - 37, top - 14)
      ctx.lineTo(width - right - 31, top - 14)
      ctx.moveTo(width - right - 34, top - 11)
      ctx.lineTo(width - right - 34, top - 17)
      ctx.moveTo(width - right - 29, top - 9)
      ctx.lineTo(width - right - 25, top - 4)
      ctx.closePath()
      ctx.stroke()

      // 缩小
      ctx.beginPath()
      ctx.arc(width - right - 10, top - 14, 6, 0, 2 * Math.PI)
      ctx.moveTo(width - right - 13, top - 14)
      ctx.lineTo(width - right - 7, top - 14)
      ctx.moveTo(width - right - 5, top - 9)
      ctx.lineTo(width - right - 1, top - 4)
      ctx.closePath()
      ctx.stroke()

      // chart图名称
      ctx.font = '12px sans-serif'
      ctx.fillStyle = chartColor.COLOR_CHARTNAME
      ctx.fillText(this.chartName, width * 0.484, height)
    },
    // 渲染xy轴
    renderXY(type) {
      const { xAxis, yAxis, min, max } = this.renderData
      if (!xAxis.length || !xAxis.length) return
      var x1 = xAxis[0]
      var x2 = xAxis[xAxis.length - 1]
      var y1 = yAxis[0]
      var y2 = yAxis[99]
      const { left, right, top, bottom, width, height } = this.grid
      // 计算每一小块的宽高、包含边界 1px => + 1
      const b_w = (width - left - right) / xAxis.length
      const b_h = (height - top - bottom) / 100
      this.elementBlock = { b_w, b_h }

      var ctx = this.getChart().getContext('2d')

      if (y1) y1 = y1.split(':')[2]
      if (y2) y2 = y2.split(':')[2]

      // 清除原文本
      ctx.clearRect(0, top - 3, left, height - top - bottom + 10)
      ctx.clearRect(left, height - bottom + 5, width - left - right, 18)

      ctx.font = '9px sans-serif'
      ctx.fillStyle = chartColor.COLOR_SCALE_TEXT

      ctx.fillText(min || '', left, height - bottom + 14)
      ctx.fillText(max || '', width - right - String(max).length * 5, height - bottom + 14)
      ctx.fillText(y1 || '', 2, top + 8)
      ctx.fillText(y2 || '', 2, height - bottom + 2)

      // 保存空数据时canvas状态
      ctx.save()
    },
    // 渲染数据
    renderChart(data) {
      if (!this.canRender) return
      data.series = Object.values(data.series).reverse()
      this.renderData = data

      var myCanvas = this.getChart()
      if (!myCanvas) return
      var ctx = myCanvas.getContext('2d')
      // 返回渲染前状态 => 清除上一次渲染
      ctx.restore()
      this.renderXY()

      const { left, top } = this.grid
      const { b_w, b_h } = this.elementBlock

      this.renderData.series.map((item, index) => {
        item.map(($item, $index) => {
          const x = $index * b_w + left
          const y = index * b_h + top
          if ($item != null) {
            ctx.fillStyle = this.blockColor($item)
            ctx.fillRect(x, y, b_w, b_h)
          }
        })
      })
      this.showTooltip()
    },
    // 添加 tooltip
    createTooltip() {
      const { left, right, top, bottom, width, height } = this.grid
      var dom = this.getDom()
      // hLine vLine  横竖线条
      var hLine = dom.querySelector('.hLine')
      var vLine = dom.querySelector('.vLine')
      var tooltip = dom.querySelector('.tooltip')
      if (!hLine) hLine = document.createElement('div')
      if (!vLine) vLine = document.createElement('div')
      if (!tooltip) tooltip = document.createElement('div')

      hLine.className = 'hLine'
      vLine.className = 'vLine'
      tooltip.className = 'tooltip'

      const commonStyle = {
        position: 'absolute',
        display: 'none',
        'z-index': '30',
        'pointer-events': 'none',
      }
      const hLineStyle = {
        ...commonStyle,
        background: chartColor.COLOR_TOOLTIP_LINE,
        width: width - left - right + 'px',
        height: '1px',
        left: left + 'px',
        top: 0
      }
      const vLineStyle = {
        ...commonStyle,
        background: chartColor.COLOR_TOOLTIP_LINE,
        width: '1px',
        height: height - top - bottom + 'px',
        left: 0,
        top: top + 'px'
      }
      const toolbipStyle = {
        ...commonStyle,
        transition: 'left 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s, top 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s',
        'background-color': 'rgba(50, 50, 50, 0.7)',
        'border-radius': '4px',
        color: chartColor.COLOR_CURSOR_TEXT,
        font: '14px / 21px "Microsoft YaHei"',
        padding: '5px',
        left: '100px',
        top: '100px',
        'min-width': '120px',
        'min-height': '50px',
      }

      for (const key in hLineStyle) {
        hLine.style[key] = hLineStyle[key]
      }
      for (const key in vLineStyle) {
        vLine.style[key] = vLineStyle[key]
      }
      for (const key in toolbipStyle) {
        tooltip.style[key] = toolbipStyle[key]
      }

      tooltip.innerHTML = `
      <div class="y_val"></div>
      <div class="x_val"></div>
      <div class="chart_val"</div>
    `
      dom.appendChild(hLine)
      dom.appendChild(vLine)
      dom.appendChild(tooltip)
    },
    // 窗口大小更改 重新计算vline hline tooltip位置
    computeTooltip() {
      const { left, right, top, bottom, width, height } = this.grid
      var dom = this.getDom()
      var hLine = dom.querySelector('.hLine')
      var vLine = dom.querySelector('.vLine')
      hLine.style.width = width - left - right + 'px'
      vLine.style.height = height - top - bottom + 'px'
    },
    // 添加监听
    addMonitor() {
      var dom = this.getDom()

      dom.onmouseenter = () => {
        dom.onmousemove = this.domMousemoveEvent
        dom.onclick = (e) => {
          if (this.eventType) this.changeVisualMap()
        }

        dom.onmouseleave = () => {
          dom.onmousemove = ''
          dom.onmouseleave = ''
          dom.onclick = ''
          this.mousePosition = { x: '', y: '' }
        }
      }
    },
    domMousemoveEvent(e) {
      var dom = this.getDom()
      this.regionEvent(e)
      dom.style.cursor = this.eventType ? 'pointer' : 'default'
      if (!this.renderData.xAxis.length) return
      const x = e.offsetX
      const y = e.offsetY
      this.mousePosition = { x, y }
      // 超出边界不计算
      var hLine = dom.querySelector('.hLine')
      var vLine = dom.querySelector('.vLine')
      var position = this.computeMousePosition()
      this.$emit('chartFun', this.$CONST_MI_COMMAND.MI_echarts_recordLeft, position)
      if (!this.computeMousePosition()) {
        hLine.style.display = 'none'
        vLine.style.display = 'none'
        this.showTooltip()
        return
      }

      hLine.style.display = 'block'
      hLine.style.top = y + 'px'
      vLine.style.display = 'block'
      vLine.style.left = x + 'px'
      this.showTooltip()
    },
    // 计算滑动区域是否属于事件区域
    regionEvent(e) {
      const x = e.offsetX
      const y = e.offsetY
      const { left, right, top, bottom, width, height } = this.grid
      // up: 上箭头 down:下箭头  enlarge:放大 narrow:缩小 wh:点击区域大小
      const wh = 18
      const region = {
        up: { l: width - right + 7, r: width - right + 7 + wh, t: top - 22, b: top - 22 + wh },
        down: { l: width - right + 7, r: width - right + 7 + wh, t: height - bottom + 4, b: height - bottom + 4 + wh },
        enlarge: { l: width - right - wh - wh - 6, r: width - right - wh - 6, t: top - 22, b: top - 22 + wh },
        narrow: { l: width - right - wh, r: width - right, t: top - 22, b: top - 22 + wh },
      }
      let eventType = ''
      for (const key in region) {
        const { l, r, t, b } = region[key]
        if (x >= l && x <= r && y >= t && y <= b) eventType = key
      }
      this.eventType = eventType
    },
    // 操作视觉映射组件
    changeVisualMap() {
      const min = this.config.y_domain[0]
      const max = this.config.y_domain[1]
      let show_min = this.config.y_Initialize[0]
      let show_max = this.config.y_Initialize[1]
      const show_length = show_max - show_min
      switch (this.eventType) {
        case 'up':
          show_max += 10
          if (show_max > max) show_max = max
          show_min = show_max - show_length
          break
        case 'down':
          show_min -= 10
          if (show_min < min) show_min = min
          show_max = show_min + show_length
          break
        case 'enlarge':
          if (show_length > 10) {
            show_max -= 10
            show_min += 10
            if (show_min >= show_max) {
              show_max = show_min
              show_min = show_max - 10
            }
          }
          break
        case 'narrow':
          show_max += 10
          show_min -= 10
          if (show_max > max) show_max = max
          if (show_min < min) show_min = min
          break
      }
      if (show_min !== this.config.y_Initialize[0] || show_max !== this.config.y_Initialize[1]) {
        this.$set(this.config, 'y_Initialize', [show_min, show_max])
        this.createVisualMap()
        this.$nextTick(() => {
          this.$emit('chartFun', this.$CONST_MI_COMMAND.MI_fall_computeVisual)
        })
      }
    },
    // 实时显示tooltip内容
    showTooltip() {
      var tooltip = this.getDom().querySelector('.tooltip')
      if (!tooltip) return
      const { width, height } = this.grid
      const { x, y } = this.mousePosition

      var position = this.computeMousePosition()
      if (!position) return tooltip.style.display = 'none'
      const { x_index, y_index } = position

      var y_dom = tooltip.querySelector('.y_val')
      var x_dom = tooltip.querySelector('.x_val')
      var chart_dom = tooltip.querySelector('.chart_val')

      const x_val = this.renderData.xAxis[x_index]
      const y_val = this.renderData.yAxis[y_index]
      var chart_val = ''
      if (this.renderData.series[y_index]) chart_val = this.renderData.series[y_index][x_index]

      tooltip.style.display = 'block'
      tooltip.style.top = ((y + 70) > height ? (y - 70) : y) + 'px'
      tooltip.style.left = ((x + 140) > width ? (x - 140) : (x + 20)) + 'px'

      y_dom.innerText = `时间:${y_val || ''}`
      x_dom.innerText = `频率:${x_val}MHz`
      chart_dom.innerText = `电平:${chart_val || ''}`
    },

    // 同步显示tooltip
    syncShowX(e) {
      var dom = this.getDom()
      var vLine = dom.querySelector('.vLine')
      var hLine = dom.querySelector('.hLine')
      var tooltip = dom.querySelector('.tooltip')
      if (!vLine || !hLine || !tooltip) return
      hLine.style.display = 'none'
      tooltip.style.display = 'none'
      if (!this.renderData.xAxis.length || !e.msgdata) return vLine.style.display = 'none'

      const x = this.renderData.xAxis.findIndex(item => item === e.msgdata.x)
      if (x === -1) return

      vLine.style.display = 'block'
      vLine.style.left = this.elementBlock.b_w * (x + 0.5) + this.grid.left + 'px'
    },

    // 计算鼠标位置
    computeMousePosition() {
      const { xAxis, yAxis } = this.renderData
      if (!xAxis.length || !xAxis.length) return false
      const { x, y } = this.mousePosition
      const { b_w, b_h } = this.elementBlock
      const { left, right, top, bottom, width, height } = this.grid

      if (!x || !y || x < left || x >= width - right || y < top || y > height - bottom) return false
      const x_index = Math.floor((x - left) / b_w)
      const y_index = Math.floor((y - top) / b_h)
      const x_val = xAxis[x_index]
      const y_val = xAxis[y_index]
      return { x_index, y_index, x_val, y_val }
    },

    // 视窗监听
    resize(type) {
      type ? window.removeEventListener('resize', this.echartsResize) : window.addEventListener('resize', this.echartsResize)
    },

    echartsResize() {
      if (this.getChart()) {
        if (this.chartTimer) {
          clearTimeout(this.chartTimer)
          this.chartTimer = ''
        }
        this.chartTimer = setTimeout(() => {
          this.initChart()
          this.renderXY()
          this.computeTooltip()
          this.chartTimer = ''
        }, 300)
      }
    },

    // 根据val转化为color
    blockColor(val) {
      const c_min = parseInt(chartColor.COLOR_VISUAL_START.replace('#', ''), 16)
      const c_mid = parseInt(chartColor.COLOR_VISUAL_MIDDLE.replace('#', ''), 16)
      const c_max = parseInt(chartColor.COLOR_VISUAL_END.replace('#', ''), 16)

      const min = this.config.y_Initialize[0]
      const mid = (this.config.y_Initialize[1] + this.config.y_Initialize[0]) / 2
      const max = this.config.y_Initialize[1]
      var color = ''

      if (val > max || val < min) return chartColor.COLOR_BACK_GROUND

      if (val >= mid) {
        color = parseInt(((val - mid) / (max - mid)) * (c_max - c_mid) + c_mid).toString(16)
      } else {
        color = parseInt(((val - min) / (mid - min)) * (mid - c_min) + c_min).toString(16)
      }

      let str = '#'
      for (let i = 0; i < 6 - color.length; i++) {
        str += '0'
      }
      str += color
      return str
    },
    // 获取echart实例
    getChart() {
      const dom = this.getDom()
      if (!dom) return ''
      return dom.querySelector('canvas')
    },
    // 获取dom
    getDom() {
      return this.$refs.Heatmap
    }
  },
}
</script>

<style>

</style>

瀑布图调用

     <Heatmap v-if="startRendering" :option="option" @chartFun="chartFun" />
     
 data() {
    return {
        myChart:'',
        option: {
            chartName: '',
        },
        chartData: {
            xAxis: [],
            yAxis: [],
            series: {},
            min: '',
            max: ''
        },
        factor: null,// 比例因子  页面大小变化的时候更改  用于重绘瀑布图
    }
  },
  methods: { 
  // 处理soket来的数据  
  setChartData(data){ 
      if (!this.myChart) return
      if (!this.factor || this.factor !== data.msgdata.factor)  this.createCoordsX(data.msgdata.factor)
      
      this.chartData.series[data.time] = data.data
      
      // 取100帧数据  series中最新100条数据进行渲染即可,其他的删除 然后
      // 优化: 可将下面部分放置再新的方法中, 使用定时器进行渲染
      // 调用  this.myChart.renderChart(JSON.parse(JSON.stringify(this.chartData)))
       const keys = Object.keys(this.chartData.series)
          if (keys.length > 100) {
            const surplusKeys = keys.slice(0, -100)
            surplusKeys.map(key => {
              delete this.chartData.series[key]
            })
          }
      this.chartData.yAxis = this.chartData.yAxis.slice(-100)
      this.myChart.renderChart(JSON.parse(JSON.stringify(this.chartData)))
  },
  // 创建坐标集
  createCoords(){ 
      var xAxis = Array
      var min = number / string
      var max = number / string
      this.$set(this.chartData, 'xAxis', xAxis)
      this.$set(this.chartData, 'yAxis', [])
      this.$set(this.chartData, 'min', min)
      this.$set(this.chartData, 'max', max)
  },
  chartFun(type, data) {
      switch (type) {
        case this.$CONST_MI_COMMAND.MI_echarts_created:
              this.myChart = data
          break
        case this.$CONST_MI_COMMAND.MI_fall_computeVisual:
              xxx
          break
        case this.$CONST_MI_COMMAND.MI_echarts_recordLeft:
             xxxx
          break
      }
    },
  }