import React, { useEffect, useRef } from 'react'; import './style.less'; interface TimeSeriesChartProps { labels: string[]; barData1: number[]; barData2: number[]; } const TimeSeriesChart: React.FC = ({ labels, barData1, barData2 }) => { const canvasRef = useRef(null); const animationIdRef = useRef(); const progressRef = useRef(0); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; // 防抖函数 const debounce = (func: () => void, wait: number) => { let timeout: NodeJS.Timeout; return () => { clearTimeout(timeout); timeout = setTimeout(() => func(), wait); }; }; // 数据校验 if (labels.length === 0 || barData1.length === 0 || barData2.length === 0) { console.warn('无数据可绘制'); return; } if (barData1.length !== labels.length || barData2.length !== labels.length) { console.error('labels、barData1 和 barData2 的长度不一致'); return; } // 计算最大值 const maxDataValue = Math.max(...barData1, ...barData2); if (!isFinite(maxDataValue) || maxDataValue < 0) { console.error('数据中可能包含非数值或者无效数值'); return; } // 如果最大值为0,表示所有数据都是0,此时可以选择不绘制或给出提示 if (maxDataValue === 0) { console.warn('所有数据均为0或无效数据'); return; } const yLabels = [0, maxDataValue * 0.2, maxDataValue * 0.4, maxDataValue * 0.6, maxDataValue * 0.8, maxDataValue]; const resizeCanvas = () => { const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // 重置并缩放 progressRef.current = 0; // 重置动画进度 draw(); // 调整大小后重新绘制 }; const draw = () => { // 清除画布 ctx.clearRect(0, 0, canvas.width, canvas.height); const chartWidth = canvas.clientWidth; const chartHeight = canvas.clientHeight; const leftPadding = 50; const rightPadding = 20; const bottomPadding = chartHeight * 0.2; const topPadding = chartHeight * 0.1; const totalBars = labels.length; const barGroupWidth = (chartWidth - leftPadding - rightPadding) / totalBars; // 避免除零 if (!isFinite(barGroupWidth) || barGroupWidth <= 0) { console.error('barGroupWidth 计算不正确,请检查数据和尺寸设置'); return; } const barWidth = barGroupWidth / 3.5; const maxBarHeight = chartHeight - topPadding - bottomPadding; const fontSize = chartWidth * 0.02; // 绘制y轴标签和网格线 yLabels.forEach(value => { const y = topPadding + (1 - value / maxDataValue) * maxBarHeight; ctx.strokeStyle = '#3e4b5b'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(leftPadding, y); ctx.lineTo(chartWidth - rightPadding, y); ctx.stroke(); ctx.fillStyle = '#FFFFFF'; ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; ctx.font = `${fontSize * 0.6}px Arial`; ctx.fillText(`${Math.round(value * 100) / 100}`, leftPadding - 10, y); }); // 绘制单个柱子的函数 const drawBar = ( value: number, index: number, barOffset: number, colorStart: string, colorEnd: string ) => { // 避免除零和无效值 if (maxDataValue === 0) return; const barHeight = (value / maxDataValue) * maxBarHeight * progressRef.current; const x = leftPadding + index * barGroupWidth + barWidth * barOffset; const y = topPadding + (maxBarHeight - barHeight); // 在创建渐变前校验数值 if (!isFinite(x) || !isFinite(y) || !isFinite(barHeight)) { console.error('Invalid value for gradient creation:', { x, y, barHeight }); return; } const gradient = ctx.createLinearGradient(x, y, x, y + barHeight); gradient.addColorStop(0, colorStart); gradient.addColorStop(1, colorEnd); ctx.fillStyle = gradient; ctx.fillRect(x, y, barWidth, barHeight); // 显示柱子值 ctx.fillStyle = '#FFFFFF'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; ctx.font = `${fontSize * 0.5}px Arial`; ctx.fillText(`${value}`, x + barWidth / 2, y - 5); }; // 绘制 barData1 的柱子 barData1.forEach((value, index) => { drawBar(value, index, 0.25, 'rgba(128, 191, 255, 1)', 'rgba(128, 191, 255, 0)'); }); // 绘制 barData2 的柱子 barData2.forEach((value, index) => { drawBar(value, index, 1.25, 'rgba(255, 234, 128, 1)', 'rgba(255, 234, 128, 0)'); }); // 绘制 x 轴标签 labels.forEach((label, index) => { const x = leftPadding + index * barGroupWidth + barGroupWidth / 2; ctx.fillStyle = '#FFFFFF'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.font = `${fontSize * 0.7}px Arial`; ctx.fillText(label, x, chartHeight - bottomPadding + 10); }); // 动画效果 if (progressRef.current < 1) { progressRef.current += 0.02; animationIdRef.current = requestAnimationFrame(draw); } }; // 初始化尺寸并首次绘制 resizeCanvas(); const handleResize = debounce(() => { resizeCanvas(); }, 100); window.addEventListener('resize', handleResize); // 启动动画 animationIdRef.current = requestAnimationFrame(draw); return () => { window.removeEventListener('resize', handleResize); if (animationIdRef.current) { cancelAnimationFrame(animationIdRef.current); } }; }, [labels, barData1, barData2]); return ; }; export default TimeSeriesChart;