Files
2026-03-16 10:37:46 +08:00

804 lines
28 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="ak-charts-container">
<canvas :id="canvasId" :canvas-id="canvasId" class="ak-charts-canvas"></canvas>
</view>
</template>
<script lang="uts">
import type { ChartSeries,ChartOption,ChartType,Margin } from '../interface.uts';
export default {
name: 'AkCharts',
props: {
option: {
type: UTSJSONObject, // 修正为 Object避免类型错误
required: true
},
canvasId: {
type: String,
default: 'ak-charts-canvas'
}
},
data() {
return {
canvas: null as UniCanvasElement | null,
canvasContext: null as CanvasContext | null,
renderingContext: null as CanvasRenderingContext2D | null,
dpr: 1,
ctxReady: false,
canvasWidth: 0,
canvasHeight: 0
}
},
watch: {
option: {
handler() {
// 类型检查,确保 option 是对象且有 type/data 字段
if (
this.ctxReady ) {
let aopt:ChartOption = this.toChartOption(this.option)
this.drawChart(aopt);
}
},
deep: true,
immediate: true
}
},
mounted() {
console.log('ak charts')
this.dpr = typeof uni.getWindowInfo === "function" ? uni.getWindowInfo().pixelRatio : 1;
this.$nextTick(() => {
console.log(this.option)
let aopt:ChartOption = this.toChartOption(this.option)
uni.createCanvasContextAsync({
id: this.canvasId,
component: this,
success: (context: CanvasContext) => {
this.canvasContext = context;
this.renderingContext = context.getContext('2d');
this.canvas = this.renderingContext!.canvas;
this.hidpi(this.canvas!);
this.canvasWidth = this.canvas!.width;
this.canvasHeight = this.canvas!.height;
this.ctxReady = true;
this.drawChart(aopt);
}
});
});
uni.$on('ak-charts-render', (payload: any) => {
let option: ChartOption | null = null;
let canvasId: string = '';
let isValid = false;
// 尝试将 payload 转为 UTSJSONObject 处理
let utsPayload: UTSJSONObject | null = null;
try {
// UTSJSONObject 通常有 get 方法
if (payload != null ) {
utsPayload = payload as UTSJSONObject;
}
} catch (e) {
utsPayload = null;
}
if (utsPayload!=null) {
const opt = utsPayload.get('option');
const cid = utsPayload.get('canvasId');
if (opt != null && cid != null) {
option = opt as ChartOption;
canvasId = cid as string;
isValid = true;
}
}
if (isValid && canvasId === this.canvasId && this.ctxReady) {
this.drawChart(option!);
}
});
},
methods: {
hidpi(canvas: UniCanvasElement) {
const context = canvas.getContext("2d")!;
const dpr = this.dpr;
canvas.width = canvas.offsetWidth * dpr;
canvas.height = canvas.offsetHeight * dpr;
context.scale(dpr, dpr);
},
// toChartOption 方法
toChartOption(obj: UTSJSONObject): ChartOption {
// 支持 series
if (Array.isArray(obj.series)) {
return {
type: obj.type as ChartType,
series: Array.isArray(obj.series) ? (obj.series as ChartSeries[]) : null,
labels: Array.isArray(obj.labels) ? (obj.labels as string[]) : []
}
}
return {
type: obj.type as ChartType,
data: Array.isArray(obj.data) ? (obj.data as number[]) : [],
labels: Array.isArray(obj.labels) ? (obj.labels as string[]) : [],
color: typeof obj.color === 'string' ? obj.color as string : ''
}
},
drawChart(option: ChartOption) {
console.log('now drawing',option)
if (this.ctxReady != true || this.renderingContext == null) return;
if (option == null || option.data == null) {
return;
}
const ctx = this.renderingContext!;
ctx.clearRect(0, 0, this.canvasWidth / this.dpr, this.canvasHeight / this.dpr);
if (option.type === 'bar') {
this.drawBar(ctx, option);
} else if (option.type === 'line') {
this.drawLine(ctx, option);
} else if (option.type === 'pie') {
this.drawPie(ctx, option);
} else if (option.type === 'doughnut') {
this.drawDoughnut(ctx, option);
} else if (option.type === 'radar') {
this.drawRadar(ctx, option);
}
},
drawBar(ctx: CanvasRenderingContext2D, option: ChartOption) {
const data = option.data != null ? option.data : ([] as number[]);
const color = option.color != null ? option.color : '#2196f3';
const labels = option.labels != null ? option.labels : ([] as string[]);
if (data == null || data.length == null || data.length === 0) return;
// 使用实际画布尺寸而不是固定值
const width = this.canvasWidth / this.dpr;
const height = this.canvasHeight / this.dpr;
const margin: Margin = {
top: 20,
right: 20,
bottom: 40,
left: 40
};
const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;
// 找出最大值,确保图表高度合适
const max = Math.max(...(data as number[]), 1);
// 调整条形宽度,确保条形之间有合适的间距
const barWidth = (chartWidth / (data.length as number)) * 0.7; // 条形宽度占用70%的可用空间
const spacing = (chartWidth / (data.length as number)) * 0.3; // 剩余30%用作间距
ctx.save();
// 绘制Y轴
ctx.beginPath();
ctx.strokeStyle = '#ccc';
ctx.moveTo(margin.left, margin.top);
ctx.lineTo(margin.left, height - margin.bottom);
ctx.stroke();
// 绘制X轴
ctx.beginPath();
ctx.moveTo(margin.left, height - margin.bottom);
ctx.lineTo(width - margin.right, height - margin.bottom);
ctx.stroke();
// 绘制数据条形
data.forEach((v, i) => {
// 计算条形位置
const x = margin.left + (chartWidth / data.length) * i + spacing / 2;
const valueHeight = (v / max) * chartHeight;
const y = height - margin.bottom - valueHeight;
// 使用指定颜色填充条形 - 修复类型错误
ctx.fillStyle = color as any;
ctx.fillRect(x, y, barWidth, valueHeight);
// 在条形上方显示数值
ctx.font = '12px sans-serif';
ctx.fillStyle = '#333' as any;
ctx.textAlign = 'center';
ctx.fillText(v.toString(), x + barWidth / 2, y - 5);
// 显示标签
if (labels != null && labels.length > i && labels[i] != null && labels[i] != '') {
ctx.font = '14px sans-serif';
ctx.fillStyle = '#666' as any;
ctx.textAlign = 'center';
ctx.fillText(labels[i] as string, x + barWidth / 2, height - margin.bottom + 20);
}
});
ctx.restore();
},
drawLine(ctx: CanvasRenderingContext2D, option: ChartOption) {
// 支持 series
if (option.series != null) {
const labels = option.labels != null ? option.labels : ([] as string[]);
// 计算画布尺寸、margin、chartWidth/chartHeight、max
const width = this.canvasWidth / this.dpr;
const height = this.canvasHeight / this.dpr;
const margin: Margin = {
top: 20,
right: 20,
bottom: 40,
left: 40
};
const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;
let max = 1;
option.series?.forEach(s => {
if (s.data != null && s.data.length > 0) {
const m = Math.max(...(s.data as number[]));
if (m > max) max = m;
}
});
// 定义默认颜色
const defaultColors = [
'#4caf50', '#f44336', '#2196f3', '#ff9800', '#9c27b0', '#009688'
];
ctx.save();
// 绘制Y轴
ctx.beginPath();
ctx.strokeStyle = '#ccc' as any;
ctx.moveTo(margin.left, margin.top);
ctx.lineTo(margin.left, height - margin.bottom);
ctx.stroke();
// 绘制X轴
ctx.beginPath();
ctx.strokeStyle = '#ccc' as any;
ctx.moveTo(margin.left, height - margin.bottom);
ctx.lineTo(width - margin.right, height - margin.bottom);
ctx.stroke();
// 绘制每条折线
option.series?.forEach((s, si) => {
const color = s.color != null ? s.color : defaultColors[si % defaultColors.length];
ctx.beginPath();
ctx.strokeStyle = color as any;
ctx.lineWidth = 2;
if (s.data != null && s.data.length > 0) {
const len = s.data.length;
s.data.forEach((v, i) => {
const denom = (len - 1) !== 0 ? (len - 1) : 1;
const x = margin.left + (chartWidth / denom) * i;
const y = height - margin.bottom - (v / max) * chartHeight;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
}
ctx.stroke();
// 绘制点
if (s.data != null && s.data.length > 0) {
const len = s.data.length;
s.data.forEach((v, i) => {
const denom = (len - 1) !== 0 ? (len - 1) : 1;
const x = margin.left + (chartWidth / denom) * i;
const y = height - margin.bottom - (v / max) * chartHeight;
ctx.beginPath();
ctx.fillStyle = color as any;
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fill();
});
}
});
ctx.restore();
return;
}
// ...原有单组数据逻辑
const data = option.data != null ? option.data : ([] as number[]);
const color = option.color != null ? option.color : '#2196f3';
const labels = option.labels != null ? option.labels : ([] as string[]);
if (data!!.length === 0) return;
// 使用实际画布尺寸而不是固定值
const width = this.canvasWidth / this.dpr;
const height = this.canvasHeight / this.dpr;
const margin: Margin = {
top: 20,
right: 20,
bottom: 40,
left: 40
};
const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;
// 找出最大值,确保图表高度合适
const max = Math.max(...data!!, 1);
ctx.save();
// 绘制Y轴
ctx.beginPath();
ctx.strokeStyle = '#ccc' as any;
ctx.moveTo(margin.left, margin.top);
ctx.lineTo(margin.left, height - margin.bottom);
ctx.stroke();
// 绘制X轴
ctx.beginPath();
ctx.strokeStyle = '#ccc' as any;
ctx.moveTo(margin.left, height - margin.bottom);
ctx.lineTo(width - margin.right, height - margin.bottom);
ctx.stroke();
// 绘制线图主体 - 先完成折线的绘制
ctx.beginPath();
ctx.strokeStyle = color as any;
ctx.lineWidth = 2;
data.forEach((v, i) => {
// 计算点的位置
const x = margin.left + (chartWidth / (data.length - 1 | 1)) * i;
const y = height - margin.bottom - (v / max) * chartHeight;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
// 先把线画出来
ctx.stroke();
// 然后再单独绘制数据点和标签
data.forEach((v, i) => {
const x = margin.left + (chartWidth / (data.length - 1 | 1)) * i;
const y = height - margin.bottom - (v / max) * chartHeight;
// 在数据点上绘制圆点
ctx.beginPath();
ctx.fillStyle = color as any;
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fill();
// 在点上方显示数值
ctx.font = '12px sans-serif';
ctx.fillStyle = '#333' as any;
ctx.textAlign = 'center';
ctx.fillText(v.toString(), x, y - 10);
// 显示标签
if (labels != null && labels.length > i && labels[i] != null && labels[i] != '') {
ctx.font = '14px sans-serif';
ctx.fillStyle = '#666' as any;
ctx.textAlign = 'center';
ctx.fillText(labels[i] as string, x, height - margin.bottom + 20);
}
});
ctx.restore();
},
drawPie(ctx: CanvasRenderingContext2D, option: ChartOption) {
const data = option.data != null ? option.data : ([] as number[]);
const labels = option.labels != null ? option.labels : ([] as string[]);
if (data!!.length === 0) return;
// 使用实际画布尺寸
const width = this.canvasWidth / this.dpr;
const height = this.canvasHeight / this.dpr;
// 计算饼图中心和半径
let centerXValue: number | null = option.centerX;
const centerX: number = centerXValue != null ? centerXValue : width / 2;
let centerYValue: number | null = option.centerY;
const centerY: number = centerYValue != null ? centerYValue : height / 2;
let radiusValue: number | null = option.radius;
const radius: number = radiusValue != null ? radiusValue : Math.min(width, height) / 2 - 40;
// 计算数据总和,用于计算每个扇区的角度
let sum = 0;
for (let i = 0; i < data!!.length; i++) {
sum += data[i];
}
// 默认颜色数组,如果没有提供足够的颜色
const defaultColors = [
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
'#FF9F40', '#8AC249', '#EA526F', '#49A6B7', '#C5D86D'
];
ctx.save();
// 绘制饼图
let startAngle: Double = (-Math.PI / 2) as Double; // 从12点钟方向开始
data!!.forEach((value, i) => {
// 计算扇区角度
const sliceAngle = 2 * Math.PI * (value / sum);
// 确定扇区颜色
let color: string = '';
if (Array.isArray(option.color)) {
// 如果提供了颜色数组,使用对应索引的颜色或循环使用
color = option.color.length > i ? option.color[i] : defaultColors[i % defaultColors.length];
} else if (typeof option.color === 'string') {
// 如果提供了单个颜色,使用色调变化
color = i === 0 ? option.color : this.shiftHue(option.color as string, i * 30);
} else {
// 使用默认颜色
color = defaultColors[i % defaultColors.length];
}
// 绘制扇区
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle);
ctx.closePath();
ctx.fillStyle = color as any;
ctx.fill();
// 绘制扇区边框
ctx.strokeStyle = "#fff" as any;
ctx.lineWidth = 2;
ctx.stroke();
// 计算标签位置 - 在扇区中间
const labelAngle = startAngle + sliceAngle / 2;
const labelRadius = radius * 0.7; // 标签位于半径的70%处
const labelX = centerX + Math.cos(labelAngle) * labelRadius;
const labelY = centerY + Math.sin(labelAngle) * labelRadius;
// 显示数值
const percentage = Math.round((value / sum) * 100);
ctx.font = 'bold 14px sans-serif';
ctx.fillStyle = '#fff' as any;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${percentage}%`, labelX, labelY);
// 更新起始角度为当前扇区的结束角度
startAngle += sliceAngle as Double;
});
// 绘制图例
if (labels != null && labels.length > 0) {
const legendX = centerX - radius - 10;
const legendY = centerY + radius + 20;
data.forEach((value, i) => {
if (labels != null && i < labels.length && labels[i] != null && labels[i] != '') {
// 确定图例颜色
let color: string = '';
if (Array.isArray(option.color)) {
color = option.color.length > i ? option.color[i] : defaultColors[i % defaultColors.length];
} else if (typeof option.color === 'string') {
color = i === 0 ? option.color : this.shiftHue(option.color as string, i * 30);
} else {
color = defaultColors[i % defaultColors.length];
}
// 绘制图例颜色方块
ctx.fillStyle = color as any;
ctx.fillRect(legendX, legendY + i * 25, 15, 15);
// 绘制图例文字
const percentage = Math.round((value / sum) * 100);
ctx.font = '14px sans-serif';
ctx.fillStyle = '#333' as any;
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(`${labels[i]} (${percentage}%)`, legendX + 25, legendY + i * 25 + 7.5);
}
});
}
ctx.restore();
},
drawDoughnut(ctx: CanvasRenderingContext2D, option: ChartOption) {
const data = option.data != null ? option.data : ([] as number[]);
const labels = option.labels != null ? option.labels : ([] as string[]);
if (data!!.length === 0) return;
// 使用实际画布尺寸
const width = this.canvasWidth / this.dpr;
const height = this.canvasHeight / this.dpr;
// 计算环形图中心和半径
let centerXValue: number | null = option.centerX;
const centerX: number = centerXValue != null ? centerXValue : width / 2;
let centerYValue: number | null = option.centerY;
const centerY: number = centerYValue != null ? centerYValue : height / 2;
let radiusValue: number | null = option.radius;
const outerRadius: number = radiusValue != null ? radiusValue : Math.min(width, height) / 2 - 40;
// 环形图特有的内圆半径默认为外圆半径的60%
let innerRadiusValue: number | null = option.innerRadius;
const innerRadius: number = innerRadiusValue != null ? innerRadiusValue : outerRadius * 0.6;
// 计算数据总和,用于计算每个扇区的角度
let sum = 0;
for (let i = 0; i < data!!.length; i++) {
sum += data[i];
}
// 默认颜色数组
const defaultColors = [
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
'#FF9F40', '#8AC249', '#EA526F', '#49A6B7', '#C5D86D'
];
ctx.save();
// 绘制环形图
let startAngle: Double = (-Math.PI / 2) as Double; // 从12点钟方向开始
data!!.forEach((value, i) => {
// 计算扇区角度
const sliceAngle = 2 * Math.PI * (value / sum);
// 确定扇区颜色
let color: string = '';
if (Array.isArray(option.color)) {
color = option.color.length > i ? option.color[i] : defaultColors[i % defaultColors.length];
} else if (typeof option.color === 'string') {
color = i === 0 ? option.color : this.shiftHue(option.color as string, i * 30);
} else {
color = defaultColors[i % defaultColors.length];
}
// 绘制扇区弧段
ctx.beginPath();
ctx.arc(centerX, centerY, outerRadius, startAngle, startAngle + sliceAngle);
ctx.arc(centerX, centerY, innerRadius, startAngle + sliceAngle, startAngle, true);
ctx.closePath();
ctx.fillStyle = color as any;
ctx.fill();
// 绘制扇区边框
ctx.strokeStyle = "#fff" as any;
ctx.lineWidth = 2;
ctx.stroke();
// 计算标签位置 - 在扇区中间
const labelAngle = startAngle + sliceAngle / 2;
const labelRadius = (outerRadius + innerRadius) / 2; // 标签位于内外圆之间
const labelX = centerX + Math.cos(labelAngle) * labelRadius;
const labelY = centerY + Math.sin(labelAngle) * labelRadius;
// 显示数值百分比
const percentage = Math.round((value / sum) * 100);
ctx.font = 'bold 14px sans-serif';
ctx.fillStyle = '#fff' as any;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${percentage}%`, labelX, labelY);
// 更新起始角度为当前扇区的结束角度
startAngle += sliceAngle as Double;
});
// 绘制图例
if (labels != null && labels.length > 0) {
const legendX = centerX - outerRadius - 10;
const legendY = centerY + outerRadius + 20;
data.forEach((value, i) => {
if (labels != null && i < labels.length && labels[i] != null && labels[i] != '') {
// 确定图例颜色
let color: string = '';
if (Array.isArray(option.color)) {
color = option.color.length > i ? option.color[i] : defaultColors[i % defaultColors.length];
} else if (typeof option.color === 'string') {
color = i === 0 ? option.color : this.shiftHue(option.color as string, i * 30);
} else {
color = defaultColors[i % defaultColors.length];
}
// 绘制图例颜色方块
ctx.fillStyle = color as any;
ctx.fillRect(legendX, legendY + i * 25, 15, 15);
// 绘制图例文字
const percentage = Math.round((value / sum) * 100);
ctx.font = '14px sans-serif';
ctx.fillStyle = '#333' as any;
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(`${labels[i]} (${percentage}%)`, legendX + 25, legendY + i * 25 + 7.5);
}
});
}
ctx.restore();
},
drawRadar(ctx: CanvasRenderingContext2D, option: ChartOption) {
const data = option.data != null ? option.data : ([] as number[]);
const labels = option.labels != null ? option.labels : ([] as string[]);
if (data!!.length === 0 || (labels != null && labels.length === 0)) return;
// 使用实际画布尺寸
const width = this.canvasWidth / this.dpr;
const height = this.canvasHeight / this.dpr;
// 计算雷达图中心和半径
let centerXValue: number | null = option.centerX;
const centerX: number = centerXValue != null ? centerXValue : width / 2;
let centerYValue: number | null = option.centerY;
const centerY: number = centerYValue != null ? centerYValue : height / 2;
let radiusValue: number | null = option.radius;
const radius: number = radiusValue != null ? radiusValue : Math.min(width, height) / 2 - 60;
// 计算数据点数量即多边形的边数至少为3
const count = Math.max(data!!.length, 3);
// 找出最大值,用于归一化数据
const max = Math.max(...data, 1);
// 计算每个角的角度
const angleStep = (2 * Math.PI) / count;
// 确定颜色
let color: string = '';
if (typeof option.color === 'string') {
color = option.color;
} else {
color = '#36A2EB';
}
ctx.save();
// 绘制雷达图网格和轴线
for (let level = 1; level <= 5; level++) {
const levelRadius = radius * (level / 5);
// 绘制多边形轮廓
ctx.beginPath();
for (let i = 0; i < count; i++) {
const angle = -Math.PI / 2 + i * angleStep;
const x = centerX + levelRadius * Math.cos(angle);
const y = centerY + levelRadius * Math.sin(angle);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.closePath();
ctx.strokeStyle = '#ddd' as any;
ctx.lineWidth = 1;
ctx.stroke();
}
// 绘制轴线
for (let i = 0; i < count; i++) {
const angle = -Math.PI / 2 + i * angleStep;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(x, y);
ctx.strokeStyle = '#ddd' as any;
ctx.lineWidth = 1;
ctx.stroke();
// 绘制标签
const labelRadius = radius + 15;
const labelX = centerX + labelRadius * Math.cos(angle);
const labelY = centerY + labelRadius * Math.sin(angle);
ctx.font = '14px sans-serif';
ctx.fillStyle = '#666' as any;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 根据标签位置调整对齐方式,使标签不超出画布
if (angle === -Math.PI / 2) { // 顶部
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
} else if (angle === Math.PI / 2) { // 底部
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
} else if (angle === 0) { // 右侧
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
} else if (Math.abs(angle) === Math.PI) { // 左侧
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
} else if (angle > -Math.PI / 2 && angle < 0) { // 右上
ctx.textAlign = 'left';
ctx.textBaseline = 'bottom';
} else if (angle > 0 && angle < Math.PI / 2) { // 右下
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
} else if (angle > Math.PI / 2 && angle < Math.PI) { // 左下
ctx.textAlign = 'right';
ctx.textBaseline = 'top';
} else if (angle > -Math.PI && angle < -Math.PI / 2) { // 左上
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
}
if (labels != null && i < labels.length && labels[i] != null) {
ctx.fillText(labels[i] as string, labelX, labelY);
}
}
// 绘制数据区域
ctx.beginPath();
for (let i = 0; i < count; i++) {
const value = i < data.length ? data[i] : 0;
const valueRadius = (value / max) * radius;
const angle = -Math.PI / 2 + i * angleStep;
const x = centerX + valueRadius * Math.cos(angle);
const y = centerY + valueRadius * Math.sin(angle);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
// 绘制数据点
ctx.fillStyle = color as any;
ctx.beginPath();
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fill();
// 显示数值
const textRadius = valueRadius + 15;
const textX = centerX + valueRadius * Math.cos(angle);
const textY = centerY + valueRadius * Math.sin(angle);
ctx.font = '12px sans-serif';
ctx.fillStyle = '#333' as any;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(value.toString(), textX, textY - 10);
}
// 闭合并填充数据区域
ctx.closePath();
ctx.fillStyle = `${color}33` as any; // 添加透明度
ctx.fill();
// 描边
ctx.strokeStyle = color as any;
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
},
// 辅助函数:根据基础颜色和色调偏移生成新颜色
shiftHue(color: string, degree: number): string {
// 简单实现:返回默认颜色数组中的一个
const defaultColors = [
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
'#FF9F40', '#8AC249', '#EA526F', '#49A6B7', '#C5D86D'
];
return defaultColors[Math.abs(degree) % defaultColors.length];
},
}
}
</script>
<style scoped>
.ak-charts-container {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.ak-charts-canvas {
width: 600rpx;
height: 400rpx;
background: #ffff7f;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
}
</style>