243 lines
5.9 KiB
JavaScript
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
|
|
}
|