<template>
	<view class="chart-container">
		<!-- Legend 图例 - 顶部 -->
		<view v-if="mergedLegendConfig.enabled && mergedLegendConfig.position === 'top'" class="legend legend-top">
			<view
				:key="index"
				v-for="(dataset, index) in chartData.datasets"
				:class="['legend-item', { 'legend-item-disabled': hiddenDatasets[index] }]"
				@tap="toggleDataset(index)"
			>
				<view class="legend-color" :style="{ backgroundColor: dataset.color }" />
				<text class="legend-label">{{ dataset.label }}</text>
			</view>
		</view>

		<view class="chart-wrapper" :style="{ width: width, height: height }">
			<!-- 加载占位符 -->
			<!-- <view v-if="!isInitialized" class="chart-loading">
				<text class="loading-text">加载中...</text>
			</view> -->

			<!-- #ifdef MP-ALIPAY -->
			<canvas
				:id="canvasId"
				v-show="isInitialized"
				:canvas-id="canvasId"
				class="chart"
				:height="canvasHeight"
				:style="{ width: canvasStyleWidth + 'px', height: canvasStyleHeight + 'px' }"
				:width="canvasWidth"
				@touchend="handleTouchEnd"
				@touchmove="handleTouchMove"
				@touchstart="handleTouchStart"
			/>
			<!-- #endif -->

			<!-- #ifndef MP-ALIPAY -->
			<canvas
				:id="canvasId"
				v-show="isInitialized"
				:canvas-id="canvasId"
				class="chart"
				:style="{ width: width, height: height }"
				type="2d"
				@touchend="handleTouchEnd"
				@touchmove="handleTouchMove"
				@touchstart="handleTouchStart"
			/>
			<!-- #endif -->

			<!-- Tooltip浮层 -->
			<view class="tooltip" :style="{...tooltipStyle}">
				<view class="tooltip-label">{{ tooltipData.label }}</view>
				<view
					:key="index"
					v-for="(dataset, index) in chartData.datasets"
					class="tooltip-item"
				>
					<view class="tooltip-dot" :style="{ backgroundColor: dataset.color }" />
					<text style="white-space: nowrap;">{{ dataset.label }}: {{ formatTooltipValue(tooltipData.values[index]) }}</text>
				</view>
			</view>

			<!-- 缩放控制按钮 -->
			<view v-if="showZoomControls && scale !== 1" class="zoom-controls">
				<button class="reset-btn" @tap="resetZoom">重置缩放</button>
			</view>
		</view>

		<!-- Legend 图例 - 底部 -->
		<view v-if="mergedLegendConfig.enabled && mergedLegendConfig.position === 'bottom'" class="legend legend-bottom">
			<view
				:key="index"
				v-for="(dataset, index) in chartData.datasets"
				:class="['legend-item', { 'legend-item-disabled': hiddenDatasets[index] }]"
				@tap="toggleDataset(index)"
			>
				<view class="legend-color" :style="{ backgroundColor: dataset.color }" />
				<text class="legend-label">{{ dataset.label }}</text>
			</view>
		</view>
	</view>
</template>

<script>
	// https://ext.dcloud.net.cn/plugin?id=25773
