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>