2018-12-05 18:31:36 +08:00
|
|
|
<template>
|
|
|
|
<div class="ring-chart">
|
2018-12-06 18:53:31 +08:00
|
|
|
<canvas :ref="ref" />
|
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
<loading v-if="!data" />
|
2018-12-06 18:53:31 +08:00
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
<template v-else>
|
|
|
|
<div class="center-info" v-if="data.active">
|
|
|
|
<div class="percent-show">{{percent}}</div>
|
|
|
|
<div class="current-label" :ref="labelRef">{{data.data[activeIndex].title}}</div>
|
|
|
|
</div>
|
2018-12-06 18:53:31 +08:00
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
<div class="label-line">
|
|
|
|
<div class="label-container">
|
2018-12-06 18:53:31 +08:00
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
<div class="label" v-for="(label, index) in data.data" :key="label.title">
|
|
|
|
<div :style="`background-color: ${data.color[index % data.data.length]}`" />
|
|
|
|
<div>{{ label.title }}</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
2018-12-06 18:53:31 +08:00
|
|
|
</div>
|
2018-12-07 15:50:25 +08:00
|
|
|
</template>
|
2018-12-05 18:31:36 +08:00
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
export default {
|
2018-12-06 18:53:31 +08:00
|
|
|
name: 'RingChart',
|
|
|
|
props: ['data'],
|
|
|
|
data () {
|
|
|
|
return {
|
|
|
|
ref: `ring-chart-${(new Date()).getTime()}`,
|
2018-12-07 15:50:25 +08:00
|
|
|
canvasDom: '',
|
2018-12-06 18:53:31 +08:00
|
|
|
canvasWH: [0, 0],
|
|
|
|
ctx: '',
|
|
|
|
|
|
|
|
labelRef: `label-ref-${(new Date()).getTime()}`,
|
|
|
|
labelDom: '',
|
|
|
|
|
|
|
|
ringRadius: '',
|
|
|
|
ringOriginPos: [0, 0],
|
|
|
|
ringLineWidth: '',
|
|
|
|
maxRingWidthP: 1.15,
|
|
|
|
|
|
|
|
activeIndex: 1,
|
|
|
|
activePercent: 1,
|
|
|
|
activeAddStatus: true,
|
|
|
|
|
|
|
|
arcData: [],
|
|
|
|
radiusData: [],
|
2018-12-07 15:50:25 +08:00
|
|
|
aroundLineData: [],
|
|
|
|
aroundTextData: [],
|
|
|
|
aroundTextFont: '13px Arial',
|
2018-12-06 18:53:31 +08:00
|
|
|
|
|
|
|
activeIncrease: 0.005,
|
2018-12-14 11:18:09 +08:00
|
|
|
activeTime: 4500,
|
2018-12-06 18:53:31 +08:00
|
|
|
|
|
|
|
offsetAngle: Math.PI * 0.5 * -1,
|
|
|
|
|
|
|
|
percent: 0,
|
|
|
|
totalValue: 0,
|
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
activeAnimationHandler: '',
|
|
|
|
awaitActiveHandler: ''
|
2018-12-06 18:53:31 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
watch: {
|
|
|
|
data (d) {
|
2018-12-07 15:50:25 +08:00
|
|
|
const { reDraw } = this
|
|
|
|
|
|
|
|
if (!d) return
|
|
|
|
|
|
|
|
reDraw()
|
2018-12-06 18:53:31 +08:00
|
|
|
},
|
|
|
|
activeIndex () {
|
2018-12-07 15:50:25 +08:00
|
|
|
const { doPercentAnimation, doLabelTextAnimation } = this
|
2018-12-06 18:53:31 +08:00
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
doPercentAnimation()
|
|
|
|
|
|
|
|
doLabelTextAnimation()
|
2018-12-06 18:53:31 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
init () {
|
|
|
|
const { $nextTick, initCanvas, calcRingConfig, data, draw } = this
|
|
|
|
|
|
|
|
$nextTick(e => {
|
|
|
|
initCanvas()
|
|
|
|
|
|
|
|
calcRingConfig()
|
|
|
|
|
|
|
|
data && draw()
|
|
|
|
})
|
|
|
|
},
|
|
|
|
initCanvas () {
|
|
|
|
const { $refs, ref, labelRef, canvasWH } = this
|
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
const canvas = this.canvasDom = $refs[ref]
|
2018-12-06 18:53:31 +08:00
|
|
|
|
|
|
|
this.labelDom = $refs[labelRef]
|
|
|
|
|
|
|
|
canvasWH[0] = canvas.clientWidth
|
|
|
|
canvasWH[1] = canvas.clientHeight
|
|
|
|
|
|
|
|
canvas.setAttribute('width', canvasWH[0])
|
|
|
|
canvas.setAttribute('height', canvasWH[1])
|
|
|
|
|
|
|
|
this.ctx = canvas.getContext('2d')
|
|
|
|
},
|
|
|
|
calcRingConfig () {
|
|
|
|
const { canvasWH, ringOriginPos } = this
|
|
|
|
|
|
|
|
ringOriginPos[0] = canvasWH[0] / 2
|
|
|
|
ringOriginPos[1] = (canvasWH[1] - 30) / 2
|
|
|
|
|
|
|
|
const ringRadius = this.ringRadius = Math.min(...canvasWH) * 0.6 / 2
|
|
|
|
|
|
|
|
this.ringLineWidth = ringRadius * 0.3
|
|
|
|
},
|
|
|
|
draw () {
|
2018-12-12 18:48:43 +08:00
|
|
|
const { ctx, canvasWH } = this
|
|
|
|
|
|
|
|
ctx.clearRect(0, 0, ...canvasWH)
|
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
const { caclArcData, data: { active }, drawActive, drwaStatic } = this
|
2018-12-06 18:53:31 +08:00
|
|
|
|
|
|
|
caclArcData()
|
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
active ? drawActive() : drwaStatic()
|
2018-12-06 18:53:31 +08:00
|
|
|
},
|
|
|
|
caclArcData () {
|
2018-12-07 15:50:25 +08:00
|
|
|
const { data: { data } } = this
|
2018-12-06 18:53:31 +08:00
|
|
|
|
|
|
|
const { getTotalValue, offsetAngle } = this
|
|
|
|
|
|
|
|
const totalValue = getTotalValue()
|
|
|
|
|
|
|
|
const full = 2 * Math.PI
|
|
|
|
|
2018-12-12 18:48:43 +08:00
|
|
|
const aveAngle = full / data.length
|
|
|
|
|
2018-12-06 18:53:31 +08:00
|
|
|
let currentPercent = offsetAngle
|
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
this.arcData = []
|
|
|
|
|
2018-12-06 18:53:31 +08:00
|
|
|
data.forEach(({ value }) => {
|
2018-12-12 18:48:43 +08:00
|
|
|
const valueAngle = totalValue === 0 ? aveAngle : value / totalValue * full
|
2018-12-06 18:53:31 +08:00
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
this.arcData.push([
|
2018-12-06 18:53:31 +08:00
|
|
|
currentPercent,
|
2018-12-12 18:48:43 +08:00
|
|
|
currentPercent += valueAngle
|
2018-12-06 18:53:31 +08:00
|
|
|
])
|
|
|
|
})
|
|
|
|
},
|
|
|
|
getTotalValue () {
|
|
|
|
const { data: { data } } = this
|
|
|
|
|
|
|
|
let totalValue = 0
|
|
|
|
|
|
|
|
data.forEach(({ value }) => (totalValue += value))
|
|
|
|
|
|
|
|
this.totalValue = totalValue
|
|
|
|
|
|
|
|
return totalValue
|
|
|
|
},
|
2018-12-07 15:50:25 +08:00
|
|
|
drawActive () {
|
2018-12-06 18:53:31 +08:00
|
|
|
const { ctx, canvasWH } = this
|
|
|
|
|
|
|
|
ctx.clearRect(0, 0, ...canvasWH)
|
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
const { calcRadiusData, drawRing, drawActive } = this
|
2018-12-06 18:53:31 +08:00
|
|
|
|
|
|
|
calcRadiusData()
|
|
|
|
|
|
|
|
drawRing()
|
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
this.activeAnimationHandler = requestAnimationFrame(drawActive)
|
2018-12-06 18:53:31 +08:00
|
|
|
},
|
|
|
|
calcRadiusData () {
|
|
|
|
const { arcData, activeAddStatus, activePercent, activeIncrease, activeIndex } = this
|
|
|
|
|
|
|
|
const radiusData = new Array(arcData.length).fill(1)
|
|
|
|
|
|
|
|
const activeRadius = (activeAddStatus ? this.activePercent += activeIncrease : activePercent)
|
|
|
|
|
|
|
|
radiusData[activeIndex] = activeRadius
|
|
|
|
|
|
|
|
const { maxRingWidthP, ringRadius, awaitActive } = this
|
|
|
|
|
|
|
|
const prevRadius = maxRingWidthP - activeRadius + 1
|
|
|
|
|
|
|
|
const prevIndex = activeIndex - 1
|
|
|
|
|
|
|
|
radiusData[prevIndex < 0 ? arcData.length - 1 : prevIndex] = prevRadius
|
|
|
|
|
|
|
|
this.radiusData = radiusData.map(v => (v * ringRadius))
|
|
|
|
|
|
|
|
if (activeRadius >= maxRingWidthP && activeAddStatus) awaitActive()
|
|
|
|
},
|
|
|
|
awaitActive () {
|
|
|
|
const { activeTime, turnToNextActive } = this
|
|
|
|
|
|
|
|
this.activeAddStatus = false
|
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
this.awaitActiveHandler = setTimeout(turnToNextActive, activeTime)
|
2018-12-06 18:53:31 +08:00
|
|
|
},
|
|
|
|
turnToNextActive () {
|
|
|
|
const { arcData, activeIndex } = this
|
|
|
|
|
|
|
|
this.activePercent = 1
|
|
|
|
|
|
|
|
this.activeIndex = (activeIndex + 1 === arcData.length ? 0 : activeIndex + 1)
|
|
|
|
|
|
|
|
this.activeAddStatus = true
|
|
|
|
},
|
|
|
|
drawRing () {
|
|
|
|
const { arcData, ctx, ringOriginPos, radiusData } = this
|
|
|
|
|
|
|
|
const { ringLineWidth, data: { color } } = this
|
|
|
|
|
|
|
|
const arcNum = arcData.length
|
|
|
|
|
|
|
|
arcData.forEach((arc, i) => {
|
|
|
|
ctx.beginPath()
|
|
|
|
|
|
|
|
ctx.arc(...ringOriginPos, radiusData[i], ...arc)
|
|
|
|
|
|
|
|
ctx.lineWidth = ringLineWidth
|
|
|
|
|
|
|
|
ctx.strokeStyle = color[i % arcNum]
|
|
|
|
|
|
|
|
ctx.stroke()
|
|
|
|
})
|
|
|
|
},
|
2018-12-07 15:50:25 +08:00
|
|
|
doPercentAnimation () {
|
|
|
|
const { totalValue, percent, activeIndex, data: { data }, doPercentAnimation } = this
|
|
|
|
|
2018-12-12 18:48:43 +08:00
|
|
|
if (!totalValue) return
|
|
|
|
|
2018-12-14 11:18:09 +08:00
|
|
|
const currentValue = data[activeIndex].value
|
|
|
|
|
|
|
|
let currentPercent = Math.trunc(currentValue / totalValue * 100)
|
2018-12-12 18:48:43 +08:00
|
|
|
|
|
|
|
currentPercent === 0 && (currentPercent = 1)
|
2018-12-14 11:18:09 +08:00
|
|
|
currentValue === 0 && (currentPercent = 0)
|
2018-12-07 15:50:25 +08:00
|
|
|
|
|
|
|
if (currentPercent === percent) return
|
|
|
|
|
|
|
|
currentPercent > percent ? this.percent++ : this.percent--
|
|
|
|
|
2018-12-14 11:18:09 +08:00
|
|
|
setTimeout(doPercentAnimation, 10)
|
2018-12-07 15:50:25 +08:00
|
|
|
},
|
|
|
|
doLabelTextAnimation () {
|
|
|
|
let { labelDom, $refs, labelRef } = this
|
|
|
|
|
|
|
|
if (!labelDom) labelDom = this.labelDom = $refs[labelRef]
|
|
|
|
|
|
|
|
labelDom.setAttribute('class', 'current-label transform-text')
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
labelDom.setAttribute('class', 'current-label')
|
|
|
|
}, 2000)
|
|
|
|
},
|
|
|
|
drwaStatic () {
|
|
|
|
const { drawStaticRing, calcAroundLineData, drawAroundLine, calcAroundTextData, drawAroundText } = this
|
|
|
|
|
|
|
|
drawStaticRing()
|
|
|
|
|
|
|
|
calcAroundLineData()
|
|
|
|
|
|
|
|
drawAroundLine()
|
|
|
|
|
|
|
|
calcAroundTextData()
|
|
|
|
|
|
|
|
drawAroundText()
|
|
|
|
},
|
|
|
|
drawStaticRing () {
|
|
|
|
const { arcData, ringRadius, drawRing } = this
|
|
|
|
|
|
|
|
this.radiusData = new Array(arcData.length).fill(1).map(v => v * ringRadius)
|
|
|
|
|
|
|
|
drawRing()
|
|
|
|
},
|
|
|
|
calcAroundLineData () {
|
2018-12-12 18:48:43 +08:00
|
|
|
const { arcData, ringRadius, ringLineWidth, ringOriginPos: [x, y], data: { data }, canvas, totalValue } = this
|
2018-12-07 15:50:25 +08:00
|
|
|
|
2018-12-12 18:48:43 +08:00
|
|
|
const { getCircleRadianPoint } = canvas
|
2018-12-07 15:50:25 +08:00
|
|
|
|
|
|
|
const radian = arcData.map(([a, b]) => (a + (b - a) / 2))
|
|
|
|
|
|
|
|
const radius = ringRadius + ringLineWidth / 2
|
|
|
|
|
2018-12-12 18:48:43 +08:00
|
|
|
const aroundLineData = radian.map(r => getCircleRadianPoint(x, y, radius, r))
|
2018-12-07 15:50:25 +08:00
|
|
|
|
|
|
|
const lineLength = 35
|
|
|
|
|
2018-12-12 18:48:43 +08:00
|
|
|
this.aroundLineData = aroundLineData.map(([bx, by], i) => {
|
|
|
|
if (!data[i].value && totalValue) return [false, false]
|
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
const lineEndXPos = (bx > x ? bx + lineLength : bx - lineLength)
|
|
|
|
|
|
|
|
return [
|
|
|
|
[bx, by],
|
|
|
|
[lineEndXPos, by]
|
|
|
|
]
|
|
|
|
})
|
|
|
|
},
|
|
|
|
drawAroundLine () {
|
|
|
|
const { aroundLineData, data: { color }, ctx, canvas: { drawLine } } = this
|
|
|
|
|
|
|
|
const colorNum = color.length
|
|
|
|
|
2018-12-12 18:48:43 +08:00
|
|
|
aroundLineData.forEach(([lineBegin, lineEnd], i) =>
|
|
|
|
lineBegin !== false &&
|
|
|
|
drawLine(ctx, lineBegin, lineEnd, 1, color[i % colorNum]))
|
2018-12-07 15:50:25 +08:00
|
|
|
},
|
|
|
|
calcAroundTextData () {
|
2018-12-14 11:18:09 +08:00
|
|
|
const { data: { data, fixed }, totalValue } = this
|
2018-12-07 15:50:25 +08:00
|
|
|
|
2018-12-12 18:48:43 +08:00
|
|
|
const aroundTextData = this.aroundTextData = []
|
2018-12-07 15:50:25 +08:00
|
|
|
|
2018-12-14 11:18:09 +08:00
|
|
|
if (!totalValue) return data.forEach(({ v, title }, i) => aroundTextData.push([0, title]))
|
|
|
|
|
|
|
|
const dataLast = data.length - 1
|
|
|
|
|
|
|
|
let totalPercent = 0
|
|
|
|
|
2018-12-07 15:50:25 +08:00
|
|
|
data.forEach(({ value, title }, i) => {
|
2018-12-14 11:18:09 +08:00
|
|
|
if (!value) return aroundTextData.push([false, false])
|
2018-12-12 18:48:43 +08:00
|
|
|
|
2018-12-14 11:18:09 +08:00
|
|
|
let percent = Number((value / totalValue * 100).toFixed(fixed || 1))
|
2018-12-07 15:50:25 +08:00
|
|
|
|
2018-12-14 11:18:09 +08:00
|
|
|
percent < 0.1 && (percent = 0.1)
|
2018-12-07 15:50:25 +08:00
|
|
|
|
2018-12-14 11:18:09 +08:00
|
|
|
const currentPercent = (i === dataLast ? 100 - totalPercent : percent).toFixed(fixed || 1)
|
2018-12-07 15:50:25 +08:00
|
|
|
|
2018-12-14 11:18:09 +08:00
|
|
|
aroundTextData.push([currentPercent, title])
|
2018-12-07 15:50:25 +08:00
|
|
|
|
2018-12-14 11:18:09 +08:00
|
|
|
totalPercent += percent
|
2018-12-07 15:50:25 +08:00
|
|
|
})
|
|
|
|
},
|
|
|
|
drawAroundText () {
|
2018-12-12 18:48:43 +08:00
|
|
|
const { ctx, aroundTextData, aroundTextFont, aroundLineData, ringOriginPos: [x] } = this
|
2018-12-07 15:50:25 +08:00
|
|
|
|
|
|
|
ctx.font = aroundTextFont
|
|
|
|
ctx.fillStyle = '#fff'
|
|
|
|
|
2018-12-12 18:48:43 +08:00
|
|
|
aroundTextData.forEach(([percent, title], i) => {
|
|
|
|
if (percent === false) return
|
|
|
|
|
|
|
|
const currentPos = aroundLineData[i][1]
|
|
|
|
|
|
|
|
ctx.textAlign = 'start'
|
|
|
|
|
|
|
|
currentPos[0] < x && (ctx.textAlign = 'end')
|
|
|
|
|
|
|
|
ctx.textBaseline = 'bottom'
|
2018-12-14 11:18:09 +08:00
|
|
|
ctx.fillText(`${percent}%`, ...currentPos)
|
2018-12-12 18:48:43 +08:00
|
|
|
|
|
|
|
ctx.textBaseline = 'top'
|
|
|
|
ctx.fillText(title, ...currentPos)
|
2018-12-07 15:50:25 +08:00
|
|
|
})
|
|
|
|
},
|
|
|
|
reDraw () {
|
|
|
|
const { activeAnimationHandler, draw } = this
|
|
|
|
|
|
|
|
cancelAnimationFrame(activeAnimationHandler)
|
|
|
|
|
|
|
|
draw()
|
|
|
|
}
|
2018-12-06 18:53:31 +08:00
|
|
|
},
|
|
|
|
mounted () {
|
|
|
|
const { init } = this
|
|
|
|
|
|
|
|
init()
|
|
|
|
}
|
2018-12-05 18:31:36 +08:00
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="less">
|
2018-12-06 18:53:31 +08:00
|
|
|
.ring-chart {
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
canvas {
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
}
|
|
|
|
|
|
|
|
.center-info {
|
|
|
|
position: absolute;
|
|
|
|
left: 50%;
|
|
|
|
top: 50%;
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
margin-top: -20px;
|
|
|
|
text-align: center;
|
|
|
|
font-family: "Microsoft Yahei", Arial, sans-serif;
|
2018-12-07 15:50:25 +08:00
|
|
|
max-width: 25%;
|
2018-12-06 18:53:31 +08:00
|
|
|
|
|
|
|
.percent-show {
|
2018-12-11 14:52:12 +08:00
|
|
|
font-size: 28px;
|
2018-12-06 18:53:31 +08:00
|
|
|
|
|
|
|
&::after {
|
|
|
|
content: '%';
|
2018-12-11 14:52:12 +08:00
|
|
|
font-size: 15px;
|
2018-12-06 18:53:31 +08:00
|
|
|
margin-left: 5px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.current-label {
|
2018-12-11 14:52:12 +08:00
|
|
|
font-size: 16px;
|
2018-12-07 15:50:25 +08:00
|
|
|
margin-top: 5%;
|
2018-12-06 18:53:31 +08:00
|
|
|
transform: rotateY(0deg);
|
2018-12-07 15:50:25 +08:00
|
|
|
overflow: hidden;
|
|
|
|
white-space: nowrap;
|
|
|
|
text-overflow: ellipsis;
|
2018-12-06 18:53:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
.transform-text {
|
2018-12-07 15:50:25 +08:00
|
|
|
animation: transform-text 2s linear;
|
2018-12-06 18:53:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
@keyframes transform-text {
|
|
|
|
to {
|
|
|
|
transform: rotateY(360deg);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.label-line {
|
|
|
|
position: absolute;
|
|
|
|
width: 100%;
|
|
|
|
height: 30px;
|
|
|
|
bottom: 0px;
|
2018-12-11 14:52:12 +08:00
|
|
|
font-size: 12px;
|
2018-12-06 18:53:31 +08:00
|
|
|
line-height: 30px;
|
2018-12-11 14:52:12 +08:00
|
|
|
color: rgba(255, 255, 255, 0.6);
|
2018-12-06 18:53:31 +08:00
|
|
|
display: flex;
|
|
|
|
justify-content: space-around;
|
|
|
|
|
|
|
|
.label-container {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: row;
|
2018-12-07 15:50:25 +08:00
|
|
|
flex-wrap: wrap;
|
|
|
|
justify-content: center;
|
2018-12-06 18:53:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
.label {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: row;
|
|
|
|
margin: 0 3px;
|
2018-12-07 15:50:25 +08:00
|
|
|
height: 20px;
|
2018-12-06 18:53:31 +08:00
|
|
|
|
|
|
|
:nth-child(1) {
|
|
|
|
width: 10px;
|
|
|
|
height: 10px;
|
|
|
|
margin-top: 10px;
|
|
|
|
margin-right: 3px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-12-05 18:31:36 +08:00
|
|
|
</style>
|