export default {
	name: 'LineChart',
	props: {
		canvasId: {
			type: String,
			default: 'lineChart_' + new Date().getTime()
		},
		
		// 图表类型：'line' | 'bar'
		chartType: {
			type: String,
			default: 'line'
		},

		// 图表尺寸
		width: {
			type: String,
			default: '100%'
		},
		height: {
			type: String,
			default: '250px'
		},

		// 图表数据
		chartData: {
			type: Object,
			required: true,
			// 数据格式示例：
			// {
			//   labels: ['1月', '2月', '3月', ...],
			//   datasets: [
			//     {
			//       label: '产品A',
			//       data: [30, 45, 60, ...],
			//       color: '#FF6B9D',
			//       gradientStart: '#FFB6C1',
			//       gradientEnd: '#FF1493'
			//     },
			//     {
			//       label: '产品B',
			//       data: [50, 65, 80, ...],
			//       color: '#4ECDC4',
			//       gradientStart: '#A8E6CF',
			//       gradientEnd: '#00CED1'
			//     }
			//   ]
			// }
		},

		// 内边距配置
		padding: {
			type: Object,
			default: ()=> ({
				top: 20,
				right: 20,
				bottom: 30,
				left: 50
			})
		},

		// 缩放配置
		zoomConfig: {
			type: Object,
			default: ()=> ({
				enabled: true, // 是否启用缩放
				minScale: 1, // 最小缩放比例
				maxScale: 10, // 最大缩放比例
				showControls: true // 是否显示重置按钮
			})
		},

		// 动画配置
		animationConfig: {
			type: Object,
			default: ()=> ({
				enabled: true, // 是否启用动画
				duration: 30 // 动画帧数（默认30帧，约500ms）
			})
		},

		// Y轴配置
		yAxisConfig: {
			type: Object,
			default: ()=> ({
				steps: 5, // Y轴刻度数量
				autoRange: true, // 自动计算范围
				min: null, // 手动设置最小值
				max: null, // 手动设置最大值
				allowNegative: true // 是否允许Y轴出现负数（默认允许）
			})
		},

		// X轴配置
		xAxisConfig: {
			type: Object,
			default: ()=> ({
				labelDensity: 'auto', // 标签密度：'auto' | 'sparse' | 'dense'
				showAllLabels: false, // 是否显示所有标签
				formatter: null, // 自定义格式化函数：(label, index) => string
				lineHeight: 14 // 多行文本的行高（像素）
			})
		},

		// Tooltip配置
		tooltipConfig: {
			type: Object,
			default: ()=> ({
				enabled: true, // 是否启用tooltip
				threshold: 60 // 触发距离阈值（px）
			})
		},

		// Legend配置
		legendConfig: {
			type: Object,
			default: ()=> ({
				enabled: true, // 是否显示图例
				position: 'bottom', // 位置：'top' | 'bottom'
				clickable: true // 是否可点击切换显示/隐藏
			})
		},

		// 柱状图配置
		barConfig: {
			type: Object,
			default: ()=> ({
				width: 8, // 固定柱宽（像素）
				innerGap: 2, // 柱组内间隙（像素）
				bottomGap: 2, // 底部与 X 轴的间隙（像素）
				leftGap: 2, // 与 Y 轴的间隙保护（像素）
				// 动画模式：'all' - 所有柱同一进度生长; 'stagger' - 按列逐步渲染
				animateMode: 'stagger',
				// 当 animateMode === 'stagger' 时，列间的延迟（进度的分数，0.0 - 1.0）
				staggerDelayFraction: 0.02,
				// 圆角：支持 number / { top, bottom } / { topLeft, topRight, bottomRight, bottomLeft }
				// 默认仅顶部圆角
				borderRadius: { top: 4, bottom: 0 }
			})
		}
	},

	data() {
		return {
			ctx: null,
			canvas: null,
			dpr: 1,
			chartWidth: 0,
			chartHeight: 0,

			// 动画相关
			animationProgress: 0,
			animationFrameCount: 0,

			// 数据点 - 改为数组以支持多折线
			points: [], // 每个元素是一个数据集的点数组

			// Tooltip
			showTooltip: false,
			currentPointIndex: -1,
			tooltipData: {
				label: '',
				values: [] // 改为数组以支持多折线
			},
			tooltipPosition: {
				x: 0,
				y: 0
			},

			// Canvas位置缓存
			canvasRect: null,
			wrapperRect: null,

			// 数据过渡 - 改为数组以支持多折线
			oldData: [], // 每个元素是一个数据集的旧数据数组
			transitionProgress: 0,
			isTransitioning: false,

			// 缩放重置动画
			isResettingZoom: false,
			resetProgress: 0,
			resetStartScale: 1,
			resetStartOffsetX: 0,
			resetStartOffsetY: 0,

			// Legend 图例
			hiddenDatasets: {}, // 隐藏的数据集索引 { 0: true, 1: false, ... }
			yAxisMin: 0,
			yAxisMax: 100,

			// 缩放和平移
			scale: 1,
			offsetX: 0,
			offsetY: 0,
			// 持久的 bar 水平偏移，用来保证首柱与 Y 轴的最小间隙（在 scale===1 时生效）
			barShift: 0,
			// 柱状图布局缓存：每个数据集对应一组矩形 { x, y, width, height, value }
			barRects: [],
			lastTouchDistance: 0,
			lastTouchCenter: { x: 0, y: 0 },
			isPinching: false,
			isDragging: false,
			lastTouchPos: { x: 0, y: 0 },
			touchStartPos: { x: 0, y: 0 },
			touchStartTime: 0,

			// #ifdef MP-ALIPAY
			canvasWidth: 750, // 初始值，避免为0导致Canvas不显示
			canvasHeight: 400,
			canvasStyleWidth: 375,
			canvasStyleHeight: 250,
			// #endif

			// 初始化状态
			isInitialized: false
		}
	},

	computed: {
		// 合并后的配置（保留默认值）
		mergedPadding() {
			return {
				top: 20,
				right: 20,
				bottom: 30,
				left: 50,
				...this.padding
			}
		},

		mergedZoomConfig() {
			return {
				enabled: true,
				minScale: 1,
				maxScale: 10,
				showControls: true,
				...this.zoomConfig
			}
		},

		mergedAnimationConfig() {
			return {
				enabled: true,
				duration: 30,
				...this.animationConfig
			}
		},

		mergedYAxisConfig() {
			return {
				steps: 5,
				autoRange: true,
				min: null,
				max: null,
				allowNegative: true,
				...this.yAxisConfig
			}
		},

		mergedXAxisConfig() {
			return {
				labelDensity: 'auto',
				showAllLabels: false,
				formatter: null,
				lineHeight: 14,
				...this.xAxisConfig
			}
		},

		mergedTooltipConfig() {
			return {
				enabled: true,
				threshold: 60,
				...this.tooltipConfig
			}
		},

		mergedLegendConfig() {
			return {
				enabled: true,
				position: 'bottom',
				clickable: true,
				...this.legendConfig
			}
		},

		mergedBarConfig() {
			return {
				width: 8, // 固定柱宽（像素）
				innerGap: 2, // 柱组内间隙（像素）
				bottomGap: 2, // 底部与 X 轴的间隙（像素）
				leftGap: 2, // 与 Y 轴的间隙保护（像素）
				// 动画模式：'all' - 所有柱同一进度生长; 'stagger' - 按列逐步渲染
				animateMode: 'stagger',
				// 当 animateMode === 'stagger' 时，列间的延迟（进度的分数，0.0 - 1.0）
				staggerDelayFraction: 0.02,
				// 圆角：默认仅顶部圆角，支持 number / { top, bottom } / per-corner 对象
				borderRadius: { top: 4, bottom: 0 },
				...this.barConfig
			}
		},

		showZoomControls() {
			return this.mergedZoomConfig.enabled && this.mergedZoomConfig.showControls
		},

		tooltipStyle() {
			if (!this.showTooltip) {
				return {
					opacity: 0,
					transform: 'translateX(-50%) scale(0.8)',
					pointerEvents: 'none',
					transition: 'none'
				}
			}
			let top = this.tooltipPosition.y

			// #ifdef MP-TOUTIAO
			if(top > parseInt(this.height) - 20) {
				top = parseInt(this.height) / 2 - 10
			}
			// #endif

			return {
				left: this.tooltipPosition.x + 'px',
				top: top + 'px',
				opacity: 1,
				transform: 'translateX(-50%) scale(1)'
			}
		}
	},

	watch: {
		chartData: {
			handler(newVal, oldChartData) {
				if (!oldChartData || !this.ctx) {
					return
				}

				// 保存旧数据用于过渡动画 - 支持多折线
				this.oldData = oldChartData.datasets.map(dataset=> [...dataset.data])

				// 开始过渡动画
				this.startDataTransition()
			},
			deep: true
		}
	},

	mounted() {
		this.initChart()
		// // #ifdef H5
		// this.initChart()
		// // #endif
	},

	onReady() {
		this.initChart()
		// // #ifndef H5
		// this.initChart()
		// // #endif
	},

	beforeDestroy() {
		// 清理资源
		this.isInitialized = false
		this.ctx = null
		this.canvas = null
	},

	methods: {
		async initChart() {
			try {
				// 重置初始化状态
				this.isInitialized = false

				// #ifdef H5
				this.dpr = 1
				// #endif

				// #ifndef H5
				const systemInfo = uni.getSystemInfoSync()
				this.dpr = systemInfo.pixelRatio || 1
				// #endif

				// #ifdef MP-ALIPAY
				await this.initAlipayCanvas()
				// #endif

				// #ifndef MP-ALIPAY
				await this.initWechatCanvas()
				// #endif

				// 初始化旧数据 - 支持多折线
				this.oldData = this.chartData.datasets.map(dataset=> [...dataset.data])

				// 标记为已初始化
				this.isInitialized = true

			} catch (error) {
				console.error('Canvas初始化失败:', error)
			}

			if (this.mergedAnimationConfig.enabled) {
				this.startAnimation()
			} else {
				this.animationProgress = 1
				this.drawChart()
			}
		},

		// 微信/抖音小程序Canvas初始化（type="2d"）
		// #ifndef MP-ALIPAY
		initWechatCanvas() {
			return new Promise((resolve)=> {
				// 延迟执行，确保DOM已渲染
				// setTimeout(()=> {
				// 先获取容器尺寸
				const query1 = uni.createSelectorQuery().in(this)
				query1.select('.chart-wrapper').boundingClientRect((rect)=> {
					if (!rect) {
						console.error('[LineChart] 无法获取容器尺寸')
						this.initFallbackCanvas()
						resolve()
						return
					}

					// 计算容器宽高
					const containerWidth = rect.width || rect.right - rect.left || 0
					const containerHeight = rect.height || rect.bottom - rect.top || 250

					if (containerWidth === 0 || containerHeight === 0) {
						console.error('[LineChart] 容器尺寸为0')
						this.initFallbackCanvas()
						resolve()
						return
					}

					// 再获取Canvas节点
					const query2 = uni.createSelectorQuery().in(this)
					query2
						.select(`#${this.canvasId}`)
						.fields({ node: true, size: true })
						.exec((res)=> {
							if (res[0] && res[0].node) {
								const canvas = res[0].node

								// 使用容器尺寸，而不是Canvas返回的尺寸
								const width = containerWidth
								const height = containerHeight

								this.canvas = canvas
								this.ctx = canvas.getContext('2d')
								this.chartWidth = width
								this.chartHeight = height
								// #ifndef H5
								canvas.width = width * this.dpr
								canvas.height = height * this.dpr
								// #endif

								this.ctx.scale(this.dpr, this.dpr)

								// 立即清空 Canvas，避免显示旧内容
								this.ctx.clearRect(0, 0, width, height)
								this.ctx.fillStyle = '#ffffff'
								this.ctx.fillRect(0, 0, width, height)

								this.updateCanvasRect()
								resolve()
							} else {
								console.error('[LineChart] 无法获取Canvas节点')
								this.initFallbackCanvas()
								resolve()
							}
						})
				}).exec()
				// }, 300) // 延迟300ms，确保DOM渲染完成
			})
		},
		// #endif

		initFallbackCanvas() {
			const query = uni.createSelectorQuery().in(this)
			query
				.select(`#${this.canvasId}`)
				.boundingClientRect((data)=> {
					this.chartWidth = data.width
					this.chartHeight = data.height
					this.ctx = uni.createCanvasContext(this.canvasId, this)
					this.updateCanvasRect()
				})
				.exec()
		},

		// 支付宝小程序Canvas初始化
		// #ifdef MP-ALIPAY
		initAlipayCanvas() {
			return new Promise((resolve)=> {
				// 计算Canvas显示尺寸（逻辑像素）
				const query = uni.createSelectorQuery().in(this)
				query.select('.chart-wrapper').boundingClientRect((rect)=> {
					if (!rect) {
						console.error('无法获取chart-wrapper尺寸')
						resolve()
						return
					}

					const displayWidth = rect.width
					const displayHeight = rect.height

					// 设置Canvas样式尺寸（显示尺寸）
					this.canvasStyleWidth = displayWidth
					this.canvasStyleHeight = displayHeight

					// 设置Canvas物理像素尺寸（实际绘制尺寸，用于高清显示）
					this.canvasWidth = Math.floor(displayWidth * this.dpr)
					this.canvasHeight = Math.floor(displayHeight * this.dpr)

					// 设置绘制时使用的逻辑尺寸
					this.chartWidth = displayWidth
					this.chartHeight = displayHeight

					// 延迟执行，确保DOM已渲染
					setTimeout(()=> {
						this.ctx = uni.createCanvasContext(this.canvasId, this)

						if (!this.ctx) {
							console.error('Canvas上下文创建失败')
							resolve()
							return
						}

						// 缓存Canvas位置
						this.updateCanvasRect()

						resolve()
					}, 300)
				}).exec()
			})
		},
		// #endif

		updateCanvasRect() {
			const query = uni.createSelectorQuery().in(this)
			query.select(`#${this.canvasId}`).boundingClientRect((data)=> {
				this.canvasRect = data
			}).exec()

			query.select('.chart-wrapper').boundingClientRect((data)=> {
				this.wrapperRect = data
			}).exec()
		},

		startAnimation() {
			this.animationProgress = 0
			this.animationFrameCount = 0
			this.animate()
		},

		animate() {
			if (this.animationFrameCount < this.mergedAnimationConfig.duration) {
				this.animationFrameCount++
				this.animationProgress = this.animationFrameCount / this.mergedAnimationConfig.duration
				this.drawChart()
				setTimeout(()=> this.animate(), 16)
			} else {
				this.animationProgress = 1
				this.drawChart()
			}
		},

		startDataTransition() {
			this.isTransitioning = true
			this.transitionProgress = 0
			this.animateDataTransition()
		},

		animateDataTransition() {
			if (this.transitionProgress < 1) {
				this.transitionProgress += 0.1 // 加快过渡速度（从0.05改为0.1，约160ms完成）

				// 如果 tooltip 正在显示，更新 tooltip 数据
				if (this.showTooltip && this.currentPointIndex !== -1) {
					this.updateTooltipData(this.currentPointIndex)
				}

				this.drawChart()
				setTimeout(()=> this.animateDataTransition(), 16)
			} else {
				this.transitionProgress = 1
				this.isTransitioning = false
				this.oldData = this.chartData.datasets.map(dataset=> [...dataset.data])

				// 最后一次更新 tooltip 数据
				if (this.showTooltip && this.currentPointIndex !== -1) {
					this.updateTooltipData(this.currentPointIndex)
				}

				this.drawChart()
			}
		},

		easeOutQuad(t) {
			return t * (2 - t)
		},

		// 更新 Tooltip 数据
		updateTooltipData(closestIndex) {
			// 收集所有数据集的值 - 如果正在过渡则使用过渡数据
			const values = this.chartData.datasets.map((dataset, index)=> {
				if (this.isTransitioning) {
					const transitionData = this.getTransitionData(index)
					return transitionData[closestIndex]
				} else {
					return dataset.data[closestIndex]
				}
			})

			this.tooltipData = {
				label: this.chartData.labels[closestIndex],
				values: values
			}
		},

		// 格式化 Tooltip 显示的值
		formatTooltipValue(value) {
			if (value === undefined || value === null) {
				return '-'
			}
			// 四舍五入到整数
			// return Math.round(value)
			return value
		},

		// 重置缩放 - 带过渡动画
		resetZoom() {
			if (this.isResettingZoom) return

			// 保存当前状态
			this.resetStartScale = this.scale
			this.resetStartOffsetX = this.offsetX
			this.resetStartOffsetY = this.offsetY

			// 开始重置动画
			this.isResettingZoom = true
			this.resetProgress = 0
			this.animateResetZoom()
		},

		// 重置缩放动画
		animateResetZoom() {
			if (!this.isResettingZoom) return

			const duration = 200 // 动画时长（毫秒，加快速度）
			const startTime = Date.now()

			const animate = ()=> {
				const elapsed = Date.now() - startTime
				const progress = Math.min(elapsed / duration, 1)

				// 使用缓动函数
				const eased = this.easeOutCubic(progress)

				// 插值计算当前值
				this.scale = this.resetStartScale + (1 - this.resetStartScale) * eased
				this.offsetX = this.resetStartOffsetX + (0 - this.resetStartOffsetX) * eased
				this.offsetY = this.resetStartOffsetY + (0 - this.resetStartOffsetY) * eased

				// 重绘图表
				this.drawChart()

				if (progress < 1) {
					// 继续动画
					setTimeout(()=> animate(), 16) // 约60fps
				} else {
					// 动画结束
					this.scale = 1
					this.offsetX = 0
					this.offsetY = 0
					this.isResettingZoom = false
					this.drawChart()
				}
			}

			animate()
		},

		// 缓动函数 - 三次方缓出
		easeOutCubic(t) {
			return 1 - Math.pow(1 - t, 3)
		},

		// 切换数据集显示/隐藏
		toggleDataset(index) {
			if (!this.mergedLegendConfig.clickable) return

			// 切换隐藏状态
			this.$set(this.hiddenDatasets, index, !this.hiddenDatasets[index])

			// 重绘图表
			this.drawChart()
		},

		// 绘制图表
		drawChart() {
			if (!this.ctx) {
				return
			}

			const { chartWidth, chartHeight, chartData, animationProgress } = this
			const padding = this.mergedPadding

			// 支付宝小程序需要在每次绘制前设置缩放
			// #ifdef MP-ALIPAY
			// 先保存状态
			this.ctx.save()
			// 设置缩放以适配高分辨率
			this.ctx.scale(this.dpr, this.dpr)
			// #endif

			// 清空画布并绘制白色背景
			// #ifdef MP-ALIPAY
			this.ctx.fillStyle = '#ffffff'
			this.ctx.fillRect(0, 0, chartWidth, chartHeight)
			// #endif

			// #ifndef MP-ALIPAY
			this.ctx.clearRect(0, 0, chartWidth, chartHeight)
			this.ctx.fillStyle = '#ffffff'
			this.ctx.fillRect(0, 0, chartWidth, chartHeight)
			// #endif

			// 计算坐标轴区域
			const graphWidth = chartWidth - padding.left - padding.right
			const graphHeight = chartHeight - padding.top - padding.bottom

			// 绘制坐标轴和网格
			this.drawAxis(graphWidth, graphHeight, padding)

			// 计算数据点位置
			this.calculatePoints(graphWidth, graphHeight, padding)

			// 保存上下文状态，为绘制数据内容设置裁剪
			this.ctx.save()

			// 设置裁剪区域
			const clipPaddingRight = 10
			const clipPaddingLeft = 6
			const clipPaddingVertical = 10
			this.ctx.beginPath()
			this.ctx.rect(
				padding.left - clipPaddingLeft,
				padding.top - clipPaddingVertical,
				graphWidth + clipPaddingLeft + clipPaddingRight,
				graphHeight + clipPaddingVertical * 2
			)
			this.ctx.clip()

			// 绘制折线 - 支持多折线
			chartData.datasets.forEach((dataset, index)=> {
				// 跳过隐藏的数据集
				if (this.hiddenDatasets[index]) return

				if (this.chartType === 'bar') {
					// 柱状图在后面统一绘制
					// prepare barRects in calculateBarRects
				} else {
					if (this.points[index]) {
						this.drawLine(this.points[index], dataset, animationProgress)
					}
				}
			})

			// 绘制数据点 - 支持多折线
			chartData.datasets.forEach((dataset, index)=> {
				// 跳过隐藏的数据集
				if (this.hiddenDatasets[index]) return

				if (this.chartType === 'bar') {
					// 柱状图绘制由 drawBars 处理
					// ensure barRects 已计算
					if (!this.barRects || this.barRects.length === 0) {
						this.calculateBarRects(graphWidth, graphHeight, padding)
					}
					const rects = this.barRects[index] || []
					this.drawBars(rects, dataset, animationProgress)
				} else {
					if (this.points[index]) {
						this.drawPoints(this.points[index], dataset, animationProgress)
					}
				}
			})

			// 恢复上下文状态
			this.ctx.restore()

			// 如果正在显示Tooltip，绘制指示线
			if (this.showTooltip && this.currentPointIndex !== -1) {
				// 不使用额外的裁剪区域绘制 tooltip 指示，避免阴影被裁切。
				// drawTooltipLine 内部会限制绘制范围为图表区域。
				this.drawTooltipLine(this.currentPointIndex)
			}

			// 支付宝小程序需要调用draw方法
			// #ifdef MP-ALIPAY
			// 恢复之前保存的状态
			this.ctx.restore()
			if (this.ctx && this.ctx.draw) {
				this.ctx.draw()
			}
			// #endif
		},

		// 绘制 X 轴标签（支持换行符）
		drawXAxisLabel(text, x, y) {
			// 将文本按换行符分割
			const lines = String(text).split('\n')
			const lineHeight = this.mergedXAxisConfig.lineHeight || 14

			// 如果只有一行，直接绘制
			if (lines.length === 1) {
				this.ctx.fillText(text, x, y)
				return
			}

			// 多行文本，计算起始 Y 坐标（居中对齐）
			const totalHeight = lines.length * lineHeight
			const startY = y - (totalHeight - lineHeight) / 2

			// 绘制每一行
			lines.forEach((line, index)=> {
				this.ctx.fillText(line, x, startY + index * lineHeight)
			})
		},

		// 绘制坐标轴 - 支持多折线
		drawAxis(graphWidth, graphHeight, padding) {
			// 收集所有数据集的数据（跳过隐藏的数据集）
			const allData = []
			this.chartData.datasets.forEach((dataset, index)=> {
				// 跳过隐藏的数据集
				if (this.hiddenDatasets[index]) return

				const data = this.isTransitioning ? this.getTransitionData(index) : dataset.data
				allData.push(...data)
			})

			// 计算 Y 轴范围
			let minValue, maxValue

			// 检查是否手动设置了范围
			if (!this.mergedYAxisConfig.autoRange &&
				this.mergedYAxisConfig.min !== null &&
				this.mergedYAxisConfig.max !== null) {
				// 使用手动设置的范围
				minValue = this.mergedYAxisConfig.min
				maxValue = this.mergedYAxisConfig.max
			} else {
				// 自动计算范围
				if (allData.length === 0) {
					// 如果所有数据集都被隐藏，使用默认值
					minValue = 0
					maxValue = 100
				} else {
					const dataMin = Math.min(...allData)
					const dataMax = Math.max(...allData)

					let dataRange = dataMax - dataMin

					// 如果所有数据都相同（dataRange = 0），设置一个默认范围
					if (dataRange === 0) {
						// 如果数据值为0，使用默认范围 0~100
						if (dataMin === 0) {
							minValue = 0
							maxValue = 100
						} else {
							// 如果数据值不为0，使用数据值的 ±20%
							const defaultRange = Math.abs(dataMin) * 0.4
							minValue = Math.floor(dataMin - defaultRange)
							maxValue = Math.ceil(dataMax + defaultRange)
						}
					} else {
						// 正常情况：添加20%的padding
						const padding_percent = 0.2
						minValue = Math.floor(dataMin - dataRange * padding_percent)
						maxValue = Math.ceil(dataMax + dataRange * padding_percent)
					}

					// 如果配置不允许负数，且计算出的最小值为负数，则将最小值设为0
					if (!this.mergedYAxisConfig.allowNegative && minValue < 0) {
						minValue = 0
					}
				}

				// 如果只设置了 min，使用手动 min 和自动计算的 max
				if (this.mergedYAxisConfig.min !== null) {
					minValue = this.mergedYAxisConfig.min
				}

				// 如果只设置了 max，使用自动计算的 min 和手动 max
				if (this.mergedYAxisConfig.max !== null) {
					maxValue = this.mergedYAxisConfig.max
				}
			}

			this.yAxisMin = minValue
			this.yAxisMax = maxValue

			const centerX = padding.left + graphWidth / 2

			// 绘制Y轴网格线
			const ySteps = this.mergedYAxisConfig.steps
			const yRange = maxValue - minValue
			const yStepValue = yRange / ySteps
			const yStepHeight = graphHeight / ySteps

			for (let i = 0; i <= ySteps; i++) {
				const y = padding.top + i * yStepHeight

				if (i === ySteps) {
					this.ctx.strokeStyle = '#d0d7de'
					this.ctx.lineWidth = 1
				} else {
					this.ctx.strokeStyle = '#eaeef2'
					this.ctx.lineWidth = 0.5
				}

				this.ctx.beginPath()
				this.ctx.moveTo(padding.left, y)
				this.ctx.lineTo(padding.left + graphWidth, y)
				this.ctx.stroke()

				this.ctx.font = 'bold 11px -apple-system, BlinkMacSystemFont, \'Segoe UI\', sans-serif'
				this.ctx.fillStyle = '#57606a'
				this.ctx.textAlign = 'right'
				this.ctx.textBaseline = 'middle'
				const labelValue = Math.round(maxValue - i * yStepValue)
				this.ctx.fillText(labelValue + '', padding.left - 10, y)
			}

			// 绘制X轴标签
			this.ctx.font = 'bold 11px -apple-system, BlinkMacSystemFont, \'Segoe UI\', sans-serif'
			this.ctx.textAlign = 'center'
			this.ctx.textBaseline = 'top'
			this.ctx.fillStyle = '#57606a'

			const labelCount = this.chartData.labels.length
			let step = 1

			// 如果配置了 showAllLabels，显示所有标签
			if (this.mergedXAxisConfig.showAllLabels) {
				step = 1
			} else {
				// 根据 labelDensity 配置计算步长
				const effectiveScale = this.scale
				const scaledSpacing = graphWidth / (labelCount - 1) * effectiveScale

				// 根据缩放后的间距和配置的密度计算步长
				if (this.mergedXAxisConfig.labelDensity === 'sparse') {
					// 稀疏模式
					if (scaledSpacing > 80) {
						step = 1
					} else if (scaledSpacing > 50) {
						step = 3
					} else if (scaledSpacing > 30) {
						step = 5
					} else if (labelCount > 20) {
						step = Math.ceil(labelCount / 5)
					} else if (labelCount > 12) {
						step = Math.ceil(labelCount / 7)
					} else {
						step = 3
					}
				} else if (this.mergedXAxisConfig.labelDensity === 'dense') {
					// 密集模式
					if (scaledSpacing > 80) {
						step = 1
					} else if (scaledSpacing > 40) {
						step = 1
					} else if (scaledSpacing > 25) {
						step = 2
					} else if (labelCount > 20) {
						step = Math.ceil(labelCount / 12)
					} else if (labelCount > 12) {
						step = Math.ceil(labelCount / 15)
					} else {
						step = 1
					}
				} else {
					// auto 模式（默认）
					if (scaledSpacing > 80) {
						step = 1
					} else if (scaledSpacing > 50) {
						step = 2
					} else if (scaledSpacing > 30) {
						step = 3
					} else if (labelCount > 20) {
						step = Math.ceil(labelCount / 7)
					} else if (labelCount > 12) {
						step = Math.ceil(labelCount / 10)
					} else if (labelCount > 7) {
						step = 2
					}
				}
			}

			this.chartData.labels.forEach((label, i)=> {
				const baseX = padding.left + i * graphWidth / (this.chartData.labels.length - 1)
				const x = centerX + (baseX - centerX) * this.scale + this.offsetX

				if ((i % step === 0 || i === labelCount - 1) && x >= padding.left - 20 && x <= padding.left + graphWidth + 20) {
					// 应用自定义格式化函数
					let displayLabel = label
					if (this.mergedXAxisConfig.formatter && typeof this.mergedXAxisConfig.formatter === 'function') {
						displayLabel = this.mergedXAxisConfig.formatter(label, i)
					}

					// 绘制标签（支持换行符）
					this.drawXAxisLabel(displayLabel, x, padding.top + graphHeight + 12)
				}
			})

			// 绘制Y轴线
			this.ctx.strokeStyle = '#d0d7de'
			this.ctx.lineWidth = 1
			this.ctx.beginPath()
			this.ctx.moveTo(padding.left, padding.top)
			this.ctx.lineTo(padding.left, padding.top + graphHeight)
			this.ctx.stroke()
		},

		// 计算数据点位置 - 支持多折线
		calculatePoints(graphWidth, graphHeight, padding) {
			this.points = []

			const minValue = this.yAxisMin || 0
			const maxValue = this.yAxisMax || 100
			const yRange = maxValue - minValue

			const centerX = padding.left + graphWidth / 2

			// 为每个数据集计算点位置
			this.chartData.datasets.forEach((dataset, datasetIndex)=> {
				const data = this.isTransitioning ? this.getTransitionData(datasetIndex) : dataset.data
				const datasetPoints = []

				data.forEach((value, i)=> {
					const baseX = padding.left + i * graphWidth / (this.chartData.labels.length - 1)
					const baseY = padding.top + graphHeight - (value - minValue) / yRange * graphHeight

					const x = centerX + (baseX - centerX) * this.scale + this.offsetX
					const y = baseY

					datasetPoints.push({ x, y, value })
				})

				this.points.push(datasetPoints)
			})

			// 如果是柱状图，计算每个柱的矩形位置
			if (this.chartType === 'bar') {
				this.calculateBarRects(graphWidth, graphHeight, padding)
			}
		},

		// 计算柱状图矩形（每个数据集对应一组矩形）
		calculateBarRects(graphWidth, graphHeight, padding) {
			this.barRects = []

			// 如果处于缩放状态，清除持久偏移（持久偏移只在 scale===1 时生效）
			if (this.scale !== 1) {
				this.barShift = 0
			}

			const labelsCount = this.chartData.labels.length
			const minValue = this.yAxisMin || 0
			const maxValue = this.yAxisMax || 100
			const yRange = maxValue - minValue || 1

			// 可见数据集索引（保持原始索引以便图例切换匹配）
			const visibleIndices = this.chartData.datasets.map((d, i)=> i).filter(i=> !this.hiddenDatasets[i])
			const visibleCount = Math.max(1, visibleIndices.length)

			// 计算基准中心（与 calculatePoints 保持一致）
			const centerAnchor = padding.left + graphWidth / 2

			// 每个数据点在未缩放时的间距（与 calculatePoints 保持一致）
			const step = graphWidth / Math.max(1, labelsCount - 1)
			const groupWidth = step * 0.8
			// 单个柱子在未缩放时的宽度
			const baseSingleBarWidth = groupWidth / visibleCount

			// 初始化每个数据集的 rect 数组
			for (let i = 0; i < this.chartData.datasets.length; i++) {
				this.barRects[i] = []
			}

			this.chartData.datasets.forEach((dataset, datasetIndex)=> {
				const data = this.isTransitioning ? this.getTransitionData(datasetIndex) : dataset.data

				data.forEach((value, i)=> {
					// baseX 与 calculatePoints 保持一致（未缩放坐标）
					const baseX = padding.left + i * step

					// 固定柱宽（像素），但限制不超过 step * 0.9
					let singleBarWidth = this.mergedBarConfig.width || 16
					singleBarWidth = Math.min(singleBarWidth, step * 0.9)

					// 组内排列：组宽由固定宽度和 innerGap 决定（组内偏移不随缩放改变）
					const visibleIndex = visibleIndices.indexOf(datasetIndex)
					const innerGap = this.mergedBarConfig.innerGap || 4
					const totalGroupWidth = visibleCount * singleBarWidth + Math.max(0, visibleCount - 1) * innerGap
					const firstOffset = -totalGroupWidth / 2 + singleBarWidth / 2
					const offset = visibleIndex !== -1 ? firstOffset + visibleIndex * (singleBarWidth + innerGap) : 0

					// 将组内偏移（不随缩放）加到缩放后的组中心上，保证组内柱不分开
					// 同时应用持久的 barShift，以确保首柱与 Y 轴有足够间隙（在 scale===1 时）
					const centerX = centerAnchor + (baseX - centerAnchor) * this.scale + offset + this.offsetX + (this.barShift || 0)

					// 计算高度和Y坐标（预留底部 gap，避免与 X 轴重叠）
					const bottomGap = this.mergedBarConfig.bottomGap || 6
					const effectiveGraphHeight = Math.max(0, graphHeight - bottomGap)
					const valueClamped = value === undefined || value === null ? 0 : value
					const height = (valueClamped - minValue) / yRange * effectiveGraphHeight
					const x = centerX - singleBarWidth / 2
					const y = padding.top + (graphHeight - bottomGap) - height

					// 存储 index 以便 tooltip 命中时能对应到 label
					this.barRects[datasetIndex].push({ x, y, width: singleBarWidth, height, value: valueClamped, centerX, index: i })
				})
			})

			// 修复首列被半隐藏的问题：始终确保最左侧柱与 Y 轴之间保留 leftGap（兼顾缩放和平移）
			if (this.barRects && this.barRects.length > 0) {
				let leftMost = Infinity
				let maxBarWidth = 0
				this.barRects.forEach((rectArr)=> {
					if (!rectArr || rectArr.length === 0) return
					for (let r = 0; r < rectArr.length; r++) {
						const rect = rectArr[r]
						if (rect && typeof rect.x === 'number') leftMost = Math.min(leftMost, rect.x)
						if (rect && typeof rect.width === 'number') maxBarWidth = Math.max(maxBarWidth, rect.width)
					}
				})

				const halfBar = Math.ceil((maxBarWidth || (this.mergedBarConfig.width || 8)) / 2)
				const desiredLeft = (padding.left || 0) + (this.mergedBarConfig.leftGap || 2) + halfBar
				if (leftMost !== Infinity && leftMost < desiredLeft) {
					const delta = desiredLeft - leftMost
					// 如果用户当前在拖拽或正在双指缩放，则不要自动调整，这可能会影响手势感受
					if (this.isDragging || this.isPinching) {
						// 跳过自动调整，等待手势结束后由 constrainOffset 修正
					} else {
						if (this.scale === 1) {
							// 未缩放时，将持久偏移累加并直接平移 rects，保证下一次计算也会应用该偏移
							this.barShift = (this.barShift || 0) + delta
							this.barRects.forEach((rectArr)=> {
								if (!rectArr) return
								rectArr.forEach((rect)=> {
									if (!rect) return
									rect.x += delta
									rect.centerX = (rect.centerX || 0) + delta
								})
							})
							// 同步调整触摸缓存，防止手势判断异常
							this.lastTouchPos = { x: (this.lastTouchPos && this.lastTouchPos.x ? this.lastTouchPos.x : 0) + delta, y: this.lastTouchPos && this.lastTouchPos.y ? this.lastTouchPos.y : 0 }
							this.touchStartPos = { x: (this.touchStartPos && this.touchStartPos.x ? this.touchStartPos.x : 0) + delta, y: this.touchStartPos && this.touchStartPos.y ? this.touchStartPos.y : 0 }
						} else {
							// 在缩放状态下，不自动修改 offsetX，避免选中或重绘时导致跳动到边缘。
							// 缩放时的平移应由用户手势控制或由外部显式调用来调整。
							// 因此这里不做自动平移，仅保留 rects 当前计算值。
						}
					}
				}
			}
		},

		// 获取过渡中的数据 - 支持多折线
		getTransitionData(datasetIndex) {
			const oldDataset = this.oldData[datasetIndex] || []
			const newData = this.chartData.datasets[datasetIndex].data
			const progress = this.transitionProgress

			const eased = this.easeOutQuad(progress)

			const result = []
			const minLength = Math.min(oldDataset.length, newData.length)

			for (let i = 0; i < minLength; i++) {
				const oldValue = oldDataset[i]
				const newValue = newData[i]
				result.push(oldValue + (newValue - oldValue) * eased)
			}

			if (newData.length > oldDataset.length) {
				for (let i = oldDataset.length; i < newData.length; i++) {
					const lastOldValue = oldDataset[oldDataset.length - 1]
					const newValue = newData[i]
					result.push(lastOldValue + (newValue - lastOldValue) * eased)
				}
			}

			return result
		},

		// 绘制线条
		drawLine(points, dataset, progress) {
			if (points.length === 0) return

			const visiblePointCount = Math.max(1, Math.ceil(points.length * progress))
			const visiblePoints = points.slice(0, visiblePointCount)

			if (visiblePoints.length < 2) return

			this.ctx.save()

			let opacity = 1
			if (visiblePointCount === 1) {
				const firstPointProgress = progress * points.length
				opacity = Math.min(1, firstPointProgress)
			}

			this.ctx.strokeStyle = dataset.color
			this.ctx.lineWidth = 2
			this.ctx.lineCap = 'round'
			this.ctx.lineJoin = 'round'

			this.ctx.globalAlpha = opacity

			this.drawSmoothCurve(visiblePoints)

			this.ctx.restore()
		},

		// 绘制平滑曲线 - 使用 Catmull-Rom 样条曲线，确保曲线经过所有数据点
		drawSmoothCurve(points) {
			if (points.length < 2) return

			this.ctx.beginPath()
			this.ctx.moveTo(points[0].x, points[0].y)

			if (points.length === 2) {
				// 只有2个点时，绘制直线
				this.ctx.lineTo(points[1].x, points[1].y)
			} else {
				// 3个点及以上，使用 Catmull-Rom 样条曲线
				for (let i = 0; i < points.length - 1; i++) {
					const p0 = points[Math.max(0, i - 1)]
					const p1 = points[i]
					const p2 = points[i + 1]
					const p3 = points[Math.min(points.length - 1, i + 2)]

					// Catmull-Rom 转贝塞尔曲线的控制点
					const cp1x = p1.x + (p2.x - p0.x) / 6
					const cp1y = p1.y + (p2.y - p0.y) / 6
					const cp2x = p2.x - (p3.x - p1.x) / 6
					const cp2y = p2.y - (p3.y - p1.y) / 6

					this.ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y)
				}
			}

			this.ctx.stroke()
		},

		// 绘制数据点
		drawPoints(points, dataset, progress) {
			const visiblePoints = Math.ceil(points.length * progress)

			for (let i = 0; i < visiblePoints; i++) {
				const pointProgress = Math.min(
					1,
					(progress - i / points.length) * points.length * 1.5
				)

				if (pointProgress > 0) {
					this.ctx.save()

					if (i === 0) {
						const firstPointProgress = progress * points.length
						this.ctx.globalAlpha = Math.min(1, firstPointProgress)
					}

					const gradient = this.ctx.createRadialGradient(
						points[i].x,
						points[i].y,
						0,
						points[i].x,
						points[i].y,
						3 * pointProgress
					)
					gradient.addColorStop(0, dataset.gradientStart)
					gradient.addColorStop(0.7, dataset.color)
					gradient.addColorStop(1, dataset.gradientEnd)

					this.ctx.fillStyle = gradient
					this.ctx.beginPath()
					this.ctx.arc(
						points[i].x,
						points[i].y,
						3 * pointProgress,
						0,
						2 * Math.PI
					)
					this.ctx.fill()

					this.ctx.strokeStyle = '#ffffff'
					this.ctx.lineWidth = 1.5
					this.ctx.beginPath()
					this.ctx.arc(
						points[i].x,
						points[i].y,
						3 * pointProgress,
						0,
						2 * Math.PI
					)
					this.ctx.stroke()

					this.ctx.restore()
				}
			}
		},

		// 绘制Tooltip指示线 - 支持多折线
		drawTooltipLine(pointIndex) {
			if (this.points.length === 0) return

			// 获取所有数据集在该索引处的点
			const allPoints = this.points.map(datasetPoints=> datasetPoints[pointIndex]).filter(p=> p)

			if (allPoints.length === 0) return

			// 如果不是柱状图，绘制垂直指示线及高亮圆点
			if (this.chartType !== 'bar') {
				// 绘制垂直指示线（使用第一个点的x坐标）
				const firstPoint = allPoints[0]
				this.ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)'
				this.ctx.lineWidth = 1
				this.ctx.setLineDash([5, 3])

				this.ctx.beginPath()
				this.ctx.moveTo(firstPoint.x, this.mergedPadding.top)
				this.ctx.lineTo(
					firstPoint.x,
					this.mergedPadding.top + (this.chartHeight - this.mergedPadding.top - this.mergedPadding.bottom)
				)
				this.ctx.stroke()

				this.ctx.setLineDash([])

				// 为每个数据集绘制高亮点
				allPoints.forEach((point, datasetIndex)=> {
					const dataset = this.chartData.datasets[datasetIndex]

					// 外圈光晕
					this.ctx.strokeStyle = this.hexToRgba(dataset.color, 0.2)
					this.ctx.lineWidth = 4
					this.ctx.beginPath()
					this.ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI)
					this.ctx.stroke()

					// 渐变填充
					const gradient = this.ctx.createRadialGradient(
						point.x, point.y, 0,
						point.x, point.y, 4
					)
					gradient.addColorStop(0, dataset.gradientStart)
					gradient.addColorStop(1, dataset.gradientEnd)

					this.ctx.fillStyle = gradient
					this.ctx.beginPath()
					this.ctx.arc(point.x, point.y, 4, 0, 2 * Math.PI)
					this.ctx.fill()

					// 白色边框
					this.ctx.strokeStyle = '#ffffff'
					this.ctx.lineWidth = 1.5
					this.ctx.beginPath()
					this.ctx.arc(point.x, point.y, 4, 0, 2 * Math.PI)
					this.ctx.stroke()
				})
			}

			// 如果是柱状图，绘制高亮柱（半透明外圈）
			if (this.chartType === 'bar') {
				// 计算该组的最小/最大X用于包裹高亮
				const groupRects = []
				this.chartData.datasets.forEach((d, idx)=> {
					if (this.hiddenDatasets[idx]) return
					const rect = this.barRects[idx] && this.barRects[idx][pointIndex] || null
					if (rect) groupRects.push({ rect, dataset: d, datasetIndex: idx })
				})

				if (groupRects.length > 0) {
					const left = Math.min(...groupRects.map(g=> g.rect.x)) - 2
					const right = Math.max(...groupRects.map(g=> g.rect.x + g.rect.width)) + 2
					const padding = this.mergedPadding
					const graphHeight = this.chartHeight - padding.top - padding.bottom
					const highlightTop = padding.top
					const highlightHeight = graphHeight
					const highlightWidth = right - left

					// 使用第一个可见数据集的颜色作为阴影色
					const color = groupRects[0].dataset.color || '#000'

					// 解析 borderRadius，支持 number 或对象
					const cfg = this.mergedBarConfig || {}
					const br = cfg.borderRadius
					let hr = 4
					if (typeof br === 'number') hr = Math.min(br, 12)
					else if (br && typeof br === 'object') hr = Math.min(br.top || br.topLeft || 4, 12)

					this.ctx.save()
					this.ctx.shadowColor = this.hexToRgba(color, 0.28)
					this.ctx.shadowBlur = 18
					this.ctx.fillStyle = this.hexToRgba(color, 0.08)
					this.ctx.beginPath()
					this.drawRoundedRect(left - (cfg.leftGap || 0), highlightTop, highlightWidth + (cfg.leftGap || 0) * 2, highlightHeight, br || hr)
					this.ctx.fill()
					this.ctx.restore()

					// 描边以增强可见性
					this.ctx.strokeStyle = this.hexToRgba(color, 0.12)
					this.ctx.lineWidth = 1
					this.ctx.beginPath()
					this.drawRoundedRect(left - (cfg.leftGap || 0), highlightTop, highlightWidth + (cfg.leftGap || 0) * 2, highlightHeight, br || hr)
					this.ctx.stroke()

					// 在阴影之上重绘该组柱体，保持柱体清晰
					groupRects.forEach(g=> {
						const r = g.rect
						const ds = this.chartData.datasets[g.datasetIndex] || {}
						if (!r) return
						this.ctx.save()
						this.ctx.beginPath()
						// 使用与 drawBars 相同的 borderRadius 配置
						this.drawRoundedRect(r.x, r.y, r.width, r.height, br || hr)
						try {
							const gfill = this.ctx.createLinearGradient(r.x, r.y, r.x, r.y + r.height)
							gfill.addColorStop(0, ds.gradientStart || ds.color)
							gfill.addColorStop(1, ds.gradientEnd || ds.color)
							this.ctx.fillStyle = gfill
						} catch (err) {
							this.ctx.fillStyle = ds.color || '#2f95ff'
						}
						this.ctx.fill()
						if (ds.borderColor) {
							this.ctx.lineWidth = ds.borderWidth || 1
							this.ctx.strokeStyle = ds.borderColor
							this.ctx.beginPath()
							this.drawRoundedRect(r.x, r.y, r.width, r.height, br || hr)
							this.ctx.stroke()
						}
						this.ctx.restore()
					})
				}
			}
		},

		// 颜色转换
		hexToRgba(hex, opacity) {
			const r = parseInt(hex.slice(1, 3), 16)
			const g = parseInt(hex.slice(3, 5), 16)
			const b = parseInt(hex.slice(5, 7), 16)
			return `rgba(${r}, ${g}, ${b}, ${opacity})`
		},

		// 绘制圆角矩形的路径（支持单值或每角对象半径），使用当前 this.ctx
		drawRoundedRect(x, y, width, height, radius) {
			// radius 可以为数字或对象 { topLeft, topRight, bottomRight, bottomLeft } 或 { top, bottom }
			let r = {
				topLeft: 0,
				topRight: 0,
				bottomRight: 0,
				bottomLeft: 0
			}
			if (typeof radius === 'number') {
				r.topLeft = r.topRight = r.bottomRight = r.bottomLeft = Math.max(0, radius)
			} else if (radius && typeof radius === 'object') {
				if (radius.top !== undefined || radius.bottom !== undefined) {
					r.topLeft = r.topRight = Math.max(0, radius.top || 0)
					r.bottomLeft = r.bottomRight = Math.max(0, radius.bottom || 0)
				} else {
					r.topLeft = Math.max(0, radius.topLeft || 0)
					r.topRight = Math.max(0, radius.topRight || 0)
					r.bottomRight = Math.max(0, radius.bottomRight || 0)
					r.bottomLeft = Math.max(0, radius.bottomLeft || 0)
				}
			}

			// 限制每个半径不超过 width/2 或 height/2
			const maxR = Math.min(width / 2, height / 2)
			r.topLeft = Math.min(r.topLeft, maxR)
			r.topRight = Math.min(r.topRight, maxR)
			r.bottomRight = Math.min(r.bottomRight, maxR)
			r.bottomLeft = Math.min(r.bottomLeft, maxR)

			this.ctx.moveTo(x + r.topLeft, y)
			this.ctx.lineTo(x + width - r.topRight, y)
			this.ctx.quadraticCurveTo(x + width, y, x + width, y + r.topRight)
			this.ctx.lineTo(x + width, y + height - r.bottomRight)
			this.ctx.quadraticCurveTo(x + width, y + height, x + width - r.bottomRight, y + height)
			this.ctx.lineTo(x + r.bottomLeft, y + height)
			this.ctx.quadraticCurveTo(x, y + height, x, y + height - r.bottomLeft)
			this.ctx.lineTo(x, y + r.topLeft)
			this.ctx.quadraticCurveTo(x, y, x + r.topLeft, y)
			this.ctx.closePath()
		},

		// 触摸开始
		handleTouchStart(e) {
			if (!this.mergedZoomConfig.enabled) {
				if (this.mergedTooltipConfig.enabled) {
					this.handleTooltip(e)
				}
				return
			}

			const touches = e.touches

			if (touches.length === 2) {
				this.isPinching = true
				this.isDragging = false
				this.showTooltip = false

				const distance = this.getTouchDistance(touches[0], touches[1])
				this.lastTouchDistance = distance

				const center = this.getTouchCenter(touches[0], touches[1])
				this.lastTouchCenter = center
			} else if (touches.length === 1) {
				this.isPinching = false
				this.lastTouchPos = { x: touches[0].x, y: touches[0].y }
				this.touchStartPos = { x: touches[0].x, y: touches[0].y }
				this.touchStartTime = Date.now()

				this.isDragging = false

				if (this.scale === 1 && this.mergedTooltipConfig.enabled) {
					this.handleTooltip(e)
				}
			}
		},

		// 触摸移动
		handleTouchMove(e) {
			if (!this.mergedZoomConfig.enabled) {
				if (this.mergedTooltipConfig.enabled) {
					this.handleTooltip(e)
				}
				return
			}

			const touches = e.touches

			if (touches.length === 2 && this.isPinching) {
				e.preventDefault && e.preventDefault()

				const distance = this.getTouchDistance(touches[0], touches[1])
				const center = this.getTouchCenter(touches[0], touches[1])

				const scaleChange = distance / this.lastTouchDistance
				let newScale = this.scale * scaleChange

				newScale = Math.max(this.mergedZoomConfig.minScale, Math.min(this.mergedZoomConfig.maxScale, newScale))

				const scaleRatio = newScale / this.scale

				this.offsetX = this.offsetX * scaleRatio
				this.offsetY = 0

				this.scale = newScale

				this.constrainOffset()

				this.lastTouchDistance = distance
				this.lastTouchCenter = center

				this.drawChart()
			} else if (touches.length === 1) {
				const touch = touches[0]
				const deltaX = Math.abs(touch.x - this.touchStartPos.x)
				const deltaY = Math.abs(touch.y - this.touchStartPos.y)

				const dragThreshold = 5
				if (!this.isDragging && (deltaX > dragThreshold || deltaY > dragThreshold)) {
					if (this.scale !== 1) {
						this.isDragging = true
						this.showTooltip = false
					}
				}

				if (this.isDragging) {
					e.preventDefault && e.preventDefault()

					const moveX = touch.x - this.lastTouchPos.x

					this.offsetX += moveX

					this.constrainOffset()

					this.lastTouchPos = { x: touch.x, y: touch.y }

					this.drawChart()
				} else {
					if (this.mergedTooltipConfig.enabled) {
						this.handleTooltip(e)
					}
				}
			}
		},

		// 触摸结束
		handleTouchEnd(e) {
			const touches = e.touches

			if (touches.length === 0) {
				const touchEndTime = Date.now()
				const touchDuration = touchEndTime - this.touchStartTime
				const deltaX = Math.abs(this.lastTouchPos.x - this.touchStartPos.x)
				const deltaY = Math.abs(this.lastTouchPos.y - this.touchStartPos.y)

				const isClick = touchDuration < 300 && deltaX < 10 && deltaY < 10

				if (this.isDragging || this.isPinching) {
					this.constrainOffset()
					this.drawChart()
				} else if (isClick && this.mergedTooltipConfig.enabled) {
					const mockEvent = {
						touches: [{
							x: this.touchStartPos.x,
							y: this.touchStartPos.y,
							clientX: this.touchStartPos.x
						}]
					}
					this.handleTooltip(mockEvent)
				}

				this.isPinching = false
				this.isDragging = false
			} else if (touches.length === 1) {
				if (this.isPinching) {
					this.constrainOffset()
					this.drawChart()
				}
				this.isPinching = false
				this.lastTouchPos = { x: touches[0].x, y: touches[0].y }
			}
		},

		// 计算两个触摸点之间的距离
		getTouchDistance(touch1, touch2) {
			const dx = touch2.x - touch1.x
			const dy = touch2.y - touch1.y
			return Math.sqrt(dx * dx + dy * dy)
		},

		// 计算两个触摸点的中心点
		getTouchCenter(touch1, touch2) {
			return {
				x: (touch1.x + touch2.x) / 2,
				y: (touch1.y + touch2.y) / 2
			}
		},

		// 处理Tooltip - 支持多折线
		handleTooltip(e) {
			if (this.points.length === 0 || this.points[0].length === 0) {
				return
			}

			if (!this.canvasRect || !this.wrapperRect) {
				this.updateCanvasRect()
				// 延迟重试
				setTimeout(()=> {
					if (this.canvasRect && this.wrapperRect) {
						this.handleTooltip(e)
					}
				}, 100)
				return
			}

			const touch = e.touches[0]
			const canvasRect = this.canvasRect
			const wrapperRect = this.wrapperRect

			let touchCanvasX, touchCanvasY

			if (this.canvas) {
				// 小程序 canvas 触摸事件直接给出 canvas 坐标
				touchCanvasX = touch.x
				touchCanvasY = touch.y
			} else {
				const touchX = touch.x || touch.clientX
				const touchY = touch.y || touch.clientY
				touchCanvasX = touchX - canvasRect.left
				touchCanvasY = touchY - canvasRect.top
			}

			// 当为柱状图时，使用 barRects 命中检测（优先）
			if (this.chartType === 'bar') {
				let foundIndex = -1
				let foundCenterX = null
				const labelCount = this.chartData.labels.length

				for (let ds = 0; ds < this.barRects.length; ds++) {
					if (this.hiddenDatasets[ds]) continue
					const rects = this.barRects[ds] || []
					for (let r = 0; r < rects.length; r++) {
						const rect = rects[r]
						if (!rect) continue
						// 命中检测：在矩形内
						if (touchCanvasX >= rect.x && touchCanvasX <= rect.x + rect.width && touchCanvasY >= rect.y && touchCanvasY <= rect.y + rect.height) {
							foundIndex = rect.index
							foundCenterX = rect.centerX
							break
						}
					}
					if (foundIndex !== -1) break
				}

				// 如果没有直接命中，则按X距离最近的组来触发（阈值为 mergedTooltipConfig.threshold）
				if (foundIndex === -1) {
					let minDist = Infinity
					let closest = -1
					for (let idx = 0; idx < labelCount; idx++) {
						// 从第一个可见数据集获取对应 rect 作为代表
						let repRect = null
						for (let ds = 0; ds < this.barRects.length; ds++) {
							if (this.hiddenDatasets[ds]) continue
							const r = this.barRects[ds] && this.barRects[ds][idx] || null
							if (r) { repRect = r; break }
						}
						if (!repRect) continue
						const dist = Math.abs(repRect.centerX - touchCanvasX)
						if (dist < minDist) { minDist = dist; closest = idx }
					}
					if (minDist < this.mergedTooltipConfig.threshold) {
						foundIndex = closest
						// find representative centerX
						for (let ds = 0; ds < this.barRects.length; ds++) {
							const r = this.barRects[ds] && this.barRects[ds][foundIndex] || null
							if (r) { foundCenterX = r.centerX; break }
						}
					}
				}

				if (foundIndex !== -1) {
					this.showTooltip = true
					this.currentPointIndex = foundIndex
					this.updateTooltipData(foundIndex)

					// 计算 tooltip 位置：以该组中最高的柱为基准
					const groupRects = []
					this.chartData.datasets.forEach((d, dsIdx)=> {
						if (this.hiddenDatasets[dsIdx]) return
						const r = this.barRects[dsIdx] && this.barRects[dsIdx][foundIndex] || null
						if (r) groupRects.push({ rect: r, dataset: d })
					})

					if (groupRects.length > 0) {
						const topY = Math.min(...groupRects.map(g=> g.rect.y))
						const repCenterX = foundCenterX || groupRects[0].rect.centerX
						let tooltipX = canvasRect.left - wrapperRect.left + repCenterX
						let tooltipY = canvasRect.top - wrapperRect.top + topY - 100

						const tooltipWidth = 120
						const tooltipHeight = 120
						const wrapperWidth = wrapperRect.width
						const wrapperHeight = wrapperRect.height

						if (tooltipX - tooltipWidth < 0) tooltipX = tooltipWidth
						else if (tooltipX + tooltipWidth > wrapperWidth) tooltipX = wrapperWidth - tooltipWidth

						if (tooltipY < 0) tooltipY = topY + 20
						else if (tooltipY + tooltipHeight > wrapperHeight) tooltipY = wrapperHeight - tooltipHeight

						this.tooltipPosition.x = tooltipX
						this.tooltipPosition.y = tooltipY
					}

					this.drawChart()
					return
				}
			}

			// 非柱状图或未命中的回退逻辑：使用折线的点距离判断（原有逻辑）
			let minDistance = Infinity
			let closestIndex = -1

			// 如果没有点数据则直接返回
			if (!this.points || !this.points[0]) return

			this.points[0].forEach((point, i)=> {
				const distance = Math.abs(point.x - touchCanvasX)

				if (distance < minDistance) {
					minDistance = distance
					closestIndex = i
				}
			})

			const threshold = this.mergedTooltipConfig.threshold
			if (closestIndex !== -1 && minDistance < threshold) {
				this.showTooltip = true
				this.currentPointIndex = closestIndex
				this.updateTooltipData(closestIndex)

				const allYs = this.points.map(datasetPoints=> datasetPoints[closestIndex].y)
				const topY = Math.min(...allYs)

				const firstPoint = this.points[0][closestIndex]
				let tooltipX = canvasRect.left - wrapperRect.left + firstPoint.x
				let tooltipY = canvasRect.top - wrapperRect.top + topY - 100

				const tooltipWidth = 120
				const tooltipHeight = 120
				const wrapperWidth = wrapperRect.width
				const wrapperHeight = wrapperRect.height

				if (tooltipX - tooltipWidth < 0) {
					tooltipX = tooltipWidth
				} else if (tooltipX + tooltipWidth > wrapperWidth) {
					tooltipX = wrapperWidth - tooltipWidth
				}

				if (tooltipY < 0) {
					tooltipY = topY + 20
				} else if (tooltipY + tooltipHeight > wrapperHeight) {
					tooltipY = wrapperHeight - tooltipHeight
				}

				this.tooltipPosition.x = tooltipX
				this.tooltipPosition.y = tooltipY

				this.drawChart()
			} else {
				if (this.showTooltip) {
					this.showTooltip = false
					this.currentPointIndex = -1
					this.drawChart()
				}
			}
		},

		// 限制平移范围
		constrainOffset() {
			const { chartWidth } = this
			const padding = this.mergedPadding
			const graphWidth = chartWidth - padding.left - padding.right

			if (this.scale === 1) {
				this.offsetX = 0
				this.offsetY = 0
				return
			}

			const centerX = padding.left + graphWidth / 2

			const maxOffsetX = (padding.left - centerX) * (1 - this.scale)
			const minOffsetX = (padding.left + graphWidth - centerX) * (1 - this.scale)

			this.offsetX = Math.max(minOffsetX, Math.min(maxOffsetX, this.offsetX))

			this.offsetY = 0
		},

		// 绘制柱状图（接收某个数据集的矩形数组）
		drawBars(rects, dataset, progress) {
			if (!rects || rects.length === 0) return

			const labelCount = this.chartData && this.chartData.labels ? this.chartData.labels.length : 0
			const animateMode = this.mergedBarConfig && this.mergedBarConfig.animateMode ? this.mergedBarConfig.animateMode : 'all'
			const staggerFraction = this.mergedBarConfig && this.mergedBarConfig.staggerDelayFraction ? this.mergedBarConfig.staggerDelayFraction : 0.02
			const borderRadius = this.mergedBarConfig && this.mergedBarConfig.borderRadius ? this.mergedBarConfig.borderRadius : 0
			const totalStagger = Math.max(0, Math.min(0.8, (labelCount - 1) * staggerFraction))

			this.ctx.save()
			rects.forEach((rect, i)=> {
				let barProgress = Math.max(0, Math.min(1, progress))
				if (animateMode === 'stagger' && typeof rect.index === 'number') {
					const start = rect.index * staggerFraction
					const denom = 1 - totalStagger
					if (denom <= 0) {
						barProgress = progress
					} else {
						barProgress = (progress - start) / denom
					}
					barProgress = Math.max(0, Math.min(1, barProgress))
					// 使用缓动函数以获得更自然的生长
					barProgress = this.easeOutQuad(barProgress)
				} else {
					barProgress = this.easeOutQuad(barProgress)
				}
				if (barProgress <= 0) return

				// 计算动画高度和绘制 Y（底部锚点）
				const baseY = rect.y + rect.height // 实际底部坐标（已考虑 bottomGap）
				const animatedHeight = rect.height * barProgress
				const drawY = baseY - animatedHeight

				// 渐变填充（从 gradientStart 到 gradientEnd），在动画过程中逐步显示高度
				try {
					const g = this.ctx.createLinearGradient(rect.x, drawY, rect.x, drawY + animatedHeight)
					g.addColorStop(0, dataset.gradientStart || dataset.color)
					g.addColorStop(1, dataset.gradientEnd || dataset.color)
					this.ctx.fillStyle = g
				} catch (err) {
					this.ctx.fillStyle = dataset.color
				}

				this.ctx.globalAlpha = 1
				// 使用圆角矩形进行填充和描边
				this.ctx.beginPath()
				this.drawRoundedRect(rect.x, drawY, rect.width, animatedHeight, borderRadius)
				this.ctx.fill()

				// 描边（轻描）
				this.ctx.strokeStyle = this.hexToRgba(dataset.color, 0.12)
				this.ctx.lineWidth = 1
				this.ctx.beginPath()
				this.drawRoundedRect(rect.x, drawY, rect.width, animatedHeight, borderRadius)
				this.ctx.stroke()
			})
			this.ctx.restore()
		},
	}
}
</script>

