164 lines
4.5 KiB
JavaScript
164 lines
4.5 KiB
JavaScript
const COLORS = {
|
|
primary: '#0B2B4B',
|
|
accent: '#FF6B35',
|
|
accentLight: 'rgba(255, 107, 53, 0.25)',
|
|
grid: '#E9EDF2',
|
|
text: '#5E6F8D',
|
|
fill: 'rgba(26, 74, 111, 0.35)',
|
|
line: '#1A4A6F'
|
|
}
|
|
|
|
function setupCanvas(canvas, width, height, dpr) {
|
|
canvas.width = width * dpr
|
|
canvas.height = height * dpr
|
|
const ctx = canvas.getContext('2d')
|
|
ctx.scale(dpr, dpr)
|
|
return ctx
|
|
}
|
|
|
|
/** 绘制雷达图 */
|
|
export function drawRadarChart(canvas, opts = {}) {
|
|
if (!canvas) return
|
|
const {
|
|
width = 280,
|
|
height = 240,
|
|
labels = [],
|
|
values = [],
|
|
dpr = 1
|
|
} = opts
|
|
const ctx = setupCanvas(canvas, width, height, dpr)
|
|
ctx.clearRect(0, 0, width, height)
|
|
|
|
const cx = width / 2
|
|
const cy = height / 2 + 8
|
|
const radius = Math.min(width, height) * 0.32
|
|
const count = labels.length || 6
|
|
const angleStep = (Math.PI * 2) / count
|
|
|
|
for (let level = 1; level <= 4; level += 1) {
|
|
ctx.beginPath()
|
|
const r = (radius * level) / 4
|
|
for (let i = 0; i <= count; i += 1) {
|
|
const angle = -Math.PI / 2 + i * angleStep
|
|
const x = cx + r * Math.cos(angle)
|
|
const y = cy + r * Math.sin(angle)
|
|
if (i === 0) ctx.moveTo(x, y)
|
|
else ctx.lineTo(x, y)
|
|
}
|
|
ctx.strokeStyle = COLORS.grid
|
|
ctx.lineWidth = 1
|
|
ctx.stroke()
|
|
}
|
|
|
|
for (let i = 0; i < count; i += 1) {
|
|
const angle = -Math.PI / 2 + i * angleStep
|
|
ctx.beginPath()
|
|
ctx.moveTo(cx, cy)
|
|
ctx.lineTo(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle))
|
|
ctx.strokeStyle = COLORS.grid
|
|
ctx.stroke()
|
|
}
|
|
|
|
ctx.beginPath()
|
|
values.forEach((val, i) => {
|
|
const ratio = Math.min(1, Math.max(0, val / 100))
|
|
const angle = -Math.PI / 2 + i * angleStep
|
|
const x = cx + radius * ratio * Math.cos(angle)
|
|
const y = cy + radius * ratio * Math.sin(angle)
|
|
if (i === 0) ctx.moveTo(x, y)
|
|
else ctx.lineTo(x, y)
|
|
})
|
|
ctx.closePath()
|
|
ctx.fillStyle = COLORS.fill
|
|
ctx.fill()
|
|
ctx.strokeStyle = COLORS.accent
|
|
ctx.lineWidth = 2
|
|
ctx.stroke()
|
|
|
|
ctx.font = '11px sans-serif'
|
|
ctx.fillStyle = COLORS.text
|
|
ctx.textAlign = 'center'
|
|
labels.forEach((label, i) => {
|
|
const angle = -Math.PI / 2 + i * angleStep
|
|
const x = cx + (radius + 18) * Math.cos(angle)
|
|
const y = cy + (radius + 18) * Math.sin(angle) + 4
|
|
ctx.fillText(label, x, y)
|
|
})
|
|
}
|
|
|
|
/** 绘制折线趋势图 */
|
|
export function drawTrendChart(canvas, opts = {}) {
|
|
if (!canvas) return
|
|
const {
|
|
width = 300,
|
|
height = 160,
|
|
points = [],
|
|
dpr = 1,
|
|
unit = ''
|
|
} = opts
|
|
const ctx = setupCanvas(canvas, width, height, dpr)
|
|
ctx.clearRect(0, 0, width, height)
|
|
|
|
if (!points.length) {
|
|
ctx.fillStyle = COLORS.text
|
|
ctx.font = '13px sans-serif'
|
|
ctx.textAlign = 'center'
|
|
ctx.fillText('暂无趋势数据', width / 2, height / 2)
|
|
return
|
|
}
|
|
|
|
const pad = { top: 16, right: 12, bottom: 28, left: 12 }
|
|
const chartW = width - pad.left - pad.right
|
|
const chartH = height - pad.top - pad.bottom
|
|
const values = points.map((p) => p.value)
|
|
const min = Math.min(...values) * 0.95
|
|
const max = Math.max(...values) * 1.05
|
|
const range = max - min || 1
|
|
|
|
ctx.strokeStyle = COLORS.grid
|
|
ctx.lineWidth = 1
|
|
for (let i = 0; i <= 3; i += 1) {
|
|
const y = pad.top + (chartH * i) / 3
|
|
ctx.beginPath()
|
|
ctx.moveTo(pad.left, y)
|
|
ctx.lineTo(width - pad.right, y)
|
|
ctx.stroke()
|
|
}
|
|
|
|
const coords = points.map((p, i) => ({
|
|
x: pad.left + (chartW * i) / Math.max(1, points.length - 1),
|
|
y: pad.top + chartH - ((p.value - min) / range) * chartH
|
|
}))
|
|
|
|
ctx.beginPath()
|
|
coords.forEach((pt, i) => {
|
|
if (i === 0) ctx.moveTo(pt.x, pt.y)
|
|
else ctx.lineTo(pt.x, pt.y)
|
|
})
|
|
ctx.strokeStyle = COLORS.line
|
|
ctx.lineWidth = 2
|
|
ctx.stroke()
|
|
|
|
coords.forEach((pt, i) => {
|
|
ctx.beginPath()
|
|
ctx.arc(pt.x, pt.y, 4, 0, Math.PI * 2)
|
|
ctx.fillStyle = COLORS.accent
|
|
ctx.fill()
|
|
ctx.strokeStyle = '#fff'
|
|
ctx.lineWidth = 1.5
|
|
ctx.stroke()
|
|
|
|
ctx.fillStyle = COLORS.text
|
|
ctx.font = '10px sans-serif'
|
|
ctx.textAlign = 'center'
|
|
ctx.fillText(points[i].label, pt.x, height - 8)
|
|
})
|
|
|
|
if (unit) {
|
|
ctx.fillStyle = COLORS.text
|
|
ctx.font = '10px sans-serif'
|
|
ctx.textAlign = 'left'
|
|
ctx.fillText(unit, pad.left, pad.top - 2)
|
|
}
|
|
}
|