如何用 mpchart 打造优雅的鸿蒙图表?喵屿应用实践全记录
如何用 mpchart 打造优雅的鸿蒙图表?喵屿应用实践全记录
1. 什么是 mpchart
mpchart(@ohos/mpchart)是 MPAndroidChart 向 HarmonyOS 平台的高保真移植版本,基于 ArkTS 语言重构,专为 ArkUI 声明式开发范式设计。它在 API 设计上高度对齐 MPAndroidChart 的使用习惯,同时充分利用了 ArkUI Canvas 渲染能力,为鸿蒙原生应用提供了一套开箱即用的图表解决方案。
1.1 核心特性
- 丰富的图表类型:支持折线图(LineChart)、饼状图(PieChart)、柱状图(BarChart)、水平柱状图(HorizontalBarChart)、雷达图(RadarChart)、气泡图(BubbleChart)、蜡烛图(CandleStickChart)、散点图(ScatterChart)、瀑布图(WaterfallChart)和组合图(CombinedChart)。
- ArkTS 原生化:完全基于 ArkTS 语言开发,与 ArkUI 组件体系无缝集成,使用
{ model }语法糖直接嵌入声明式 UI。 - Canvas 渲染:底层基于
CanvasRenderingContext2D进行像素级绘制,支持流畅的动画效果。 - 丰富的交互能力:支持捏合缩放、拖拽平移、点击高亮、长按标记等触摸手势。
- 高度可定制:支持渐变填充、自定义轴标签格式化器、数据点样式、图例配置等。
1.2 适用场景
| 场景 | 推荐图表类型 |
|---|---|
| 健康数据趋势(体重、身高、体温) | LineChart |
| 消费分类占比 | PieChart |
| 月度收支对比 | BarChart |
| 多维能力评估 | RadarChart |
| 多指标混合展示 | CombinedChart |
2. 快速开始与常用 API 介绍
2.1 快速开始
在模块的 oh-package.json5 中添加依赖:
{
"dependencies": {
"@ohos/mpchart": "3.0.25"
}
}
添加后执行 ohpm install 或在 DevEco Studio 中同步项目即可完成安装。或者直接运行:
ohpm install @ohos/mpchart
2.2 核心架构
mpchart 采用 Model-View 分离 的架构设计:
- Model 层(如
LineChartModel、PieChartModel):负责数据管理、坐标计算、渲染调度和交互处理。 - View 层(如
LineChart、PieChart):封装 ArkUI 组件,接收 Model 实例并通过 Canvas 绘制图表。
使用时,开发者先创建并配置 Model 实例,再将其传递给对应的 Chart 组件即可完成渲染。
2.3 折线图(LineChart)使用入门
折线图是 mpchart 中最常用的图表类型,适用于展示数据随时间变化的趋势。以下是一个完整的折线图示例:
import {
EntryOhos,
JArrayList,
LineChart,
LineChartModel,
LineData,
LineDataSet,
Mode,
XAxisPosition,
} from '@ohos/mpchart';
@ComponentV2
export struct SimpleLineChart {
@Local model: LineChartModel = new LineChartModel();
aboutToAppear(): void {
// 1. 开启动画
this.model.animateX(500);
// 2. 隐藏描述和图例
this.model.getDescription()?.setEnabled(false);
this.model.getLegend()?.setEnabled(false);
// 3. 配置坐标轴
this.model.getXAxis()?.setPosition(XAxisPosition.BOTTOM);
this.model.getAxisRight()?.setEnabled(false);
// 4. 禁用缩放,允许横向拖拽
this.model.setScaleEnabled(false);
this.model.setDragXEnabled(true);
// 5. 构建数据
this.setChartData();
}
private setChartData(): void {
const values: JArrayList<EntryOhos> = new JArrayList();
values.add(new EntryOhos(0, 4.5));
values.add(new EntryOhos(1, 4.7));
values.add(new EntryOhos(2, 4.6));
values.add(new EntryOhos(3, 4.8));
values.add(new EntryOhos(4, 5.0));
const dataSet = new LineDataSet(values, '体重趋势');
dataSet.setMode(Mode.CUBIC_BEZIER); // 平滑曲线
dataSet.setDrawCircles(true);
dataSet.setColorByColor(0xd75d0b);
const lineData = new LineData();
lineData.addDataSet(dataSet);
this.model.setData(lineData);
}
build() {
LineChart({ model: this.model })
.width('100%')
.height(200)
}
}
2.3.1 LineChartModel 常用 API
| API | 说明 |
|---|---|
animateX(duration: number) |
设置 X 轴动画时长(毫秒) |
animateY(duration: number) |
设置 Y 轴动画时长(毫秒) |
setData(data: LineData) |
设置图表数据 |
setScaleEnabled(enabled: boolean) |
启用/禁用缩放 |
setDragXEnabled(enabled: boolean) |
启用/禁用横向拖拽 |
setDragDecelerationEnabled(enabled: boolean) |
启用/禁用惯性拖拽 |
setAutoScaleMinMaxEnabled(enabled: boolean) |
自动调整 Y 轴范围 |
setVisibleXRangeMinimum(range: number) |
设置 X 轴最小可见范围 |
setVisibleXRangeMaximum(range: number) |
设置 X 轴最大可见范围 |
moveViewToX(xIndex: number) |
滚动到指定 X 轴位置 |
getXAxis() |
获取 X 轴配置对象 |
getAxisLeft() |
获取左 Y 轴配置对象 |
getAxisRight() |
获取右 Y 轴配置对象 |
getDescription() |
获取描述文本配置 |
getLegend() |
获取图例配置 |
2.3.2 LineDataSet 常用配置
| API | 说明 |
|---|---|
setMode(mode: Mode) |
设置曲线模式:LINEAR(直线)、CUBIC_BEZIER(平滑曲线)、STEPPED(阶梯线) |
setDrawCircles(enabled: boolean) |
是否在数据点绘制圆点 |
setDrawCircleHole(enabled: boolean) |
数据点圆点是否空心 |
setCircleRadius(radius: number) |
数据点圆点半径 |
setCircleColor(color: number) |
数据点圆点颜色 |
setColorByColor(color: number) |
设置折线颜色 |
setDrawFilled(enabled: boolean) |
是否填充折线下方区域 |
setGradientFillColor(colors: JArrayList<ChartColorStop>) |
设置渐变填充色 |
setValueTextSize(size: number) |
数据标签文字大小 |
setValueTextColor(color: number) |
数据标签文字颜色 |
2.3.3 自定义坐标轴标签格式化器
mpchart 提供 IAxisValueFormatter 接口用于自定义坐标轴标签显示:
import { IAxisValueFormatter } from '@ohos/mpchart';
class DateAxisValueFormatter implements IAxisValueFormatter {
private dates: string[] = [];
constructor(dates: string[]) {
this.dates = dates;
}
public getFormattedValue(value: number): string {
const index = Math.round(value);
if (index >= 0 && index < this.dates.length) {
const parts = this.dates[index].split('-');
if (parts.length === 3) {
return `${parseInt(parts[1]).toString()}/${parseInt(parts[2]).toString()}`;
}
return this.dates[index];
}
return '';
}
}
// 使用方式
this.model.getXAxis()?.setValueFormatter(new DateAxisValueFormatter(dateList));
2.4 饼状图(PieChart)使用入门
饼状图适用于展示各部分占总体的比例关系。以下是一个完整的饼状图示例:
import {
IAxisValueFormatter,
JArrayList,
PieChart,
PieChartModel,
PieData,
PieDataSet,
PieEntry,
ValuePosition,
} from '@ohos/mpchart';
interface PieChartDataItem {
label: string;
value: number;
}
@ComponentV2
export struct SimplePieChart {
@Local model: PieChartModel = new PieChartModel();
@Param chartData: PieChartDataItem[] = [];
@Param colorList: number[] = [0x4b6f3f, 0x638750, 0x7ea568, 0x94b982];
@Monitor('chartData')
refreshChart(): void {
this.setChartData();
}
aboutToAppear(): void {
// 开启动画
this.model.animateX(1000);
// 使用百分比显示
this.model.setUsePercentValues(true);
// 隐藏图例和描述
this.model.getLegend()?.setEnabled(false);
this.model.getDescription()?.setEnabled(false);
// 设置环形饼图
this.model.setHoleRadius(60);
this.model.setHoleColor(Color.Transparent);
this.model.setTransparentCircleColor(Color.White);
// 标签样式
this.model.setEntryLabelColor(0x000000);
this.model.setEntryLabelTextSize(10);
this.setChartData();
}
private setChartData(): void {
const entries: JArrayList<PieEntry> = new JArrayList();
for (let i = 0; i < this.chartData.length; i++) {
entries.add(new PieEntry(this.chartData[i].value, this.chartData[i].label));
}
const dataSet = new PieDataSet(entries, '分类占比');
dataSet.setSliceSpace(2); // 扇形间隙
dataSet.setValueTextSize(10); // 百分比文字大小
dataSet.setValueTextColor(0x000000); // 百分比文字颜色
dataSet.setYValuePosition(ValuePosition.OUTSIDE_SLICE); // 百分比显示在外部
dataSet.setXValuePosition(ValuePosition.OUTSIDE_SLICE); // 标签显示在外部
// 设置引出线样式
dataSet.setValueLinePart1OffsetPercentage(120);
dataSet.setValueLinePart1Length(0.6);
dataSet.setValueLinePart2Length(0.4);
dataSet.setValueLineColor(0xd0d0d0);
// 设置扇形颜色
const colors: JArrayList<number> = new JArrayList();
this.colorList.forEach((c: number): void => { colors.add(c); });
dataSet.setColorsByList(colors);
// 自定义百分比格式化
dataSet.setValueFormatter(new PercentFormatter());
const pieData = new PieData(dataSet);
this.model.setData(pieData);
}
build() {
PieChart({ model: this.model })
.width('100%')
.height(200)
}
}
class PercentFormatter implements IAxisValueFormatter {
public getFormattedValue(value: number): string {
return value.toFixed(2) + '%';
}
}
2.4.1 PieChartModel 常用 API
| API | 说明 |
|---|---|
animateX(duration: number) |
设置动画时长(毫秒) |
setUsePercentValues(enabled: boolean) |
是否使用百分比显示 |
setHoleRadius(percent: number) |
中心孔半径(占半径百分比,50 为一半) |
setHoleColor(color: number) |
中心孔颜色 |
setTransparentCircleColor(color: number) |
透明圆环颜色 |
setTransparentCircleRadius(percent: number) |
透明圆环半径 |
setEntryLabelColor(color: number) |
标签文字颜色 |
setEntryLabelTextSize(size: number) |
标签文字大小 |
setCenterText(text: string) |
中心文字 |
setCenterTextSize(size: number) |
中心文字大小 |
setCenterTextColor(color: number) |
中心文字颜色 |
setMaxAngle(angle: number) |
最大角度(180 为半圆) |
setMinAngleForSlices(angle: number) |
扇形最小角度 |
setDrawEntryLabels(enabled: boolean) |
是否显示标签 |
setExtraOffsets(left: number, top: number, right: number, bottom: number) |
设置内边距 |
2.4.2 PieDataSet 常用配置
| API | 说明 |
|---|---|
setSliceSpace(space: number) |
扇形之间的间隙 |
setValueTextSize(size: number) |
数值文字大小 |
setValueTextColor(color: number) |
数值文字颜色 |
setYValuePosition(position: ValuePosition) |
数值位置:OUTSIDE_SLICE / INSIDE_SLICE |
setXValuePosition(position: ValuePosition) |
标签位置:OUTSIDE_SLICE / INSIDE_SLICE |
setValueLineColor(color: number) |
引出线颜色 |
setValueLinePart1Length(length: number) |
引出线第一段长度 |
setValueLinePart2Length(length: number) |
引出线第二段长度 |
setValueLinePart1OffsetPercentage(percent: number) |
引出线偏移百分比 |
setUsingSliceColorAsValueLineColor(enabled: boolean) |
引出线使用扇形颜色 |
setColorsByList(colors: JArrayList<number>) |
设置颜色列表 |
setValueFormatter(formatter: IAxisValueFormatter) |
设置数值格式化器 |
setSelectionShift(shift: number) |
选中时扇形偏移距离 |
2.5 柱状图(BarChart)简要示例
import {
BarChart,
BarChartModel,
BarData,
BarDataSet,
BarEntry,
JArrayList,
} from '@ohos/mpchart';
@ComponentV2
export struct SimpleBarChart {
@Local model: BarChartModel = new BarChartModel();
aboutToAppear(): void {
this.model.animateY(800);
this.model.getDescription()?.setEnabled(false);
const values: JArrayList<BarEntry> = new JArrayList();
values.add(new BarEntry(0, 1200));
values.add(new BarEntry(1, 850));
values.add(new BarEntry(2, 1500));
values.add(new BarEntry(3, 980));
const dataSet = new BarDataSet(values, '月度支出');
dataSet.setColorByColor(0x4b6f3f);
const barData = new BarData();
barData.addDataSet(dataSet);
this.model.setData(barData);
}
build() {
BarChart({ model: this.model })
.width('100%')
.height(200)
}
}
2.6 通用 API 速查(基类 ChartModel)
以下 API 适用于所有图表类型的 Model 对象:
| API | 说明 |
|---|---|
setData(data: ChartData) |
设置图表数据 |
animateX(duration: number) |
X 轴动画 |
animateY(duration: number) |
Y 轴动画 |
setScaleEnabled(enabled: boolean) |
是否可缩放 |
setDragEnabled(enabled: boolean) |
是否可拖拽 |
getDescription() |
获取描述对象,可配置文字、颜色、位置 |
getLegend() |
获取图例对象,可配置样式、位置、方向 |
setExtraOffsets(l, t, r, b) |
设置图表内边距 |
setNoDataText(text: string) |
无数据时显示文字 |
setHighlightPerTapEnabled(enabled: boolean) |
点击高亮开关 |
setMarker(marker: MarkerView) |
设置自定义标记视图 |
3. 喵屿中的具体应用
喵屿中 mpchart 被应用于两大核心场景:宠物健康数据趋势展示 和 养宠账单分类占比分析。
3.1 健康数据折线图(HealthLineChart)
3.1.1 组件设计
HealthLineChart 是一个基于 @ComponentV2 封装的通用折线图组件。它接收外部传入的健康数据,自动处理数据补全、坐标轴格式化、曲线样式等逻辑。
3.1.2 数据模型
export class HealthDataPoint {
date: string = '';
value: number = 0;
constructor(date: string, value: number) {
this.date = date;
this.value = value;
}
}
3.1.3 核心实现
@ComponentV2
export struct HealthLineChart {
@Local model: LineChartModel = new LineChartModel();
@Param title: string = '';
@Param unit: string = '';
@Param data: HealthDataPoint[] = [];
@Param chartHeight: number = 200;
@Param lineColor: number = 0x4b6f3f;
@Monitor('data')
refreshChart(): void {
this.setChartData();
}
aboutToAppear(): void {
this.model.animateX(500);
this.model.getLegend()?.setEnabled(false);
this.model.getDescription()?.setEnabled(false);
this.model.getXAxis()?.setPosition(XAxisPosition.BOTTOM);
this.model.getAxisRight()?.setEnabled(false);
this.model.setAutoScaleMinMaxEnabled(true);
this.model.getAxisLeft()?.setGridColor(Color.Orange);
this.model.getXAxis()?.setDrawGridLines(false);
this.model.setScaleEnabled(false);
this.model.setDragXEnabled(true);
this.model.setDragDecelerationEnabled(true);
this.setChartData();
}
private setChartData(): void {
const processedData = this.processData();
const values: JArrayList<EntryOhos> = new JArrayList();
for (let i = 0; i < processedData.length; i++) {
values.add(new EntryOhos(i, Number(processedData[i].value)));
}
const dataSet = new LineDataSet(values, this.title);
dataSet.setMode(Mode.CUBIC_BEZIER);
dataSet.setDrawCircles(true);
dataSet.setDrawCircleHole(true);
dataSet.setColorByColor(Color.Orange);
dataSet.setCircleColor(0xd75d0b);
dataSet.setCircleRadius(4);
dataSet.setCircleHoleRadius(2);
// 渐变填充
const gradientFillColor = new JArrayList<ChartColorStop>();
gradientFillColor.add(['#1CE8E2D1', 0.2]);
gradientFillColor.add(['#7FE8E2D1', 0.4]);
gradientFillColor.add(['#8cd75d0b', 1.0]);
dataSet.setGradientFillColor(gradientFillColor);
dataSet.setDrawFilled(true);
const lineData = new LineData();
lineData.addDataSet(dataSet);
// 限制可视区域
this.model.setVisibleXRangeMinimum(7);
this.model.setVisibleXRangeMaximum(7);
this.model.moveViewToX(processedData.length - 1);
this.model.getXAxis()?.setValueFormatter(
new DateAxisValueFormatter(processedData)
);
this.model.setData(lineData);
}
build() {
Column({ space: 8 }) {
Row() {
Text(this.title)
.fontSize(16)
.fontColor($r('[resource].color.fontColor_gray'));
Blank()
if (this.data.length > 0) {
Text(`最新:${Number(this.data[this.data.length - 1].value).toFixed(1)}${this.unit}`)
.fontSize(14)
.fontColor($r('[resource].color.font_color_level1'));
}
}
.width('100%')
.padding({ left: 16, right: 16 })
if (this.data.length > 0) {
LineChart({ model: this.model })
.width('100%')
.height(this.chartHeight)
.padding({ left: 16, right: 16 })
} else {
// 空状态占位
Column({ space: 16 }) {
Image($r('[resource].media.no_item'))
.width(100)
.height(100)
Text($r('[resource].string.no_item'))
.fontSize(15)
.fontColor($r('[resource].color.gray_2'))
}
.transition(
TransitionEffect.asymmetric(
TransitionEffect.move(TransitionEdge.END)
.animation({ duration: 500, delay: 50, curve: Curve.Ease })
.combine(TransitionEffect.opacity(0)),
TransitionEffect.move(TransitionEdge.START)
.animation({ duration: 500, delay: 50, curve: Curve.Ease })
.combine(TransitionEffect.opacity(0))
)
)
}
}
.width('100%')
.backgroundColor($r('[resource].color.item_bg_main'))
.borderRadius(16)
.padding({ top: 12, bottom: 12 })
}
}
3.1.4 设计要点分析
数据预处理策略:processData() 方法负责确保图表始终显示固定数量的数据点(最多 7 个)。当数据不足 7 个时,在左侧补足占位数据点;超过 7 个时截取最新的 7 个。
private processData(): HealthDataPoint[] {
const processedData: HealthDataPoint[] = [];
const dataLength = this.data.length;
if (dataLength <= 7) {
const missingCount = 7 - dataLength;
const lastDate = dataLength > 0
? new Date(this.data[0].date)
: new Date();
for (let i = missingCount; i > 0; i--) {
const date = new Date(lastDate);
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split('T')[0];
processedData.push(new HealthDataPoint(dateStr, 0));
}
processedData.push(...this.data);
} else {
processedData.push(...this.data);
}
return processedData;
}
自定义日期轴格式化器:将 YYYY-MM-DD 格式转换为简洁的 M/D 月日格式:
class DateAxisValueFormatter implements IAxisValueFormatter {
private data: HealthDataPoint[] = [];
constructor(data: HealthDataPoint[]) {
this.data = data;
}
public getFormattedValue(value: number): string {
const index = Math.round(value);
if (index >= 0 && index < this.data.length) {
const dateStr = this.data[index].date;
const parts = dateStr.split('-');
if (parts.length === 3) {
const month = parseInt(parts[1]).toString();
const day = parseInt(parts[2]).toString();
return `${month}/${day}`;
}
return dateStr;
}
return '';
}
}
状态驱动更新:使用 @Monitor('data') 监听外部数据变化,自动触发图表重绘,无需手动调用刷新。
3.1.5 使用示例
在 HealthDataView 中,组件被复用了三次,分别展示体重、身高和体温趋势:
// 体重折线图
HealthLineChart({
title: '猫咪的体重',
unit: 'kg',
data: this.weightData,
chartHeight: 180,
lineColor: 0x4b6f3f
})
// 身高折线图
HealthLineChart({
title: '猫咪的身高',
unit: 'cm',
data: this.heightData,
chartHeight: 180,
lineColor: 0x638750
})
// 体温折线图
HealthLineChart({
title: '猫咪的体温',
unit: '°C',
data: this.temperatureData,
chartHeight: 180,
lineColor: 0x7ea568
})