<style scoped>
/* 图表容器 */
.chart-container {
	/* display: flex;
	flex-direction: column;
	width: 100%; */
}

.chart-wrapper {
	position: relative;
	background: #ffffff;
	border-radius: 32rpx;
	/* overflow: hidden; */
	flex: 1;
	/* #ifdef MP-TOUTIAO */
	height: 250px;
	/* #endif */
}

.chart-loading {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	background: #ffffff;
	z-index: 10;
}

.loading-text {
	font-size: 28rpx;
	color: #999999;
}

canvas {
	display: block;
}

.tooltip {
	position: absolute;
	background: rgba(255, 255, 255, 0.95);
	border-radius: 16rpx;
	padding: 24rpx;
	box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
	pointer-events: none;
	z-index: 10;
	min-width: 240rpx;
	transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
	            transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),
	            left 0.2s cubic-bezier(0.4, 0, 0.2, 1),
	            top 0.2s cubic-bezier(0.4, 0, 0.2, 1);
	will-change: transform, opacity;
}

.tooltip-label {
	font-size: 24rpx;
	color: #666;
	margin-bottom: 16rpx;
	font-weight: 500;
    white-space: nowrap;
}

.tooltip-item {
	display: flex;
	align-items: center;
	margin-bottom: 8rpx;
	font-size: 26rpx;
	color: #333;
    white-space: nowrap;
}

