废话不多说,先整目标效果图
- 开发需求:期望上述完成效果图的组件封装
- 数据格式:
// 其中颜色可传可不传,不传的话,按照封装的组件中,随机生成颜色
scores: [60, 60, 50, 20, 10, 32, 88], // 必须的
// 格式1
myOptions: {
radius: 60,
width: 300,
height: 200,
data: [
{ weighting: 20, title: '语文' },
{ weighting: 20, title: '数学' },
{ weighting: 20, title: '英语' },
{ weighting: 10, title: '地理' },
{ weighting: 10, title: '生物' },
{ weighting: 10, title: '历史' },
{ weighting: 10, title: '化学' }
]
},
// 格式2
myOption: {
radius: 60,
width: 300,
height: 200,
data: [
{ weighting: 20, title: '语文', color: '#1de9b6' },
{ weighting: 20, title: '数学', color: '#7c4dff' },
{ weighting: 20, title: '英语', color: '#ffb200' },
{ weighting: 10, title: '地理', color: '#00affe' },
{ weighting: 10, title: '生物', color: '#ff4181' },
{ weighting: 10, title: '历史', color: '#a1a1a1' },
{ weighting: 10, title: '化学', color: '#f2a2a2' }
]
},
- 调用界面
<template>
<div>
<canvas-chart v-model="scores" :options="myOption" ref="chart1"></canvas-chart>
<canvas-chart v-model="scores" :options="myOptions" ref="chart2"></canvas-chart>
</div>
</template>
<script>
import canvasChart from './component/canvasChart.vue'
export default {
components: { canvasChart },
data () {
return {
scores: [60, 60, 50, 20, 10, 32, 88],
myOptions: {
radius: 60,
width: 300,
height: 200,
data: [
{ weighting: 20, title: '语文' },
{ weighting: 20, title: '数学' },
{ weighting: 20, title: '英语' },
{ weighting: 10, title: '地理' },
{ weighting: 10, title: '生物' },
{ weighting: 10, title: '历史' },
{ weighting: 10, title: '化学' }
]
},
myOption: {
radius: 60,
width: 300,
height: 200,
data: [
{ weighting: 20, title: '语文', color: '#1de9b6' },
{ weighting: 20, title: '数学', color: '#7c4dff' },
{ weighting: 20, title: '英语', color: '#ffb200' },
{ weighting: 10, title: '地理', color: '#00affe' },
{ weighting: 10, title: '生物', color: '#ff4181' },
{ weighting: 10, title: '历史', color: '#a1a1a1' },
{ weighting: 10, title: '化学', color: '#f2a2a2' }
]
},
}
},
mounted () {
const random = (max=100,min=0)=>{
return Math.floor(Math.random() * (max - min)) + min
}
setInterval(() => {
this.scores = [random(),random(),random(),random(),random(),random(),random()]
}, 2000)
}
}
</script>
<style></style>
- 封装界面
<template>
<div>
<canvas id="canvasChart" ref="canvasChartRef"></canvas>
<p ref="tooltipsRef" class="tooltips">
<ul v-for="(item, index) in value" :key="index">
<span class="left">
<Icon type="ios-radio-button-on" :style="`color: ${option.data[index].color}`"/>
{{option.data[index].title}}
</span>
<span class="right">
W:{{option.data[index].weighting}}%
S:{{item}}
</span>
</ul>
</p>
</div>
</template>
<script>
const PI2 = Math.PI * 2
export default {
name: 'canvasChart',
model: {
prop: 'value'
},
props: {
options: {
type: Object,
default () {
return {
width: 200,
radius: 60,
height: 150,
data: [{ score: 0, weighting: 10, title: 'FCP', color: '#1de9b6' }]
}
},
},
value: {
type: Array,
},
},
data () {
return {
ctx: null,
path2D: null,
option: {
radius: 60,
width: 200,
height: 150
}
}
},
watch: {
// 这里适用于数据实时刷新的时候
value (val) {
val.forEach((x, i) => {
this.option.data[i].score = x
})
this.update()
}
},
created () {
// 默认值处理
const { data } = Object.assign(this.option, this.options)
data.forEach((item, i) => {
item.score = this.value[i]
if (!item.color) {
item.color = this.randomColor()
}
})
},
mounted () {
const { width, height } = this.option
var canvas = this.$refs.canvasChartRef
if (canvas.getContext) {
this.ctx = canvas.getContext('2d')
canvas.width = width
canvas.height = height
// 把圆心移到图中心
this.ctx.translate(canvas.width / 2, canvas.height / 2)
this.$nextTick(() => {
this.draw()
this.hoverTooltips()
})
} else {
console.log('no supported!')
}
},
methods: {
/**
* 1 内圆 + 权重得分
* 2 绘制占比环
* a. 按照weighting。绘制占比环。(注意起始位置,用于连接处的空白区域)
* b. 按照score。绘制weighting占比区域。(每次的起始位置都是weighting的位置)
* c. 绘制文本
* 3 tooltips
*/
draw () {
const data = this.option.data
// 转化弧度
this.drawAngle(data)
let arcAngle = 0
let titleAngle = 0
let total = 0
data.map((item) => {
// 1
total += (item.score * item.weighting) / 100
// 2 -> a,b
arcAngle = this.drawArc(arcAngle, item)
// 2 -> c
titleAngle = this.drawTitle(titleAngle, item)
})
// 3
this.path2D = this.centerCircle(total)
},
//转换弧度方法
drawAngle (data) {
let sum = 0
for (let item of data) {
sum += item.weighting
}
data.map((v) => {
// 权重weighting占的圆环的百分比 ,即所占弧度
const angle = (v.weighting / sum) * PI2
v.angle = angle
})
return data
},
drawArc (start, item) {
const { ctx, option } = this
ctx.lineWidth = 6
let { score, weighting } = item
// 弧间距
// 连接处的空白区域:weighting值不变,初始值+3开始画,但weighting要-3
let paddingLR = 3
weighting -= paddingLR
let sAngle = start + paddingLR
let eAngle = weighting + sAngle
// 绘制权重,weighting占圆环百分比
ctx.beginPath()
ctx.strokeStyle = item.color + 30
ctx.lineCap = 'round'
ctx.arc(0, 0, option.radius, (sAngle / 100) * PI2, (eAngle / 100) * PI2)
ctx.stroke()
// 绘制成绩,当前score在当前weighting下的百分比
// socre * wei / 100表示当前分数在当前权重的占比
// 绘制时,可sAngle -> sAngle + num, 或者sAngle -> sAngle-(weighting-num)
const num = weighting - (score * weighting) / 100
ctx.beginPath()
ctx.lineCap = 'round'
ctx.strokeStyle = item.color
ctx.arc(0, 0, option.radius, (sAngle / 100) * PI2, ((eAngle - num) / 100) * PI2)
ctx.stroke()
ctx.restore()
ctx.save()
return eAngle
},
drawTitle (start, item) {
/**
* 确定伸出去的线,通过圆心和伸出去的点,确定这个线
* 确定伸出去的点,需要确定伸出去线的长度
* 固定伸出去线的长度
* 计算这个点的坐标
* 需要根据角度(当前扇形的其实弧度+对应弧度的一般)和斜边的长度(半径+伸出去的长度)
* outX = x0 + cos(angle)+ (r+outline)
* outY = x0 + sin(angle)+ (r+outline)
*/
const eAngle = start + item.angle
const outline = 12
const { ctx } = this
ctx.restore()
ctx.strokeStyle = item.color //饼状图伸出去的那条线
//
var x0 = this.option.radius * Math.cos(start + item.angle / 2)
var y0 = this.option.radius * Math.sin(start + item.angle / 2)
// 斜边edge
var edge = this.option.radius + outline
// xEdge = edge * cos(α), yEdge = edge * sin(α),
var xEdge = edge * Math.cos(start + item.angle / 2)
var yEdge = edge * Math.sin(start + item.angle / 2)
ctx.beginPath()
ctx.lineWidth = 2
ctx.moveTo(x0, y0) // 需要将起始点移到直线与圆的交点处
ctx.lineTo(xEdge, yEdge)
ctx.textAlign = 'right' //文字对齐
ctx.font = '12px Arial'
//结束的点坐标和文字的大小有关
var textWidth = this.ctx.measureText(item.title).width
if (xEdge > x0) {
//右
ctx.lineTo(xEdge + textWidth, yEdge)
ctx.textAlign = 'left'
} else {
//左
ctx.lineTo(xEdge - textWidth, yEdge)
ctx.textBaseline = 'bottom'
}
ctx.stroke()
this.ctx.fillText(item.title, xEdge, yEdge)
return eAngle
},
centerCircle (total) {
const { ctx, options } = this
// 记录内圆区域
const path2D = new Path2D()
ctx.beginPath()
ctx.fillStyle = '#CCCCCC'
// ctx.arc(0,0, options.radius*0.9, -Math.PI, Math.PI, false)
// ctx.fill()
path2D.arc(0, 0, options.radius * 0.9, 0, 2 * Math.PI)
ctx.fill(path2D)
ctx.restore()
ctx.save()
// 加权得分
ctx.beginPath()
ctx.font = '38px Arial'
ctx.fillStyle = 'rgb(255, 164, 0)'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(total.toFixed(0), 0, 0)
ctx.restore()
ctx.save()
return path2D
},
hoverTooltips () {
const _self = this
const { ctx, path2D } = _self
const tips = this.$refs.tooltipsRef
ctx.canvas.addEventListener('mousemove', function (e) {
const canvasInfo = ctx.canvas.getBoundingClientRect()
if (ctx.isPointInPath(path2D, e.clientX - canvasInfo.left, e.clientY - canvasInfo.top)) {
tips.style.setProperty('display', 'block', 'important')
tips.style.setProperty('top', e.clientY - 20 + 'px', 'important')
tips.style.setProperty('left', e.clientX + 10 + 'px', 'important')
} else {
tips.style.setProperty('display', 'none', 'important')
}
})
},
// 更新画布
update () {
const {ctx} = this
const canvas = ctx.canvas
canvas.width = this.option.width;
canvas.height = this.option.height;
ctx.translate(canvas.width / 2, canvas.height / 2)
this.draw()
},
randomColor () {//得到随机的颜色值
// var r = Math.floor(Math.random() * 256)
// var g = Math.floor(Math.random() * 256)
// var b = Math.floor(Math.random() * 256)
// return "rgb(" + r + "," + g + "," + b + ")"
return '#' + ('00000' + (Math.random() * 0x1000000 << 0).toString(16)).substr(-6);
}
},
}
</script>
<style lang="less" type="text/less" scoped>
canvas {
// box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
position: relative;
}
.tooltips {
display: none;
position: absolute;
font-size: 14px;
z-index: 2;
background: white;
padding: 10px;
box-shadow: rgb(0 0 0 / 20%) 1px 2px 10px;
transition: all 0.2s;
border-radius: 4px;
.left {
display: inline-block;
width: 60px;
}
.right {
font-size: 12px;
}
}
</style>