应要求,需要画一个雷达图展示星座运势,并需要将html转为图片完成分享。
因为是app内嵌h5,引入额外的插件对于加载速度并不友好,所以决定自己用原生js完成,echarts与html2canvas不在考虑范围内。
雷达图
这是做前端以来第一次做比较复杂的绘图,过程踩了不少坑,写个短文,做以记录。
在开始绘图之前,必须要进行构思,这是必不可少的一步。
直接看下面这张图就一目了然:
(又把初中数学学了一遍)
从图上得知,本质上是做圆的内切五边形,利用公式计算坐标。
代码在此,以vue为例:
标签与样式
<template>
<div ref="radarContainer" class="radar-container">
<img :src="source" class="radar-canvas" />
</div>
</template>
<style scoped>
.radar-container {
height: 100%;
position: relative;
overflow: hidden;
}
.radar-canvas {
height: 256px;
width: 256px;
}
</style>
data
data() {
return {
data: [
{ label: '爱情', type: 1, percent: 45 },
{ label: '工作', type: 2, percent: 20.8},
{ label: '综合', type: 3, percent: 30.3 },
{ label: '健康', type: 4, percent: 100 },
{ label: '工作', type: 5, percent: 56 }
],
ctx: null,
source: '',
circlePoint: {} // 记录圆上的点
}
},
mounted
mounted() {
this.$nextTick(() => {
const step = this.data.length // 五个点,设置绘制次数
const w = 256 // 取决于在设计稿中的真实宽度
const ratio = 3
const canvas = document.createElement('CANVAS')
this.ctx = canvas.getContext('2d') // 实例化2dcanvas
canvas.width = w * ratio // 必须给画布设置width height,否则坐标系比例不正确
canvas.height = w * ratio
canvas.style.width = w + 'px'
canvas.style.height = w + 'px'
// 值得注意的是canvas.style.width 与 canvas.width的关系相当于显示器宽度与显示器分辨率的关系
this.ctx.scale(ratio, ratio)
// ratio与scale的使用是为了canvas能渲染更清晰的图像,此处设置了三倍渲染大小
// 相当于屏幕大小不变,分辨率却扩大三倍
const radius = 128 - 12 // 圆的半径 = w / 2,-12是为了画一个不会占满画布的小圆
this.drawRadar({ step, radius, canvas })
})
},
开始绘制
drawRadar(option) {
this.radarBG(option)
this.radarBone(option)
this.radarPoint()
this.radarLine()
this.source = option.canvas.toDataURL('image/png', 0.3) // 将canvas的内容转化为图像
},
绘制背景
radarBG({ step, radius }) {
// 不使圆紧贴在画布边缘,使圆心偏移12像素,以offset值绘制大圆
const offset = radius + 12
// 遍历6次而不是5次,是为了留最外一层做label的绘制
for (let s = 6; s > 0; s--) {
this.ctx.beginPath() // 表示开始绘制
this.ctx.lineWidth = 1 // 线条粗细
this.ctx.setLineDash([1, 2]) // 虚线
for (let i = 0; i < step; i++) {
const rad = ((2 * Math.PI) / step) * i // 弧度
const x = offset + Math.sin(rad) * radius * (s / 6)
const y = offset - Math.cos(rad) * radius * (s / 6)
if (s === 6) {
this.radarLabel(i, x, y) // 绘制标签
} else {
if (s === 5) {
// === 5 时为雷达图最外层的时候,只需在此时记录一次内部的点
// 此处计算处于伞骨上的点坐标
const { type, percent } = this.data[i]
const percentX =
offset +
Math.sin(rad) *
(radius * (percent / 100)) *
(s / 6)
const percentY =
offset -
Math.cos(rad) *
(radius * (percent / 100)) *
(s / 6)
this.circlePoint[type] = {
x: percentX,
y: percentY
}
}
this.ctx.lineTo(x, y) // 绘制线条
}
}
this.ctx.closePath() // 闭合线条
this.ctx.strokeStyle = `black` // 线条样式-颜色设置
this.ctx.stroke() // 线条样式-绘制
}
},
绘制标签
radarLabel(i, x, y) {
const text = this.data[i].label
this.ctx.font = `normal normal 300 14px PingFangSC-Regular`
this.ctx.fillStyle = 'black'
this.ctx.textAlign = 'center'
this.ctx.textBaseline = 'top'
this.ctx.fillText(text, x, y)
},
绘制伞骨
radarBone({ step, radius }) {
const offset = radius + 12
for (let s = 6; s > 4; s--) {
if (s === 5) {
this.ctx.beginPath() // 表示开始绘制
this.ctx.lineWidth = 1 // 线条粗细
this.ctx.setLineDash([1, 2]) // 虚线
for (let i = 0; i < step; i++) {
const rad = ((2 * Math.PI) / step) * i // 弧度
const x = offset + Math.sin(rad) * radius * (s / 6)
const y = offset - Math.cos(rad) * radius * (s / 6)
this.ctx.moveTo(offset, offset)
this.ctx.lineTo(x, y)
}
}
this.ctx.strokeStyle = `black` // 线条样式-颜色设置
this.ctx.stroke() // 线条样式-绘制
}
},
绘制点
radarPoint() {
for (const item of this.data) {
const { type } = item
const { x, y } = this.circlePoint[type]
this.ctx.beginPath() // 表示开始绘制
this.ctx.arc(x, y, 1.2, 0, 2 * Math.PI)
this.ctx.fillStyle = 'red'
this.ctx.fill()
this.ctx.closePath()
}
},
绘制折线
radarLine() {
this.ctx.beginPath() // 表示开始绘制
for (const item of this.data) {
const { type } = item
const { x, y } = this.circlePoint[type]
this.ctx.lineTo(x, y) // 绘制线条
}
this.ctx.closePath() // 闭合
this.ctx.fillStyle = 'rgba(164,153,234,.6)'
this.ctx.fill()
}
雷达图绘制完成
效果:
绘图过程参考了此文:https://blog.csdn.net/github_39673115/article/details/78369409