HarmonyOS APP开发---“理财通“个人记账理财App,需要用到这个库
·
HarmonyOS APP开发—"理财通"个人记账理财App,需要用到这个库
理财 App 的数据看板要三种图:支出分类堆叠柱状图、资产走势渐变折线、持仓占比饼图。自己用 Canvas 画要上千行。
@ohos/mpchart几十行搞定——但金融场景对数据准确性零容忍,金额显示 3.1 和 3.10 是两回事,堆叠图数据异常还会直接崩溃。
📦 仓库地址 |
ohpm install @ohos/mpchart
场景
"理财通"的数据看板:
- 月度支出堆叠柱状图:每根柱子一个月,柱内按"餐饮/交通/购物/居住"分色堆叠——一眼看出哪个月哪类花超了
- 资产走势渐变折线图:最近 30 天总资产变化,折线下方渐变填充
- 持仓占比饼图:股票/基金/现金/理财的比例分布
金融场景的两个特殊要求:
- 数据不能错:金额小数位要统一,堆叠数据异常会崩溃
- 视觉要专业:渐变方向、坐标轴刻度、图例位置,用户一眼看出专不专业
为什么是 mpchart
| 需求 | 自研 Canvas | @ohos/mpchart |
|---|---|---|
| 堆叠柱状图 | 300+ 行 | ✅ 30 行 |
| 渐变折线图 | 400+ 行 | ✅ 30 行 |
| 饼图 | 250+ 行 | ✅ 20 行 |
| 手势缩放 | 手写 | ✅ 内置 |
| 点击高亮 Marker | 手写 | ✅ 内置 |
| 预算警戒线 | 手写 | ✅ LimitLine |
关键实现
先定金额格式化(金融场景第一要求)
Y 轴、Marker 气泡、Tooltip 全部用同一个函数,小数位才不会打架:
function formatMoney(value: number, withSymbol = true): string {
if (!isFinite(value)) return withSymbol ? '¥--' : '--'
const [intPart, decPart] = Math.abs(value).toFixed(2).split('.')
const withComma = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',') // 千分位
const sign = value < 0 ? '-' : ''
return `${sign}${withSymbol ? '¥' : ''}${withComma}.${decPart}`
}
// Y 轴空间有限时用简写
function formatMoneyShort(value: number): string {
return Math.abs(value) >= 10000 ? (value / 10000).toFixed(1) + '万' : value.toFixed(0)
}
console.info(formatMoney(1234567.891)) // ¥1,234,567.89
console.info(formatMoney(-500)) // -¥500.00
支出堆叠柱状图
堆叠涉及多数据集累加计算,数据异常容易触发崩溃,渲染前必须校验:
// ✅ 校验函数:返回错误信息,null 表示通过
function validateStackedData(dataSets: BarDataSet[], expectedCount: number): string | null {
if (dataSets.length === 0) return '数据集为空'
for (const set of dataSets) {
if (set.getEntryCount() !== expectedCount) {
return `${set.getLabel()} 条目数不一致: ${set.getEntryCount()} vs ${expectedCount}`
}
for (let i = 0; i < set.getEntryCount(); i++) {
const v = set.getEntryForIndex(i).getY()
if (!isFinite(v)) return `${set.getLabel()} 第 ${i} 项不是有效数字`
if (v < 0) return `${set.getLabel()} 第 ${i} 项为负数: ${v}`
}
}
return null
}
const CATEGORIES = [
{ key: 'food', name: '餐饮', color: 0xFFFF6B6B },
{ key: 'transport', name: '交通', color: 0xFF4ECDC4 },
{ key: 'shopping', name: '购物', color: 0xFFFFEAA7 },
{ key: 'housing', name: '居住', color: 0xFF6C5CE7 },
]
aboutToAppear() {
const dataSets = CATEGORIES.map((cat) => {
const entries = new JArrayList<BarEntry>()
this.expenseData[cat.key].forEach((amount, i) => entries.add(new BarEntry(i, amount)))
const set = new BarDataSet(entries, cat.name)
set.setColorByColor(cat.color)
return set
})
// ✅ 先校验再渲染
const error = validateStackedData(dataSets, this.months.length)
if (error) {
console.error('堆叠数据校验失败: ' + error)
this.hasError = true
return
}
const barData = new BarData(dataSets)
barData.setBarWidth(0.6)
this.model.setData(barData)
this.model.getXAxis().setPosition(XAxisPosition.BOTTOM)
this.model.getXAxis().setValueFormatter((v) => this.months[Math.floor(v)] ?? '')
this.model.getAxisLeft().setValueFormatter((v) => formatMoneyShort(v)) // ✅ 统一格式化
this.model.getAxisRight().setEnabled(false)
this.model.getLegend().setEnabled(true)
// 预算警戒线
const budgetLine = new LimitLine(20000, '月度预算')
budgetLine.setLineColor(0xFFFF6B6B)
budgetLine.setLabelPosition(LimitLabelPosition.RIGHT_TOP)
this.model.getAxisLeft().addLimitLine(budgetLine)
this.barChart.setOnChartValueSelectedListener({
onValueSelected: (e, h) => {
promptAction.showToast({ message: `${this.months[Math.floor(e.getX())]} ${formatMoney(e.getY())}` })
},
onNothingSelected: () => {},
})
this.barChart.setModel(this.model)
}
数据异常时显示友好提示,而不是崩溃或空白:
if (this.hasError) {
Column({ space: 8 }) {
Text('📊').fontSize(40)
Text('数据异常,暂无法展示').fontSize(14).fontColor('#999')
}.layoutWeight(1).justifyContent(FlexAlign.Center)
} else {
this.barChart
}
资产走势渐变折线图
const dataSet = new LineDataSet(entries, '总资产')
dataSet.setLineWidth(2)
dataSet.setDrawCircles(false) // 30 个点不画圆圈,视觉更简洁
dataSet.setDrawFilled(true)
dataSet.setFillDrawable(new LinearGradient(0xFF4ECDC4, 0x224ECDC4)) // 高深低浅
// 涨跌着色
const trendColor = assets[assets.length - 1] >= assets[0] ? 0xFF4ECDC4 : 0xFFFF6B6B
dataSet.setColorByColor(trendColor)
model.setPinchZoom(true) // 双指缩放看细节
渐变验收清单(渐变方向错了整体观感就垮):
| 检查项 | 预期 |
|---|---|
| 渐变方向 | 按 Y 值高低分布,不是简单从图表顶部到底部 |
| 高点颜色 | 深色不透明 |
| 低点颜色 | 浅色带透明度 |
| 数据变化时 | 渐变跟随重新计算 |
| 缩放后 | 渐变仍正确 |
| 深色模式 | 暗背景下依然清晰 |
持仓占比饼图
const entries = new JArrayList<PieEntry>()
holdings.forEach((h) => entries.add(new PieEntry(h.value, h.name)))
const dataSet = new PieDataSet(entries, '持仓分布')
dataSet.setColors(holdings.map((h) => h.color))
dataSet.setSliceSpace(2) // 扇区间隙,视觉更清晰
model.setUsePercentValues(true) // 显示百分比而非绝对值
一个容易踩的点:出海要切坐标轴语言
社区修过一个多语言问题:坐标轴值在繁体中文和日语环境下都显示简体中文。只切 UI 文案不切轴标签,出海用户立刻能看出来。
import { i18n } from '@kit.LocalizationKit'
const lang = i18n.System.getSystemLanguage()
const MONTHS: Record<string, string[]> = {
'zh-Hans': ['1月','2月','3月','4月','5月','6月'],
'zh-Hant': ['1月','2月','3月','4月','5月','6月'],
'ja': ['1月','2月','3月','4月','5月','6月'],
'en': ['Jan','Feb','Mar','Apr','May','Jun'],
}
const CURRENCY: Record<string, string> = { 'zh-Hans':'¥', 'zh-Hant':'¥', 'ja':'¥', 'en':'$' }
model.getXAxis().setValueFormatter((v) => (MONTHS[lang] ?? MONTHS['en'])[Math.floor(v)] ?? '')
model.getAxisLeft().setValueFormatter((v) => (CURRENCY[lang] ?? '$') + formatMoneyShort(v))
小结
"理财通"这个场景里,@ohos/mpchart 解决了三件事:三种图表一个库几十行出图、手势缩放和点击高亮内置、LimitLine 直接画预算警戒线。
但金融 App 的专业度体现在细节:金额格式化统一收口、堆叠数据渲染前校验、渐变方向肉眼验收、出海时轴标签跟着切语言。这四条做到了,用户才会觉得这个理财 App 靠谱。
更多推荐
所有评论(0)