.tooltip-item:last-child {
	margin-bottom: 0;
}

.tooltip-dot {
	width: 16rpx;
	height: 16rpx;
	border-radius: 50%;
	margin-right: 12rpx;
}

.zoom-controls {
	position: absolute;
	top: 24rpx;
	right: 24rpx;
	z-index: 5;
	animation: fadeInDown 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes fadeInDown {
	from {
		opacity: 0;
		transform: translateY(-16rpx);
	}
	to {
		opacity: 1;
		transform: translateY(0);
	}
}

@keyframes fadeInSlide {
	from {
		opacity: 0;
		transform: translateY(-20rpx);
	}
	to {
		opacity: 1;
		transform: translateY(0);
	}
}

.reset-btn {
	background: #ffffff;
	color: #666666;
	border: 2rpx solid #e0e0e0;
	border-radius: 16rpx;
	padding: 12rpx 28rpx;
	font-size: 24rpx;
	font-weight: 500;
	box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.06);
	transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
	cursor: pointer;
	display: flex;
	align-items: center;
	gap: 8rpx;
}

.reset-btn:hover {
	background: #fafafa;
	border-color: #d0d0d0;
	box-shadow: 0 6rpx 12rpx rgba(0, 0, 0, 0.1);
	color: #333333;
}

