/** * 简化版 ECharts - 仅支持折线图 * 用于微信小程序 Canvas 2D */ class ECharts { constructor(canvas) { this.canvas = canvas this.ctx = canvas.getContext('2d') this.option = null // canvas.width 已经是缩放后的尺寸,直接使用 this.width = canvas.width || 750 this.height = canvas.height || 500 this.padding = { top: 40, right: 40, bottom: 60, left: 80 } console.log('ECharts 构造函数:', { canvasWidth: canvas.width, canvasHeight: canvas.height, chartWidth: this.width, chartHeight: this.height }) } setOption(option) { this.option = option this.render() } clear() { if (!this.ctx) return this.ctx.clearRect(0, 0, this.width, this.height) } resize() { // 自动调整大小 } render() { if (!this.option || !this.ctx) { console.log('ECharts render: 缺少 option 或 ctx') return } const { xAxis, yAxis, series } = this.option if (!series || !series[0]) { console.log('ECharts render: 缺少 series 数据') return } const data = series[0].data || [] const categories = xAxis?.data || [] console.log('ECharts render:', { dataCount: data.length, categoryCount: categories.length, width: this.width, height: this.height }) this.clear() // 计算绘图区域 const chartWidth = this.width - this.padding.left - this.padding.right const chartHeight = this.height - this.padding.top - this.padding.bottom // 计算数据范围 const maxValue = Math.max(...data) * 1.1 const minValue = Math.min(...data) * 0.9 const valueRange = maxValue - minValue || 1 // 绘制坐标轴 this.drawAxes(chartWidth, chartHeight, minValue, maxValue, categories) // 绘制区域填充 if (series[0].areaStyle) { this.drawArea(data, chartWidth, chartHeight, minValue, valueRange) } // 绘制折线 this.drawLine(data, chartWidth, chartHeight, minValue, valueRange) // 绘制数据点 this.drawPoints(data, chartWidth, chartHeight, minValue, valueRange) } drawAxes(chartWidth, chartHeight, minValue, maxValue, categories) { const ctx = this.ctx const { top, right, bottom, left } = this.padding ctx.strokeStyle = '#e0e0e0' ctx.lineWidth = 1 // X 轴 ctx.beginPath() ctx.moveTo(left, top + chartHeight) ctx.lineTo(left + chartWidth, top + chartHeight) ctx.stroke() // Y 轴 ctx.beginPath() ctx.moveTo(left, top) ctx.lineTo(left, top + chartHeight) ctx.stroke() // 绘制 Y 轴刻度 ctx.fillStyle = '#666' ctx.font = '20px sans-serif' ctx.textAlign = 'right' ctx.textBaseline = 'middle' const ySteps = 5 for (let i = 0; i <= ySteps; i++) { const value = minValue + (maxValue - minValue) * (i / ySteps) const y = top + chartHeight - (chartHeight * (i / ySteps)) ctx.fillText( '¥' + Math.round(value), left - 10, y ) // 绘制网格线 ctx.strokeStyle = '#f0f0f0' ctx.beginPath() ctx.moveTo(left, y) ctx.lineTo(left + chartWidth, y) ctx.stroke() } // 绘制 X 轴标签 ctx.textAlign = 'center' ctx.textBaseline = 'top' const xStep = chartWidth / (categories.length || 1) const skipStep = Math.ceil(categories.length / 6) // 最多显示6个标签 categories.forEach((category, index) => { if (index % skipStep !== 0) return // 跳过部分标签 const x = left + xStep * (index + 0.5) const y = top + chartHeight + 10 ctx.save() ctx.translate(x, y) ctx.rotate(45 * Math.PI / 180) ctx.fillText(category, 0, 0) ctx.restore() }) } drawLine(data, chartWidth, chartHeight, minValue, valueRange) { const ctx = this.ctx const { top, left } = this.padding const xStep = chartWidth / (data.length || 1) ctx.strokeStyle = '#1890ff' ctx.lineWidth = 3 ctx.lineCap = 'round' ctx.lineJoin = 'round' ctx.beginPath() data.forEach((value, index) => { const x = left + xStep * (index + 0.5) const y = top + chartHeight - ((value - minValue) / valueRange) * chartHeight if (index === 0) { ctx.moveTo(x, y) } else { ctx.lineTo(x, y) } }) ctx.stroke() } drawArea(data, chartWidth, chartHeight, minValue, valueRange) { const ctx = this.ctx const { top, left } = this.padding const xStep = chartWidth / (data.length || 1) // 创建渐变 const gradient = ctx.createLinearGradient(0, top, 0, top + chartHeight) gradient.addColorStop(0, 'rgba(24, 144, 255, 0.3)') gradient.addColorStop(1, 'rgba(24, 144, 255, 0.05)') ctx.fillStyle = gradient ctx.beginPath() data.forEach((value, index) => { const x = left + xStep * (index + 0.5) const y = top + chartHeight - ((value - minValue) / valueRange) * chartHeight if (index === 0) { ctx.moveTo(x, y) } else { ctx.lineTo(x, y) } }) // 闭合路径 const lastX = left + xStep * (data.length - 0.5) ctx.lineTo(lastX, top + chartHeight) ctx.lineTo(left + xStep * 0.5, top + chartHeight) ctx.closePath() ctx.fill() } drawPoints(data, chartWidth, chartHeight, minValue, valueRange) { const ctx = this.ctx const { top, left } = this.padding const xStep = chartWidth / (data.length || 1) data.forEach((value, index) => { const x = left + xStep * (index + 0.5) const y = top + chartHeight - ((value - minValue) / valueRange) * chartHeight // 绘制点 ctx.fillStyle = '#fff' ctx.strokeStyle = '#1890ff' ctx.lineWidth = 2 ctx.beginPath() ctx.arc(x, y, 4, 0, 2 * Math.PI) ctx.fill() ctx.stroke() }) } } function init(canvas, width, height) { const chart = new ECharts(canvas) return chart } module.exports = { init }