引言:数据可视化是解读数据的 “直观语言”

在开源鸿蒙(OpenHarmony)Flutter 应用中,数据可视化将枯燥的数字转化为直观的图表、仪表盘,帮助用户快速理解数据趋势和核心指标 —— 从个人健康数据统计,到企业级数据看板,数据可视化是提升应用价值的重要手段。

本文将以 “场景化实战” 为核心,从基础的图表库选型、常见图表实现,到进阶的交互式图表、自定义图表样式,再到开源鸿蒙特有的分布式数据看板,用 “原理 + 精简代码 + 多设备适配” 的方式,带你打造适配全场景的数据可视化方案。

一、图表库选型:FlChart vs charts_flutter

开源鸿蒙 Flutter 应用开发中,常用的数据可视化库有FlChartcharts_flutter,两者各有优势:

维度 FlChart charts_flutter
学习成本 中(API 清晰,文档丰富) 中(基于 Material Design,风格统一)
自定义程度 高(支持细节样式自定义) 中(样式相对固定,定制需额外开发)
性能 优秀(渲染高效,适合大数据) 较好(大数据场景需优化)
图表类型 丰富(折线图、柱状图、饼图等) 丰富(支持地图、热力图等特殊图表)
开源鸿蒙适配 良好(无原生依赖,直接使用) 良好(需确保版本兼容)

选型建议

  • 需高度自定义样式、追求渲染性能 → 选择 FlChart;
  • 偏好 Material Design 风格、需要特殊图表(如地图) → 选择 charts_flutter;
  • 本文以FlChart为例展开,其更适合开源鸿蒙多设备适配场景。

二、基础实战:常见图表实现(FlChart)

2.1 环境配置

yaml

dependencies:
  fl_chart: ^0.55.2

2.2 实战 1:折线图(数据趋势展示)

适合展示连续数据的变化趋势(如气温变化、销售额走势)。

dart

import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';

class LineChartPage extends StatelessWidget {
  const LineChartPage({super.key});

