Files
2026-01-06 18:00:43 +08:00

243 lines
5.9 KiB
JavaScript

/**
* 简化版 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
}