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) } }