  // 模拟一周销售额数据
  final List<FlSpot> salesData = const [
    FlSpot(0, 1200), // 周一
    FlSpot(1, 1500), // 周二
    FlSpot(2, 1100), // 周三
    FlSpot(3, 1800), // 周四
    FlSpot(4, 1600), // 周五
    FlSpot(5, 2000), // 周六
    FlSpot(6, 2200), // 周日
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('一周销售额趋势')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: LineChart(
          LineChartData(
            gridData: const FlGridData(show: true), // 显示网格
            titlesData: FlTitlesData(
              bottomTitles: AxisTitles(
                sideTitles: SideTitles(
                  showTitles: true,
                  interval: 1, // 每隔1个点显示标签
                  getTitlesWidget: (value, meta) {
                    const weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
                    return Text(weekDays[value.toInt()]);
                  },
                ),
              ),
              leftTitles: AxisTitles(
                sideTitles: SideTitles(
                  showTitles: true,
                  interval: 500, // 每隔500显示刻度
                  getTitlesWidget: (value, meta) => Text('${value.toInt()}元'),
                ),
              ),
            ),
            borderData: FlBorderData(show: true), // 显示边框
            minX: 0,
            maxX: 6,
            minY: 0,
            maxY: 2500,
            lineBarsData: [
              LineChartBarData(
                spots: salesData,
                isCurved: true, // 曲线平滑
                color: Colors.blue,
                width: 3,
                dotData: const FlDotData(show: true), // 显示数据点
                belowBarData: BarAreaData(
                  show: true,
                  color: Colors.blue.withOpacity(0.1), // 填充下方区域
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

2.3 实战 2:柱状图(分类数据对比)

适合展示不同分类数据的对比(如各部门业绩、产品销量)。

dart

class BarChartPage extends StatelessWidget {
  const BarChartPage({super.key});

  // 模拟各部门业绩数据
  final List<double> departmentPerformance = [85, 68, 92, 76, 88];
  final List<String> departments = ['研发部', '市场部', '销售部', '财务部', '人事部'];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('各部门季度业绩')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: BarChart(
          BarChartData(
            gridData: const FlGridData(show: false),
            titlesData: FlTitlesData(
              bottomTitles: AxisTitles(
                sideTitles: SideTitles(
                  showTitles: true,
                  interval: 1,
                  getTitlesWidget: (value, meta) => Text(departments[value.toInt()]),
                ),
              ),
              leftTitles: AxisTitles(
                sideTitles: SideTitles(
                  showTitles: true,
                  interval: 20,
                  getTitlesWidget: (value, meta) => Text('${value.toInt()}分'),
                ),
              ),
            ),
            borderData: FlBorderData(show: true),
            minY: 0,
            maxY: 100,
            barGroupsSpace: 20, // 柱状图组间距
            barTouchData: BarTouchData(enabled: true), // 支持点击交互
            barData: [
              BarChartGroupData(
                showingTooltipIndicators: [0, 1, 2, 3, 4],
                barRods: departmentPerformance.map((performance) {
                  return BarChartRodData(
                    toY: performance,
                    color: performance >= 90 ? Colors.green : Colors.blue, // 90分以上绿色
                    width: 20,
                    borderRadius: BorderRadius.circular(4),
                  );
                }).toList(),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

2.4 实战 3:饼图(占比数据展示)

适合展示各部分占总体的比例(如收入构成、用户来源分布)。

dart

class PieChartPage extends StatelessWidget {
  const PieChartPage({super.key});

  // 模拟用户来源分布数据
  final List<PieChartSectionData> userSourceData = [
    PieChartSectionData(
      value: 45,
      color: Colors.blue,
      title: 'APP商店',
      titleStyle: const TextStyle(color: Colors.white, fontSize: 14),
    ),
    PieChartSectionData(
      value: 30,
      color: Colors.green,
      title: '社交媒体',
      titleStyle: const TextStyle(color: Colors.white, fontSize: 14),
    ),
    PieChartSectionData(
      value: 15,
      color: Colors.orange,
      title: '搜索引擎',
      titleStyle: const TextStyle(color: Colors.white, fontSize: 14),
    ),
    PieChartSectionData(
      value: 10,
      color: Colors.purple,
      title: '其他渠道',
      titleStyle: const TextStyle(color: Colors.white, fontSize: 14),
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('用户来源分布')),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: PieChart(
            PieChartData(
              sections: userSourceData,
              sectionsSpace: 2, // 各部分间距
              centerSpaceRadius: 60, // 中心空白半径
              pieTouchData: PieTouchData(enabled: true), // 支持点击交互
              titleData: const FlTitleData(
                show: true,
                title: Text('用户来源占比', style: TextStyle(fontSize: 18)),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

三、进阶实战:交互式图表与自定义样式

3.1 交互式图表(点击显示详情)

通过TouchData实现图表交互,点击数据点 / 柱状图显示详细信息。

dart

class InteractiveLineChartPage extends StatefulWidget {
  const InteractiveLineChartPage({super.key});

  @override
  State<InteractiveLineChartPage> createState() => _InteractiveLineChartPageState();
}

class _InteractiveLineChartPageState extends State<InteractiveLineChartPage> {
  final List<FlSpot> salesData = const [
    FlSpot(0, 1200),
    FlSpot(1, 1500),
    FlSpot(2, 1100),
    FlSpot(3, 1800),
    FlSpot(4, 1600),
    FlSpot(5, 2000),
    FlSpot(6, 2200),
  ];
  final List<String> weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
  String? _selectedDay;
  double? _selectedSales;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('交互式销售额趋势')),
      body: Column(
        children: [
          // 显示选中的数据详情
          if (_selectedDay != null && _selectedSales != null)
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Text(
                '$_selectedDay 销售额:${_selectedSales?.toInt()}元',
                style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
            ),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: LineChart(
                LineChartData(
                  gridData: const FlGridData(show: true),
                  titlesData: FlTitlesData(
                    bottomTitles: AxisTitles(
                      sideTitles: SideTitles(
                        showTitles: true,
                        interval: 1,
                        getTitlesWidget: (value, meta) => Text(weekDays[value.toInt()]),
                      ),
                    ),
                    leftTitles: AxisTitles(
                      sideTitles: SideTitles(
                        showTitles: true,
                        interval: 500,
                        getTitlesWidget: (value, meta) => Text('${value.toInt()}元'),
                      ),
                    ),
                  ),
                  borderData: FlBorderData(show: true),
                  minX: 0,
                  maxX: 6,
                  minY: 0,
                  maxY: 2500,
                  lineBarsData: [
                    LineChartBarData(
                      spots: salesData,
                      isCurved: true,
                      color: Colors.blue,
                      width: 3,
                      dotData: const FlDotData(show: true),
                    ),
                  ],
                  // 交互配置
                  lineTouchData: LineTouchData(
                    enabled: true,
                    touchTooltipData: LineTouchTooltipData(
                      getTooltipItems: (touchedSpots) {
                        return touchedSpots.map((spot) {
                          final day = weekDays[spot.x.toInt()];
                          final sales = spot.y.toInt();
                          // 更新选中状态
                          setState(() {
                            _selectedDay = day;
                            _selectedSales = spot.y;
                          });
                          return LineTooltipItem(
                            '$day: $sales元',
                            const TextStyle(color: Colors.white),
                          );
                        }).toList();
                      },
                    ),
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

3.2 自定义图表样式(适配开源鸿蒙主题)

结合开源鸿蒙系统主题,自定义图表颜色、字体,实现风格统一。

dart

class ThemedBarChartPage extends StatelessWidget {
  const ThemedBarChartPage({super.key});

  final List<double> data = [85, 68, 92, 76, 88];
  final List<String> labels = ['研发部', '市场部', '销售部', '财务部', '人事部'];

  @override
  Widget build(BuildContext context) {
    // 获取开源鸿蒙系统主题色
    final theme = Theme.of(context);
    final primaryColor = theme.primaryColor;
    final secondaryColor = theme.colorScheme.secondary;

    return Scaffold(
      appBar: AppBar(title: const Text('主题化部门业绩图表')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: BarChart(
          BarChartData(
            gridData: FlGridData(
              show: true,
              gridColor: theme.dividerColor.withOpacity(0.3), // 网格色适配主题
            ),
            titlesData: FlTitlesData(
              bottomTitles: AxisTitles(
                sideTitles: SideTitles(
                  showTitles: true,
                  interval: 1,
                  getTitlesWidget: (value, meta) => Text(
                    labels[value.toInt()],
                    style: theme.textTheme.bodyMedium, // 字体适配主题
                  ),
                ),
              ),
              leftTitles: AxisTitles(
                sideTitles: SideTitles(
                  showTitles: true,
                  interval: 20,
                  getTitlesWidget: (value, meta) => Text(
                    '${value.toInt()}分',
                    style: theme.textTheme.bodyMedium,
                  ),
                ),
              ),
            ),
            borderData: FlBorderData(
              show: true,
              borderColor: theme.dividerColor,
            ),
            minY: 0,
            maxY: 100,
            barData: [
              BarChartGroupData(
                barRods: data.map((value) {
                  return BarChartRodData(
                    toY: value,
                    // 自定义颜色:优秀(≥90)用主题强调色,良好用主题主色
                    color: value >= 90 ? secondaryColor : primaryColor,
                    width: 20,
                    borderRadius: BorderRadius.circular(4),
                    // 柱状图渐变效果
                    gradient: LinearGradient(
                      colors: [
                        value >= 90 ? secondaryColor.withOpacity(0.8) : primaryColor.withOpacity(0.8),
                        value >= 90 ? secondaryColor : primaryColor,
                      ],
                      begin: Alignment.topCenter,
                      end: Alignment.bottomCenter,
                    ),
                  );
                }).toList(),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

四、高阶实战:开源鸿蒙分布式数据看板

4.1 分布式数据看板核心逻辑

开源鸿蒙分布式数据看板实现 “多设备数据实时同步 + 可视化展示”,核心流程:

  1. 数据采集:源设备(如手机)采集数据(如销售数据、健康数据);
  2. 数据同步:通过分布式存储将数据实时同步到其他设备(如平板、智慧屏);
  3. 可视化渲染:各设备监听分布式存储变化,实时更新图表展示;
  4. 多设备适配:根据设备屏幕尺寸(手机→智慧屏)调整图表布局和大小。

4.2 实战:分布式销售数据看板

步骤 1:分布式数据同步工具类

dart

// distributed_store_util.dart
import 'package:ohos_shared_preferences/ohos_shared_preferences.dart';
import 'dart:convert';

class DistributedStoreUtil {
  static final SharedPreferences _prefs = SharedPreferences.getInstance() as SharedPreferences;

  // 保存销售数据到分布式存储
  static Future<void> saveSalesData(List<double> data) async {
    await _prefs.setString('distributed_sales_data', jsonEncode(data));
  }

  // 获取分布式销售数据
  static Future<List<double>> getSalesData() async {
    final dataStr = _prefs.getString('distributed_sales_data') ?? '[]';
    final List<dynamic> dataList = jsonDecode(dataStr);
    return dataList.map((e) => e.toDouble()).toList();
  }

  // 监听数据变化(回调通知)
  static void listenSalesData(void Function(List<double>) callback) {
    _prefs.addListener(() async {
      final data = await getSalesData();
      callback(data);
    });
  }
}
步骤 2:数据采集端(手机)

dart

class DataCollectionPage extends StatefulWidget {
  const DataCollectionPage({super.key});

  @override
  State<DataCollectionPage> createState() => _DataCollectionPageState();
}

class _DataCollectionPageState extends State<DataCollectionPage> {
  final List<double> _salesData = [1200, 1500, 1100, 1800, 1600, 2000, 2200];
  int _currentDay = 0;

  // 模拟实时更新数据(每天增加随机销售额)
  void _updateData() {
    if (_currentDay < 7) {
      setState(() {
        _salesData[_currentDay] += (100 + Random().nextInt(300)).toDouble();
      });
      // 同步到分布式存储
      DistributedStoreUtil.saveSalesData(_salesData);
      _currentDay++;
      // 3秒后更新下一天数据
      Future.delayed(const Duration(seconds: 3), _updateData);
    }
  }

  @override
  void initState() {
    super.initState();
    // 初始化时同步初始数据
    DistributedStoreUtil.saveSalesData(_salesData);
    // 启动模拟数据更新
    Future.delayed(const Duration(seconds: 2), _updateData);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('数据采集端(手机)')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            const Text('实时销售数据采集', style: TextStyle(fontSize: 18)),
            const SizedBox(height: 20),
            Expanded(
              child: LineChart(
                LineChartData(
                  gridData: const FlGridData(show: true),
                  titlesData: FlTitlesData(
                    bottomTitles: AxisTitles(
                      sideTitles: SideTitles(
                        showTitles: true,
                        interval: 1,
                        getTitlesWidget: (value, meta) {
                          const weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
                          return Text(weekDays[value.toInt()]);
                        },
                      ),
                    ),
                  ),
                  borderData: FlBorderData(show: true),
                  minX: 0,
                  maxX: 6,
                  minY: 1000,
                  maxY: 3000,
                  lineBarsData: [
                    LineChartBarData(
                      spots: _salesData.asMap().entries.map((e) {
                        return FlSpot(e.key.toDouble(), e.value);
                      }).toList(),
                      isCurved: true,
                      color: Colors.blue,
                      width: 3,
                      dotData: const FlDotData(show: true),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
步骤 3:数据展示端(平板 / 智慧屏)

dart

class DistributedDashboardPage extends StatefulWidget {
  const DistributedDashboardPage({super.key});

  @override
  State<DistributedDashboardPage> createState() => _DistributedDashboardPageState();
}

class _DistributedDashboardPageState extends State<DistributedDashboardPage> {
  List<double> _salesData = [];
  final List<String> _weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];

  @override
  void initState() {
    super.initState();
    // 初始化获取数据
    _initData();
    // 监听分布式数据变化
    DistributedStoreUtil.listenSalesData((data) {
      setState(() {
        _salesData = data;
      });
    });
  }

  Future<void> _initData() async {
    final data = await DistributedStoreUtil.getSalesData();
    setState(() {
      _salesData = data;
    });
  }

  @override
  Widget build(BuildContext context) {
    // 根据屏幕宽度适配布局:手机(单列)、平板/智慧屏(双列)
    final isLargeScreen = MediaQuery.of(context).size.width > 600;

    return Scaffold(
      appBar: AppBar(title: const Text('分布式数据看板(平板/智慧屏)')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: isLargeScreen
            ? Row(
                children: [
                  // 左侧折线图(趋势)
                  Expanded(child: _buildLineChart()),
                  const SizedBox(width: 20),
                  // 右侧柱状图(对比)
                  Expanded(child: _buildBarChart()),
                ],
              )
            : Column(
                children: [
                  Expanded(child: _buildLineChart()),
                  const SizedBox(height: 20),
                  Expanded(child: _buildBarChart()),
                ],
              ),
      ),
    );
  }

  // 构建折线图
  Widget _buildLineChart() {
    if (_salesData.isEmpty) return const Center(child: Text('加载中...'));

    return LineChart(
      LineChartData(
        gridData: const FlGridData(show: true),
        titlesData: FlTitlesData(
          bottomTitles: AxisTitles(
            sideTitles: SideTitles(
              showTitles: true,
              interval: 1,
              getTitlesWidget: (value, meta) => Text(_weekDays[value.toInt()]),
            ),
          ),
          leftTitles: AxisTitles(
            sideTitles: SideTitles(
              showTitles: true,
              interval: 500,
              getTitlesWidget: (value, meta) => Text('${value.toInt()}元'),
            ),
          ),
        ),
        borderData: FlBorderData(show: true),
        minX: 0,
        maxX: 6,
        minY: 1000,
        maxY: 3000,
        lineBarsData: [
          LineChartBarData(
            spots: _salesData.asMap().entries.map((e) {
              return FlSpot(e.key.toDouble(), e.value);
            }).toList(),
            isCurved: true,
            color: Colors.blue,
            width: 3,
            dotData: const FlDotData(show: true),
            belowBarData: BarAreaData(show: true, color: Colors.blue.withOpacity(0.1)),
          ),
        ],
      ),
    );
  }

  // 构建柱状图
  Widget _buildBarChart() {
    if (_salesData.isEmpty) return const Center(child: Text('加载中...'));

    return BarChart(
      BarChartData(
        gridData: const FlGridData(show: false),
        titlesData: FlTitlesData(
          bottomTitles: AxisTitles(
            sideTitles: SideTitles(
              showTitles: true,
              interval: 1,
              getTitlesWidget: (value, meta) => Text(_weekDays[value.toInt()]),
            ),
          ),
          leftTitles: AxisTitles(
            sideTitles: SideTitles(
              showTitles: true,
              interval: 500,
              getTitlesWidget: (value, meta) => Text('${value.toInt()}元'),
            ),
          ),
        ),
        borderData: FlBorderData(show: true),
        minY: 1000,
        maxY: 3000,
        barData: [
          BarChartGroupData(
            barRods: _salesData.map((value) {
              return BarChartRodData(
                toY: value,
                color: Colors.green,
                width: 20,
                borderRadius: BorderRadius.circular(4),
              );
            }).toList(),
          ),
        ],
      ),
    );
  }
}

五、多设备适配与性能优化

5.1 多设备适配技巧

  • 屏幕尺寸适配:通过MediaQuery获取屏幕宽度,动态调整图表布局(单列 / 双列)、大小和字体;
  • 设备类型适配:针对手机(小屏)简化图表细节(如隐藏网格、减小字体),针对智慧屏(大屏)增加图表维度和交互细节;
  • 系统主题适配:复用开源鸿蒙系统主题色、字体,确保图表与系统风格统一。

5.2 性能优化(大数据场景)

  • 数据采样:当数据量超过 1000 条时,对数据进行采样(如每隔 5 条取 1 条),减少渲染压力;
  • 懒加载:图表初始化时只加载部分数据,滚动时再加载剩余数据;
  • 避免频繁重建:使用StatefulWidget缓存图表数据,仅在数据变化时触发重建;
  • 硬件加速:确保启用 Flutter 硬件加速,提升图表渲染帧率。

六、常见问题(FAQ)

Q1:图表在开源鸿蒙智慧屏上显示模糊怎么办?

A1:1. 设置图表widthheight为具体数值(而非百分比),确保适配大屏分辨率;2. 增加图表字体大小和线条宽度;3. 关闭图表缩放功能,避免拉伸模糊。

Q2:分布式数据同步延迟如何解决?

A2:1. 优化数据格式(如使用 JSON 而非 XML,减少数据体积);2. 减少同步频率(如批量同步而非单条同步);3. 优先使用鸿蒙分布式网络(如 Wi-Fi Direct),提升传输速度。

Q3:如何实现 3D 图表效果?

A3:FlChart 本身不支持 3D 效果,可通过以下方式实现:1. 使用Transform添加透视变换;2. 自定义BarChartRodData的渐变和阴影,模拟 3D 立体感;3. 集成第三方 3D 图表库(如flutter_3d_charts)。

结语:构建开源鸿蒙全场景数据可视化体系

数据可视化是连接数据与用户的桥梁,从基础的折线图、柱状图,到交互式图表、分布式数据看板,每个场景都需要结合设备特性和业务需求进行设计。

通过本文的实战案例,你已经掌握了 FlChart 的核心用法、交互式图表开发、分布式数据同步和多设备适配技巧,能够在开源鸿蒙手机、平板、智慧屏等设备上实现高效、美观的数据可视化效果。合理运用数据可视化,能让你的应用在全场景生态中更具竞争力,为用户提供更直观的数据解读体验。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