3.2 账单分类饼状图(BillPieChart)
3.2.1 组件设计
BillPieChart 封装了一个环形饼状图,用于展示养宠账单的支出类型占比或各宠物之间的费用分配比例。组件支持切换两种统计维度,每种维度配有对应的配色方案。
3.2.2 数据模型
export interface BillPieChartItem {
label: string;
value: number;
resource: number;
}
3.2.3 核心实现
@ComponentV2
export struct BillPieChart {
@Local model: PieChartModel = new PieChartModel();
@Param chartData: BillPieChartItem[] = [];
@Param colorList: number[] = [
0x4b6f3f, 0x638750, 0x7ea568, 0x94b982,
0xabd39c, 0xc6e5b9, 0xdff3d7, 0xf2fdee,
];
@Param valueColor: number = 0x000000;
@Param valueSize: number = 10;
@Param labelColor: number = 0x000000;
@Param labelSize: number = 10;
@Param valueLineColor: number = 0xd0d0d0;
@Param chartHeight: Length = 200;
@Computed
get totalValue(): number {
return this.chartData.reduce((pre: number, cur: BillPieChartItem) => pre + cur.value, 0);
}
@Monitor('chartData')
refreshChart(): void {
this.setChartData();
}
aboutToAppear(): void {
this.model.animateX(1000);
this.model.setUsePercentValues(true);
this.model.getLegend()?.setEnabled(false);
this.model.getDescription()?.setEnabled(false);
this.model.setExtraOffsets(12, 12, 12, 12);
this.model.setTransparentCircleColor(Color.White);
this.model.setEntryLabelColor(this.labelColor);
this.model.setEntryLabelTextSize(this.labelSize);
this.model.setHoleRadius(75);
this.model.setHoleColor(Color.Transparent);
this.model.setMinAngleForSlices(25);
this.setChartData();
}
private setChartData(): void {
const entries: JArrayList<PieEntry> = new JArrayList();
for (let i = 0; i < this.chartData.length; i++) {
entries.add(new PieEntry(this.chartData[i].value, this.chartData[i].label));
}
const dataSet = new PieDataSet(entries, '账单分类');
dataSet.setSliceSpace(2);
dataSet.setUsingSliceColorAsValueLineColor(false);
dataSet.setValueLinePart1OffsetPercentage(120);
dataSet.setValueLinePart1Length(0.6);
dataSet.setValueLinePart2Length(0.4);
dataSet.setValueLineColor(this.valueLineColor);
dataSet.setValueTextSize(this.valueSize);
dataSet.setValueTextColor(this.valueColor);
dataSet.setValueFormatter(new PercentFormatter());
dataSet.setYValuePosition(ValuePosition.OUTSIDE_SLICE);
dataSet.setXValuePosition(ValuePosition.OUTSIDE_SLICE);
const colors: JArrayList<number> = new JArrayList();
for (let index = 0; index < this.colorList.length; index++) {
colors.add(this.colorList[index]);
}
dataSet.setColorsByList(colors);
const pieData = new PieData(dataSet);
this.model.setData(pieData);
}
build() {
Column() {
if (this.chartData.length > 0) {
PieChart({ model: this.model })
.width('100%')
.height('100%')
} else {
Image($r('[resource].media.no_item')).width(100).height(100)
Text($r('[resource].string.no_item'))
.fontSize(15)
.fontColor($r('[resource].color.gray_2'))
}
}
.width('100%')
.height(this.chartHeight)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
}
}
class PercentFormatter implements IAxisValueFormatter {
public getFormattedValue(value: number): string {
return value.toFixed(2) + '%';
}
}
3.2.4 设计要点分析
计算属性优化:使用 @Computed 装饰器计算总金额,当 chartData 不变时避免重复计算:
@Computed
get totalValue(): number {
return this.chartData.reduce((pre: number, cur: BillPieChartItem) => pre + cur.value, 0);
}
双配色方案切换:在 BillStatisticsView 中,根据统计维度(类型占比 vs 宠物占比)动态切换配色:
// 类型占比 - 绿色系
this.colorList = [
0x4b6f3f, 0x638750, 0x7ea568, 0x94b982,
0xabd39c, 0xc6e5b9, 0xdff3d7, 0xf2fdee,
];
// 宠物占比 - 橙色系
this.colorList = [
0xb3511e, 0xd77525, 0xf2992c, 0xfbb935,
0xffce52, 0xffe38e, 0xfff1ca, 0xfffbef,
];
引出线精细控制:通过调整 setValueLinePart1OffsetPercentage、setValueLinePart1Length 和 setValueLinePart2Length 三个参数,实现引出线从饼图扇形外侧优雅地延伸到百分比标签位置,避免标签重叠。
最小角度保护:setMinAngleForSlices(25) 确保即使数据占比很小的分类也有足够的视觉空间,避免扇形过细难以辨认。

