节流战的Canvas图表:消费趋势图与预算进度
如果你想控制消费、养成节约习惯,推荐去鸿蒙应用市场搜一下**「节流战」**,下载体验体验。设置月度预算、记录每笔消费、参与预算挑战,一套走下来对消费习惯会有更清晰的认识。体验完再回来看这篇文章,你会更清楚消费趋势图和预算进度背后是怎么实现的。
写在前面
大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,CSS3动画、requestAnimationFrame、Web Animation API这些都算是看家本领。去年开始转战鸿蒙生态,用ArkTS开发App,这一路踩了不少坑,也积累了不少心得。
很多人觉得"前端转鸿蒙"应该很容易------都是写UI嘛,组件化、状态管理、生命周期,概念都差不多。但真正上手之后你会发现,相似的地方让你觉得亲切,不同的地方让你抓狂。
比如:
- Canvas绘图 :Web的Canvas API和鸿蒙的
CanvasRenderingContext2D几乎一模一样,但创建方式不同。 - 动画效果 :Web用
requestAnimationFrame,鸿蒙用animator模块。 - 数据存储 :Web的
localStorage到了ArkTS变成了@ohos.data.preferences。
接下来这篇文章,我会用"节流战"的实际开发经历,带你看看鸿蒙的Canvas怎么画消费趋势图------从数据聚合到图表绘制。
这篇文章聊什么
节流战的Canvas图表功能,核心要解决两个问题:
- 消费趋势图:展示最近30天的消费走势
- 预算进度:展示本月预算的使用情况
第一步:消费数据结构
typescript
interface Expense {
id: string;
amount: number;
category: string;
description: string;
date: string;
timestamp: number;
isImpulse: boolean; // 是否冲动消费
}
interface Budget {
monthlyLimit: number;
categoryLimits: Record<string, number>;
}
// 8个消费分类
const EXPENSE_CATEGORIES = [
{ id: 'food', name: '餐饮', icon: '🍜', color: '#F59E0B' },
{ id: 'transport', name: '交通', icon: '🚌', color: '#3B82F6' },
{ id: 'shopping', name: '购物', icon: '🛍️', color: '#EC4899' },
{ id: 'entertainment', name: '娱乐', icon: '🎮', color: '#8B5CF6' },
{ id: 'living', name: '生活', icon: '🏠', color: '#10B981' },
{ id: 'health', name: '医疗', icon: '💊', color: '#EF4444' },
{ id: 'education', name: '教育', icon: '📚', color: '#14B8A6' },
{ id: 'other', name: '其他', icon: '📦', color: '#6B7280' }
];
第二步:30天消费趋势图
typescript
@Entry
@Component
struct ExpenseTrendPage {
@State expenses: Expense[] = []
@State dailyTotals: number[] = []
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
async aboutToAppear() {
await this.loadExpenses()
this.calculateDailyTotals()
}
async loadExpenses() {
const store = await preferences.getPreferences(getContext(), 'jieji_data');
const stored = await store.get('expenses', '[]') as string;
this.expenses = JSON.parse(stored);
}
calculateDailyTotals() {
const today = new Date();
this.dailyTotals = [];
for (let i = 29; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().slice(0, 10);
const dayTotal = this.expenses
.filter(e => e.date === dateStr)
.reduce((sum, e) => sum + e.amount, 0);
this.dailyTotals.push(dayTotal);
}
}
build() {
Column() {
Text('30天消费趋势')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
Canvas(this.ctx)
.width('100%')
.height(220)
.backgroundColor('#1F2937')
.borderRadius(12)
.onReady(() => {
this.drawTrendChart()
})
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#111827')
}
private drawTrendChart() {
const ctx = this.ctx;
const width = 350;
const height = 220;
const padding = { top: 20, right: 20, bottom: 30, left: 50 };
ctx.clearRect(0, 0, width, height);
const maxVal = Math.max(...this.dailyTotals, 1);
// 画柱状图
const barWidth = (width - padding.left - padding.right) / 30 - 2;
this.dailyTotals.forEach((total, index) => {
const x = padding.left + (width - padding.left - padding.right) * (index / 30) + 1;
const barHeight = (total / maxVal) * (height - padding.top - padding.bottom);
const y = height - padding.bottom - barHeight;
// 渐变色
const gradient = ctx.createLinearGradient(x, y, x, height - padding.bottom);
gradient.addColorStop(0, '#F59E0B');
gradient.addColorStop(1, '#F59E0B33');
ctx.fillStyle = gradient;
ctx.fillRect(x, y, barWidth, barHeight);
});
// 画Y轴标签
ctx.fillStyle = '#6B7280';
ctx.font = '10px sans-serif';
ctx.textAlign = 'right';
for (let i = 0; i <= 4; i++) {
const value = maxVal * (i / 4);
const y = height - padding.bottom - (height - padding.top - padding.bottom) * (i / 4);
ctx.fillText(`¥${Math.round(value)}`, padding.left - 5, y + 4);
}
}
}
第三步:预算进度环
typescript
// 画预算进度环
private drawBudgetRing(budgetUsed: number, budgetTotal: number) {
const ctx = this.ctx;
const centerX = 100;
const centerY = 100;
const radius = 80;
const lineWidth = 12;
const progress = Math.min(budgetUsed / budgetTotal, 1);
const startAngle = -Math.PI / 2;
const endAngle = startAngle + progress * Math.PI * 2;
// 背景环
ctx.strokeStyle = '#374151';
ctx.lineWidth = lineWidth;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.stroke();
// 进度环
const color = progress > 0.9 ? '#EF4444' : progress > 0.7 ? '#F59E0B' : '#10B981';
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
ctx.stroke();
// 中心文字
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold 24px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${Math.round(progress * 100)}%`, centerX, centerY);
}
总结
这篇文章围绕"节流战"的Canvas图表功能,讲解了两个核心主题:
- 消费趋势图:30天柱状图的数据聚合和绘制
- 预算进度环:圆弧进度条的绘制
Canvas绘图的核心技巧是坐标映射和渐变色运用。柱状图用渐变色可以让图表更有层次感。
如果你想控制消费,希望这篇文章能帮你理解节流战背后的图表实现。去鸿蒙应用市场下载体验一下吧,有问题欢迎交流。