Initial commit

This commit is contained in:
2026-03-16 10:37:46 +08:00
commit c052a67816
508 changed files with 22987 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
# ak-charts
一个简单的 uni_modules 图表插件支持基础柱状图和折线图UTS 插件规范。
## 使用方法
1. 在页面中引用组件:
````vue
<ak-charts :option="option" canvas-id="my-canvas"></ak-charts>
````
2. 通过 AkCharts.render(option, canvasId) 进行全局渲染调用。
option 示例:
```js
{
type: 'bar', // 或 'line'
data: [10, 20, 30],
labels: ['A', 'B', 'C'],
color: '#2196f3'
}
```
## 目录结构
- index.uts 插件主入口
- components/ak-charts/ak-charts.vue 图表组件
- package.json 插件描述
- README.md 插件说明

View File

@@ -0,0 +1,803 @@
<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>

View File

@@ -0,0 +1,45 @@
/**
* ak-charts UTS 插件主入口
* 提供注册和渲染图表的基础接口
*/
export type ChartType = 'bar' | 'line' | 'pie' | 'doughnut' | 'radar';
export type ChartSeries = {
name: string;
data: number[];
color?: string;
};
export type ChartOption = {
type: ChartType;
data?: number[];
series?: ChartSeries[]; // 新增
labels?: string[];
color?: string | string[];
centerX?: number;
centerY?: number;
radius?: number;
innerRadius?: number; // 环形图内圆半径
}
export type Margin {
top: number;
right: number;
bottom: number;
left: number;
}
export class AkCharts {
// 注册图表(可扩展)
static register(type: ChartType, render: Function): void {
// 这里可以实现自定义图表类型注册
}
// 渲染图表(实际渲染由组件完成)
static render(option: ChartOption, canvasId: string): void {
// 这里只做参数校验和分发,实际渲染由 ak-charts.vue 组件实现
// 可通过 uni.$emit/uni.$on 或全局事件通信
uni.$emit('ak-charts-render', { option, canvasId });
}
}
export default AkCharts;

View File

@@ -0,0 +1,12 @@
{
"name": "ak-charts",
"version": "0.1.0",
"description": "一个简单的uni_modules图表插件支持基础柱状图和折线图UTS插件规范。",
"uni_modules": {
"uni_modules": true,
"platforms": ["app", "h5", "mp-weixin"],
"uts": true
},
"main": "index.uts",
"keywords": ["charts", "canvas", "uni_modules", "uts"]
}