4. 最佳实践与总结
4.1 组件封装模式
推荐将图表封装为独立的 @ComponentV2 组件,通过 @Param 接收数据和配置,通过 @Local 持有 Model 实例。这种模式的优势在于:
- 复用性强:同一图表组件可在不同页面以不同数据多次实例化。
- 职责清晰:组件只负责图表渲染,数据获取和处理由父组件完成。
- 易于测试:通过传入不同的数据即可验证图表行为。
// 推荐的组件封装模式
@ComponentV2
export struct MyChart {
@Local model: LineChartModel = new LineChartModel();
@Param data: DataPoint[] = [];
@Param config: ChartConfig = new ChartConfig();
@Monitor('data')
refreshChart(): void {
this.setChartData();
}
aboutToAppear(): void {
this.applyConfig();
this.setChartData();
}
private applyConfig(): void {
// 根据 config 配置图表外观
}
private setChartData(): void {
// 构建并设置图表数据
}
build() {
LineChart({ model: this.model })
}
}
4.2 状态管理最佳实践
使用 @Monitor 而非轮询:当外部数据源更新时,@Monitor 自动感知变化并触发图表重绘,无需手动调用刷新方法。这比定时器轮询或事件回调更高效,代码也更简洁。
使用 @Computed 缓存计算结果:对于数据汇总、百分比计算等耗时操作,使用 @Computed 装饰器可以避免在每次 UI 刷新时重复计算。
V1/V2 混用注意:如果项目尚未完全迁移到状态管理 V2,需注意在 V1 的 @Component 中使用 V2 的 @ComponentV2 子组件时,V1 组件自身的状态变化不会自动触发 V2 子组件的 @Monitor 回调。此时需要手动通过 @Param 传参或使用 emitter 事件总线来桥接。
4.3 性能优化建议
-
控制数据量:折线图数据点建议控制在 100 个以内,超过该数量应进行降采样处理。喵屿中的 HealthLineChart 通过
processData()方法将显示数据限制在 7 个点。 -
合理使用动画:
animateX和animateY的 duration 参数建议在 300-1000ms 之间。过短看不到动画效果,过长则影响交互响应。 -
空状态处理:始终为图表组件提供空数据状态的 UI 回退方案,如喵屿中的空状态占位图。
4.4 常见问题与解决
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 图表不显示 | 未调用 setData() 或数据为空 |
确保 setData() 在 aboutToAppear 生命周期中调用,且数据非空 |
| 数据更新后图表未刷新 | 未使用 @Monitor 监听数据变化 |
对数据输入属性添加 @Monitor 装饰器,并在回调中重新调用 setChartData() |
| 饼状图扇形过小无法选中 | 数据占比差异过大 | 使用 setMinAngleForSlices() 设置最小角度 |
| 折线图 X 轴标签重叠 | 数据点过多 | 控制可视范围(setVisibleXRangeMaximum)或自定义 ValueFormatter |
| 图表动画不流畅 | 数据量过大或渲染任务过重 | 减少数据点,降低动画时长,考虑分帧加载 |
4.5 总结
mpchart 作为 HarmonyOS 生态中最成熟的图表库之一,为鸿蒙原生应用提供了媲美 MPAndroidChart 的图表能力。通过喵屿的实践,我们总结了以下核心原则:
-
Model 驱动渲染:始终通过 Model 对象配置图表,Chart 组件仅作为渲染容器。这种分离使得图表配置逻辑可独立于 UI 布局进行测试和维护。
-
V2 状态管理优先:充分利用
@ComponentV2+@Local+@Param+@Monitor的组合,实现声明式的数据驱动图表更新。 -
封装即复用:将图表封装为参数化的通用组件,通过
@Param暴露可配置项,在不同场景中按需实例化。 -
防御性编程:始终处理空数据、异常值等边界情况,提供友好的空状态 UI,保证应用的健壮性。
-
渐进增强:从基础的图表展示开始,逐步叠加动画、交互、自定义样式等高级功能,避免过度设计。
借助 mpchart 丰富的图表类型和灵活的 API,开发者可以在 HarmonyOS 应用中快速构建专业、美观的数据可视化界面。
更多推荐


所有评论(0)