如何用 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 层(如 LineChartModelPieChartModel):负责数据管理、坐标计算、渲染调度和交互处理。
  • View 层(如 LineChartPieChart):封装 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,
];

引出线精细控制:通过调整 setValueLinePart1OffsetPercentagesetValueLinePart1LengthsetValueLinePart2Length 三个参数,实现引出线从饼图扇形外侧优雅地延伸到百分比标签位置,避免标签重叠。

最小角度保护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 性能优化建议

  1. 控制数据量:折线图数据点建议控制在 100 个以内,超过该数量应进行降采样处理。喵屿中的 HealthLineChart 通过 processData() 方法将显示数据限制在 7 个点。

  2. 合理使用动画animateXanimateY 的 duration 参数建议在 300-1000ms 之间。过短看不到动画效果,过长则影响交互响应。

  3. 空状态处理:始终为图表组件提供空数据状态的 UI 回退方案,如喵屿中的空状态占位图。

4.4 常见问题与解决

问题 原因 解决方案
图表不显示 未调用 setData() 或数据为空 确保 setData()aboutToAppear 生命周期中调用,且数据非空
数据更新后图表未刷新 未使用 @Monitor 监听数据变化 对数据输入属性添加 @Monitor 装饰器,并在回调中重新调用 setChartData()
饼状图扇形过小无法选中 数据占比差异过大 使用 setMinAngleForSlices() 设置最小角度
折线图 X 轴标签重叠 数据点过多 控制可视范围(setVisibleXRangeMaximum)或自定义 ValueFormatter
图表动画不流畅 数据量过大或渲染任务过重 减少数据点,降低动画时长,考虑分帧加载

4.5 总结

mpchart 作为 HarmonyOS 生态中最成熟的图表库之一,为鸿蒙原生应用提供了媲美 MPAndroidChart 的图表能力。通过喵屿的实践,我们总结了以下核心原则:

  1. Model 驱动渲染:始终通过 Model 对象配置图表,Chart 组件仅作为渲染容器。这种分离使得图表配置逻辑可独立于 UI 布局进行测试和维护。

  2. V2 状态管理优先:充分利用 @ComponentV2 + @Local + @Param + @Monitor 的组合,实现声明式的数据驱动图表更新。

  3. 封装即复用:将图表封装为参数化的通用组件,通过 @Param 暴露可配置项,在不同场景中按需实例化。

  4. 防御性编程:始终处理空数据、异常值等边界情况,提供友好的空状态 UI,保证应用的健壮性。

  5. 渐进增强:从基础的图表展示开始,逐步叠加动画、交互、自定义样式等高级功能,避免过度设计。

借助 mpchart 丰富的图表类型和灵活的 API,开发者可以在 HarmonyOS 应用中快速构建专业、美观的数据可视化界面。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