SOURCE

console 命令行工具 X clear

                    
>
console
const canvas = document.getElementById('canvas')
const lineCanvas = document.createElement('canvas')
const areaCanvas = document.createElement('canvas')

const ctx = canvas.getContext('2d')
const lineCtx = lineCanvas.getContext('2d')
const areaCtx = areaCanvas.getContext('2d')

const canvasW = canvas.width
const canvasH = canvas.height

areaCanvas.width = lineCanvas.width = canvasW
areaCanvas.height = lineCanvas.height = canvasH

// 重新映射 canvas的 (0, 0),映射的结果是让canvas的坐标原点位于 canvas的中心位置
// 主要是为了后续方便绘图
lineCtx.translate(canvasW / 2, canvasH / 2)
ctx.translate(canvasW / 2, canvasH / 2)
areaCtx.translate(canvasW / 2, canvasH / 2)

// 雷达图数据
const baseData = [
    {title:'平均血糖'},
    {title:'高血糖强度'},
    {title:'低血糖强度'},
    {title:'变异系数'},
    {title:'葡萄糖未达标时间'},
]
const mData = [
    { titleList: ['爱心传递至', '3个城市'], score: 3, fullScore: 5 },
    { titleList: ['帮助了8人'], score: 5, fullScore: 10 },
    { titleList: [`收到5感谢`], score: 5, fullScore: 10 },
    { titleList: ['获得', '15人点赞'], score: 15, fullScore: 15 },
    { titleList: [`可赠送10件闲置`], score: 10, fullScore: 20 }
]

// 多边形的边数
const mCount = baseData.length
// 最外层多边形边长
const prismW = 200
// 最外层多边形外接圆半径
const mRadius = prismW / 2 / Math.cos(108 / 2 / 180 * Math.PI)
// 多边形的内角角度
const mAngle = Math.PI * 2 / mCount
// 需要多少个多边形线框
const polygonCount = 1
const sAngle = (90 / mCount) / 180 * Math.PI

// 需要旋转多少度,才能将多边形旋转到底边平行于 X轴,奇多边形才需要,偶多边形不需要旋转
// 主要是为了方便计算坐标
const rotateAngle = mCount % 2 === 0 ? 0 : (sAngle * (mCount % 4 === 3 ? -1 : 1))
lineCtx.rotate(-rotateAngle)
areaCtx.rotate(-rotateAngle)

// 保存最外层多边形各个顶点的坐标
const polygonPoints = []
// 雷达区域顶点坐标
const radarVertex = []
// 多边形线颜色
lineCtx.strokeStyle = '#ccc'
// 雷达区域边线 width
areaCtx.lineWidth = 1

// 绘制多边形,包括多边形对角顶点之间的连线
drawPolygon()
// ctx.drawImage(lineCanvas, -canvasW / 2, -canvasH / 2, canvasW, canvasH)
// 绘制多边形顶点处文案
drawVertexTxt()
// 绘制雷达区域
// drawRadar()

// 绘制中心绿色面积


// 绘制多边形
function drawPolygon() {
    // #region 绘制多边形
    const r = mRadius / polygonCount
    let currentRadius = 0
    for (let i = 0; i < polygonCount; i++) {
        lineCtx.beginPath()
        currentRadius = r * (i + 1)
        for (let j = 0; j < mCount; j++) {
            const x = currentRadius * Math.cos(mAngle * j)
            const y = currentRadius * Math.sin(mAngle * j)
            // 记录最外层多边形各个顶点的坐标
            if (i === polygonCount - 1) {
                polygonPoints.push([x, y])
            }
            j === 0 ? lineCtx.moveTo(x, y) : lineCtx.lineTo(x, y)
        }
        lineCtx.closePath()
        lineCtx.stroke()
    }
    // #endregion

    // #region 绘制多边形对角连线
    for (let i = 0; i < polygonPoints.length; i++) {
        lineCtx.moveTo(0, 0)
        lineCtx.lineTo(polygonPoints[i][0], polygonPoints[i][1])
    }
    // 绘制刻度
    lineCtx.save();
    lineCtx.rotate(90 * Math.PI / 180);
    lineCtx.fillText("-0", 0, -20);
    lineCtx.stroke()
    lineCtx.restore();
    // #endregion
}

// 绘制多边形对角连线
function drawDiagonal() {
    lineCtx.save()
    for (let i = 0; i < polygonPoints.length; i++) {
        lineCtx.moveTo(0, 0)
        lineCtx.lineTo(polygonPoints[i][0], polygonPoints[i][1])
    }
    lineCtx.stroke()
    lineCtx.restore()
}

