Canvas 折线图组件封装

背景

前段时间开发快应用,因为快应用的生态不活跃,没有像 ECharts 这么强大的图表库,所以为了实现需求自己用原生的Canvas封装了一个。
直接上代码!!!


Canvas折线图组件代码

<template>
  <div class="wrapper">
		<canvas id="canvas"  show="{{ isHaveData }}" style="width:{{canvasWH.width + 'px'}}; height: {{canvasWH.height + 'px'}};"></canvas>
    <div show="{{ !isHaveData }}" class="no-data">
      <text>暂无数据</text>
    </div>
	</div>
</template>
<script>
import { getCurrentTime, hourToString, formatYAxis, checkYAxisHaveData } from '../../utils/common.js'

export default {
  props: {
    padding: {
      default: { top:10, right: 10, bottom:10, left:40 }
    },
    dataZone: {
      default: { width: 640, height:400 }
    },
    showHourNum: {
      default: 8
    }
  },
	data() {
		return {
      // padding: { top:10, right: 10, bottom:10, left:40 },
      // dataZone: { width: 640, height:400 },
      xAxis: [7, 8, 9, 10, 11, 12, 13, 14, 15],
      yData: [36.5, 36.6, 36.4, 36.8, 36.1, 37.2, 36.8, "", "", "", "", "", "", 36.8, 36.8],
      isHaveData: false,
      canvasWH: {
        width: this.dataZone.width + this.padding.left + this.padding.right + 50,
        height: this.dataZone.height + this.padding.top + this.padding.bottom + 30
      }
		}
  },
  draw(endTime) {
    const canvas = this.$element('canvas')
    const context = canvas.getContext("2d")

    this.clearCanvas(context) // 清空画布
    this.drawBackground(context) // 画背景
    this.drawYAxis(context) // 画 y轴
    this.drawXAxis(context) // 画 X 轴
    this.drawLine(context, endTime) // 画折线
  },
  clearCanvas(ctx) {
    ctx.clearRect(0, 0, this.canvasWH.width, this.canvasWH.height)
  },
  drawBackground(ctx) {
    // 画背景
    const MAX = 45
    const MIN = 25
    const oneDegreeHigh = this.dataZone.height / (MAX-MIN)
    ctx.beginPath();
    let total = 0
    // 45 ~ 43
    const height0 = (45 - 43) * oneDegreeHigh
    ctx.fillStyle = "#F2F5F4"
    ctx.fillRect(this.padding.left, total+this.padding.top, this.dataZone.width, height0);
    total += height0
    // 43 ~ 37
    const height1 = (43 - 37) * oneDegreeHigh
    ctx.fillStyle = "#FFAEAE"
    ctx.fillRect(this.padding.left, total + this.padding.top, this.dataZone.width, height1);
    total += height1
    // 37 ~36
    const height2 = (37 - 36) * oneDegreeHigh
    ctx.fillStyle = "#9CF5CA"
    ctx.fillRect(this.padding.left, total + this.padding.top, this.dataZone.width, height2);
    total += height2
    // 36 ~ 32
    const height3 = (36 - 32) * oneDegreeHigh
    ctx.fillStyle = "#B1E9FF"
    ctx.fillRect(this.padding.left, total + this.padding.top, this.dataZone.width, height3);
    total += height3
    // 32 ~25
    const height4 = (32 - 25) * oneDegreeHigh
    ctx.fillStyle = "#F2F5F4"
    ctx.fillRect(this.padding.left, total + this.padding.top, this.dataZone.width, height4);
  },
  drawYAxis(ctx) {
    // 画 y轴
    const MAX = 45
    const MIN = 25
    const oneDegreeHigh = this.dataZone.height / (MAX-MIN)
    const height0 = (45 - 43) * oneDegreeHigh
    const height1 = (45 - 37) * oneDegreeHigh 
    const height2 = (45 - 36) * oneDegreeHigh
    const height3 = (45 - 32) * oneDegreeHigh
    const height4 = (45 - 25) * oneDegreeHigh
    ctx.beginPath();
    ctx.font = "bold 12px Arial"
    ctx.fillStyle = "#646464"
    ctx.textAlign = "center"
    ctx.textBaseline  = "middle"
    ctx.fillText( '45', 20, this.padding.top) // 实心文字
    ctx.fillText( '43', 20, this.padding.top + height0) // 实心文字
    ctx.fillText( '37', 20, this.padding.top + height1) // 实心文字
    ctx.fillText( '36', 20, this.padding.top + height2) // 实心文字
    ctx.fillText( '32', 20, this.padding.top + height3) // 实心文字
    ctx.fillText( '25', 20, this.padding.top + height4) // 实心文字
  },
  drawXAxis(ctx) {
    const _this = this
    axisLine(ctx) // 设置线
    axisTick(ctx, 8) // 设置刻度
    axisLabel(ctx, 18) // 设置文字
    // X 轴线
    function axisLine(ctx) {
      const start = {x: _this.padding.left - 3, y: _this.padding.top + _this.dataZone.height}
      const end = {x: _this.padding.left + _this.dataZone.width, y: _this.padding.top + _this.dataZone.height}
      ctx.beginPath();
      ctx.moveTo(start.x, start.y)
      ctx.lineWidth="1";
      ctx.strokeStyle="#808080"; // 路径Style
      ctx.lineTo(end.x, end.y)
      ctx.stroke(); // 进行绘制
    }
    // X 轴标记
    function axisTick(ctx, size) {
      const dataLen = _this.xAxis.length
      const interval = _this.dataZone.width / (dataLen - 1)
      const y0 = _this.padding.top + _this.dataZone.height - size / 2
      const y1 = _this.padding.top + _this.dataZone.height + size / 2
      ctx.beginPath();
      for (let i = 0; i < dataLen; i++) {
        ctx.lineWidth="2";
        ctx.strokeStyle="#808080"; // 路径Style
        ctx.moveTo(i*interval + _this.padding.left, y0)
        ctx.lineTo(i*interval + _this.padding.left, y1)
      }
      ctx.stroke(); // 进行绘制
    }
    // X 轴文字
    function axisLabel(ctx, top) {
      const dataLen = _this.xAxis.length
      const interval = _this.dataZone.width / (dataLen - 1)
      const y = _this.padding.top + _this.dataZone.height + top
      ctx.beginPath();
      ctx.font = "bold 12px Arial"
      ctx.fillStyle = "#646464"
      ctx.textAlign = "center"
      for (let i = 0; i < dataLen; i++) {
        const x = i * interval + _this.padding.left
        ctx.fillText( _this.xAxis[i], x, y) // 标记文字
      }
    }
  },
  drawLine(ctx, endTime) {
    const _this = this
    ctx.moveTo(0 + this.padding.left,200);
    // ctx.font = "bold 10px Arial"
    // ctx.fillStyle = "#4B4B4B"
    ctx.lineWidth="2";
    ctx.strokeStyle="#4B4B4B";
    const endMinute = endTime.slice(-4, -2)
    const xArray = calcuX(endMinute)
    let isStroke = false
    this.yData.forEach((val, index)=> {
      if (val === '') {
        isStroke = false
      } else {
        const x = xArray[index]
        const y = calcuY(val)
        if (isStroke) {
          ctx.lineTo(x, y)
          ctx.stroke(); // 进行绘制
        } else {
          ctx.beginPath();
          ctx.moveTo(x, y);
          ctx.arc(x, y, 0.5, 0, 2*Math.PI) // 画点 (x, y, 半径, 起始角度,结束角度)
          ctx.fillStyle="back"
          // ctx.fill()
        }
        isStroke = true
      }
    })

    // 计算 1 个 y轴坐标
    function calcuY(tempre) {
      const yPointInterval = _this.dataZone.height / (45 -25)
      const y = (45 - tempre) * yPointInterval + _this.padding.top
      return parseFloat(y.toFixed(1))
    }
    // 计算 全部 x轴坐标
    function calcuX(endMinute) {
      let xArray = []
      const POINT_TIME = 3 // 3分钟一个点
      const dataLen = _this.xAxis.length
      const hourInterval = _this.dataZone.width / (dataLen-1)
      const xPointInterval = hourInterval / (60 / POINT_TIME)
      const xAxisSize = _this.padding.left + _this.dataZone.width
      const lastPointX = xAxisSize - parseInt((60-endMinute)/3) * xPointInterval
      for(let i = 0; i < _this.yData.length; i++) {
        xArray.unshift(lastPointX - i * xPointInterval)
      }
      return xArray
    }
  },

  createXbyEndTime(endTime) {
    let endHour = endTime.slice(-6, -4)
    let arr = []
    let hour = parseInt(endHour) + 1 // 在当前小时的基础上,多增加 1 小时
    for (let i = 0; i < 24; i++) {
      if (hour < 0) {
        arr.unshift(hourToString(hour + 24))
      } else {
        arr.unshift(hourToString(hour))
      }
      hour--
    }
    return arr
  },
  initChart(endTime, originalYArray) {
    const showDataLength = this.showHourNum * 20 // 一小时20个点
    const endMinute = endTime.slice(-4, -2)
    const offsetPoint = parseInt((60-endMinute)/3 - 1) // 差几个点补全最后一小时
    this.yData = formatYAxis(originalYArray).slice(0 - (showDataLength-offsetPoint))
    this.xAxis = this.createXbyEndTime(endTime).slice(0 - (this.showHourNum+1))
    this.isHaveData = checkYAxisHaveData(this.yData)
    this.draw(endTime)
    console.log(`加载canvas endTime=${endTime} originalYArray=${originalYArray} xAxis=${this.xAxis}`)
  },
}
</script>
<style lang="scss" scoped>
@import './../../assets/styles/style.scss';
.wrapper {
  padding-top: 10px;
}
.box {
	width: 740px;
	height: 450px;
}
.no-data {
  width: 100%;
  height: 500px;
  justify-content: center;
  text {
    text-align: center;
  }
}
</style>

posted @ 2022-03-30 16:03  Better-HTQ  阅读(193)  评论(0编辑  收藏  举报