.reset-btn:active {
	transform: translateY(2rpx);
	box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.08);
}

/* Legend 图例样式 */
.legend {
	display: flex;
	align-items: center;
	justify-content: center;
	flex-wrap: wrap;
	gap: 32rpx;
	padding: 10rpx 40rpx;
	background: #ffffff;
	/* animation: fadeInSlide 0.3s ease-out; */
}

.legend-top {
	border-radius: 32rpx 32rpx 0 0;
	box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.08);
}

.legend-bottom {
	border-radius: 0 0 32rpx 32rpx;
}

.legend-item {
	display: flex;
	align-items: center;
	gap: 16rpx;
	padding: 12rpx 24rpx;
	border-radius: 12rpx;
	cursor: pointer;
	transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
	user-select: none;
	background: rgba(0, 0, 0, 0.02);
}

.legend-item:hover {
	background: rgba(0, 0, 0, 0.04);
}

.legend-item:active {
	transform: scale(0.96);
	background: rgba(0, 0, 0, 0.06);
}

.legend-item-disabled {
	opacity: 0.35;
}

.legend-item-disabled .legend-label {
	text-decoration: line-through;
}

.legend-color {
	width: 20rpx;
	height: 20rpx;
	border-radius: 4rpx;
	flex-shrink: 0;
	box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.2);
}

.legend-label {
	font-size: 26rpx;
	font-weight: 500;
	color: #333333;
	white-space: nowrap;
}
</style>