// 绘制多边形顶点处文案
function drawVertexTxt() {
    lineCtx.font = 'normal normal lighter 16px Arial'
    lineCtx.fillStyle = '#333'
    // 奇数多边形,距离设备顶边最近的点(即最高点的那一点),需要专门设置一下 textAlign
    const topPointIndex = mCount - Math.round(mCount / 4)
    for (let i = 0; i < polygonPoints.length; i++) {
        lineCtx.save()
        lineCtx.translate(polygonPoints[i][0], polygonPoints[i][1])
        lineCtx.rotate(rotateAngle)
        let indentX = 0
        let indentY = 0
        if (i === topPointIndex) {
            // 最高点
            lineCtx.textAlign = 'center'
            indentY = -8
        } else {
            if (polygonPoints[i][0] > 0 && polygonPoints[i][1] >= 0) {
                lineCtx.textAlign = 'start'
                indentX = 10
            } else if (polygonPoints[i][0] < 0) {
                lineCtx.textAlign = 'end'
                indentX = -10
            }
        }
        // 如果是正四边形,则需要单独处理最低点
        if (mCount === 4 && i === 1) {
            lineCtx.textAlign = 'center'
            indentY = 10
        }
        // 开始绘制文案

        lineCtx.fillText(baseData[i].title, indentX, indentY + 0)

        lineCtx.restore()
    }
}

// 绘制雷达区域
function drawRadar() {
    let score = null
    let xList = []
    let yList = []
    // 计算并存储雷达区域顶点坐标
    for (let i = 0; i < mCount; i++) {
        // score不能超过 fullScore
        score = Math.min(baseData[i].score, baseData[i].fullScore)
        xList.push(Math.cos(mAngle * i) * score / baseData[i].fullScore)
        yList.push(Math.sin(mAngle * i) * score / baseData[i].fullScore)
        radarVertex.push([mRadius * xList[i], mRadius * yList[i]])
    }
    // 裁剪选区,比真实的雷达区域大一圈(indentV),这是为了保证完全遮罩
    const indentV = 40
    areaCtx.beginPath()
    for (let i = 0; i < mCount; i++) {
        score = Math.min(baseData[i].score, baseData[i].fullScore)
        const x = (mRadius + indentV) * xList[i]
        const y = (mRadius + indentV) * yList[i]
        i === 0 ? areaCtx.moveTo(x, y) : areaCtx.lineTo(x, y)
    }
    areaCtx.closePath()
    areaCtx.clip()

    const toAngle = 2 * Math.PI
    const canvasMaxSize = Math.max(canvasW, canvasH)
    // 将离屏 canvas上的 雷达图区域画到主 canvas上,用圆来填充,产生视觉上的雷达图逐渐填充的效果
    const ltX = -canvasW / 2
    const ltY = -canvasH / 2
    const rqDraw = currentAngle => {
        ctx.clearRect(ltX, ltY, canvasW, canvasH)
        areaCtx.clearRect(ltX, ltY, canvasW, canvasH)

        // #region 绘制雷达区域
        areaCtx.beginPath()
        for (let i = 0; i < mCount; i++) {
            i === 0
                ? areaCtx.moveTo(radarVertex[i][0], radarVertex[i][1])
                : areaCtx.lineTo(radarVertex[i][0], radarVertex[i][1])
        }
        areaCtx.fillStyle = 'rgba(204,0,0,0.3)'
        areaCtx.strokeStyle = 'red'
        areaCtx.closePath()
        areaCtx.stroke()
        areaCtx.fill()
        // #endregion

        // #region 绘制覆盖雷达区域的遮罩
        areaCtx.save()
        areaCtx.beginPath()
        areaCtx.globalCompositeOperation = 'destination-in'
        areaCtx.moveTo(0, 0)
        areaCtx.arc(0, 0, canvasMaxSize, 0, currentAngle)
        areaCtx.closePath()
        areaCtx.fillStyle = 'blue'
        areaCtx.fill()
        areaCtx.restore()
        // #endregion

        ctx.drawImage(lineCanvas, ltX, ltY)
        ctx.drawImage(areaCanvas, ltX, ltY)
        // 动态雷达图绘制完毕的标识
        if (currentAngle === toAngle) {
            // return drawVertexDot()
        }
        let newAngle = currentAngle + 0.25
        if (newAngle > toAngle) newAngle = toAngle
        // requestAnimationFrame(() => {
        //   rqDraw(newAngle)
        // })
        setTimeout(() => {
            rqDraw(newAngle)
        }, 16)
    }
    rqDraw(0)
}

// 雷达图绘制结束后,在雷达区域的顶点处绘制小圆点
function drawVertexDot() {
    ctx.rotate(-rotateAngle)
    ctx.fillStyle = '#fe5c5b'
    const dotRadius = 4
    const len = radarVertex.length
    // 画点
    const rqDrawDox = currentDotRadius => {
        for (let i = 0; i < len; i++) {
            ctx.beginPath()
            ctx.arc(radarVertex[i][0], radarVertex[i][1], currentDotRadius, 0, 2 * Math.PI)
            ctx.fill()
        }
        if (currentDotRadius < dotRadius) {
            requestAnimationFrame(() => {
                rqDrawDox(currentDotRadius + 0.5)
            })
        }
    }
    rqDrawDox(1)
}
<canvas id="canvas" width="600" height="500"></canvas>