<template> <div class="arc-ring-chart"> <loading v-if="!status" /> <div class="canvas-container"> <canvas :ref="ref" /> </div> <label-line :label="dealAfterLabelLine" :colors="drawColors" /> </div> </template> <script> import colorsMixin from '../../mixins/colorsMixin.js' import canvasMixin from '../../mixins/canvasMixin.js' export default { name: 'ArcRingChart', props: ['data', 'labelLine', 'colors'], mixins: [colorsMixin, canvasMixin], data () { return { ref: `concentric-arc-chart-${(new Date()).getTime()}`, status: false, defaultDecorationCircleRadius: 0.65, defaultArcRadiusArea: [0.3, 0.4], defaultArcWidthArea: [2, 10], defaultLabelFontSize: 12, decorationRadius: '', radianOffset: Math.PI / -2, totalValue: 0, arcRadius: [], arcRadian: [], arcWidth: [], labelLinePoints: [], dealAfterLabelLine: [] } }, watch: { data (d) { const { checkData, draw } = this checkData() && draw() } }, methods: { async init () { const { initCanvas, initColors, checkData, draw } = this await initCanvas() initColors() checkData() && draw() }, checkData () { const { data } = this this.status = false if (!data || !data.series) return false this.status = true return true }, draw () { const { clearCanvas, calcLabelLineData, drawDecorationCircle } = this clearCanvas() calcLabelLineData() drawDecorationCircle() const { calcArcRadius, calcArcRadian, calcArcWidth, drawArc } = this calcArcRadius() calcArcRadian() calcArcWidth() drawArc() const { calcLableLinePoints, drawLabelLine, drawLabelText } = this calcLableLinePoints() drawLabelLine() drawLabelText() }, calcLabelLineData () { const { labelLine, deepClone, data: { series } } = this if (!labelLine) return const dealAfterLabelLine = this.dealAfterLabelLine = deepClone(labelLine) if (labelLine.labels === 'inherit') dealAfterLabelLine.labels = series.map(({ title }) => title) }, drawDecorationCircle () { const { ctx, data: { decorationCircleRadius }, defaultDecorationCircleRadius, centerPos } = this const radius = this.decorationRadius = Math.min(...centerPos) * (decorationCircleRadius || defaultDecorationCircleRadius) ctx.beginPath() ctx.strokeStyle = 'rgba(250, 250, 250, 0.2)' ctx.lineWidth = 4 ctx.arc(...centerPos, radius, 0, Math.PI * 2) ctx.stroke() ctx.beginPath() ctx.lineWidth = 1 ctx.arc(...centerPos, radius - 7, 0, Math.PI * 2) ctx.closePath() ctx.stroke() }, calcArcRadius () { const { data: { series, arcRadiusArea }, defaultArcRadiusArea, centerPos, randomExtend } = this const fullRadius = Math.min(...centerPos) const currentArcRaidusArea = arcRadiusArea || defaultArcRadiusArea const maxRadius = fullRadius * Math.max(...currentArcRaidusArea) const minRadius = fullRadius * Math.min(...currentArcRaidusArea) this.arcRadius = series.map(t => randomExtend(minRadius, maxRadius)) }, calcArcRadian () { const { data: { series }, multipleSum, radianOffset } = this const valueSum = this.totalValue = multipleSum(...series.map(({ value }) => value)) let radian = radianOffset const fullRadian = Math.PI * 2 const avgRadian = fullRadian / series.length this.arcRadian = series.map(({ value }) => { const valueRadian = valueSum === 0 ? avgRadian : value / valueSum * fullRadian return [radian, (radian += valueRadian)] }) }, calcArcWidth () { const { data: { series, arcWidthArea }, defaultArcWidthArea, randomExtend } = this const currentArea = arcWidthArea || defaultArcWidthArea const maxWidth = Math.max(...currentArea) const minWidth = Math.min(...currentArea) this.arcWidth = series.map(t => randomExtend(minWidth, maxWidth)) }, drawArc () { const { ctx, arcRadius, arcRadian, arcWidth, drawColors, centerPos } = this const colorNum = drawColors.length arcRadius.forEach((radius, i) => { ctx.beginPath() ctx.arc(...centerPos, radius, ...arcRadian[i]) ctx.strokeStyle = drawColors[i % colorNum] ctx.lineWidth = arcWidth[i] ctx.stroke() }) }, calcLableLinePoints () { const { arcRadian, arcRadius, centerPos: [x, y], totalValue } = this const { canvas: { getCircleRadianPoint }, data: { series } } = this let [leftlabelLineNum, rightLabelLineNum] = [0, 0] const arcMiddlePoints = arcRadian.map((radian, i) => { const middleRadian = (radian[1] - radian[0]) / 2 + radian[0] const point = getCircleRadianPoint(x, y, arcRadius[i], middleRadian) point[0] > x && (series[i].value || !totalValue) && rightLabelLineNum++ point[0] <= x && (series[i].value || !totalValue) && leftlabelLineNum++ return point }) const { getYPos, decorationRadius } = this const labelLineYArea = [y - decorationRadius + 10, y + decorationRadius - 10] const leftYPos = getYPos(labelLineYArea, leftlabelLineNum) const rightYPos = getYPos(labelLineYArea, rightLabelLineNum) const offsetX = decorationRadius + 10 const leftlabelLineEndX = x - offsetX const rightLableLineEndX = x + offsetX const maxRadius = Math.max(...arcRadius) const leftNearRadiusX = x - maxRadius - 8 const rightNearRadiusX = x + maxRadius + 8 this.labelLinePoints = arcMiddlePoints.map(([px, py], i) => { if (!series[i].value && totalValue) return [false, false, false, false] if (px > x) { const yPos = rightYPos.shift() return [ [px, py], [rightNearRadiusX, py], [rightLableLineEndX - 10, yPos], [rightLableLineEndX, yPos] ] } else { const yPos = leftYPos.pop() return [ [px, py], [leftNearRadiusX, py], [leftlabelLineEndX + 10, yPos], [leftlabelLineEndX, yPos] ] } }) }, getYPos (area, num) { let gap = 0 const minus = area[1] - area[0] if (num === 1) { return [area[0] + minus / 2] } else if (num === 2) { const offset = minus * 0.1 return [area[0] + offset, area[1] - offset] } else { gap = minus / (num - 1) return new Array(num).fill(0).map((t, i) => area[0] + i * gap) } }, drawLabelLine () { const { ctx, labelLinePoints, canvas: { drawPolyline }, drawColors } = this const colorNum = drawColors.length labelLinePoints.forEach((polyline, i) => polyline[0] && drawPolyline(ctx, polyline, 2, drawColors[i % colorNum], false, [10, 0], true)) }, drawLabelText () { const { ctx, labelLinePoints, data: { series, labelFontSize, fixed }, totalValue, defaultLabelFontSize, centerPos: [x] } = this ctx.font = `${labelFontSize || defaultLabelFontSize}px Arial` ctx.fillStyle = '#fff' let totalPercent = 0 const dataLast = series.length - 1 series.forEach(({ value, title }, i) => { if (!value && totalValue) return let currentPercent = (value / totalValue * 100).toFixed(fixed || 1) i === dataLast && (currentPercent = (100 - totalPercent).toFixed(fixed || 1)) !totalValue && (currentPercent = 0) const textPos = labelLinePoints[i][2] const isLeft = textPos[0] < x ctx.textAlign = isLeft ? 'end' : 'start' ctx.textBaseline = 'bottom' ctx.fillText(`${currentPercent}%`, ...textPos) ctx.textBaseline = 'top' ctx.fillText(title, ...textPos) totalPercent += Number(currentPercent) }) } }, mounted () { const { init } = this init() } } </script> <style lang="less"> .arc-ring-chart { position: relative; display: flex; flex-direction: column; color: #fff; .canvas-container { position: relative; flex: 1; } canvas { width: 100%; height: 100%; } } </style>