在这里插入图片描述

说起数据统计,我自己以前总觉得这是个可有可无的功能。但用了一段时间记账App后,我发现数据统计真的很有用。看着图表,能清楚地知道自己的习惯完成情况、收支状况、任务进度。这种可视化的反馈,比单纯的数字列表有用多了。

为什么数据可视化这么重要

在开始写代码之前,我先想清楚了这个功能的价值。

第一个是直观性。人脑对图形的处理速度比文字快得多。一张图表能传达的信息,可能需要一大段文字才能说清楚。比如看到一条上升的曲线,就知道趋势在变好;看到一个大的饼图扇区,就知道占比很高。

第二个是激励作用。看着习惯完成率从60%提升到85%,会有成就感。看着收入曲线一点点上升,会有动力继续努力。这种正向反馈对坚持很重要。

第三个是发现问题。通过数据分析,能发现一些平时注意不到的问题。比如发现某个月支出突然增加,就要反思是不是花钱太随意了。发现某个习惯完成率很低,就要想办法改进。

功能设计的思路

在设计这个功能时,我考虑了以下几个方面。

多维度的统计

不能只统计一个维度,要从多个角度展示数据。比如习惯统计、财务统计、任务统计,每个维度都有自己的图表。

多种图表类型

不同的数据适合不同的图表。趋势数据用折线图,对比数据用柱状图,占比数据用饼图。选对图表类型,能让数据更清晰。

概览卡片

在图表上方放几个概览卡片,显示关键指标。比如总习惯数、总支出、完成率。这样用户不用看图表,就能知道大概情况。

页面整体结构

先看页面的基本框架:

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('数据统计'),
      ),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(16.w),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _buildOverviewCards(),
            SizedBox(height: 24.h),
            _buildHabitChart(),
            SizedBox(height: 24.h),
            _buildFinanceChart(),
            SizedBox(height: 24.h),
            _buildTaskChart(),
          ],
        ),
      ),
    );
  }
}

StatelessWidget的选择

这里用了StatelessWidget,因为页面只是展示数据,没有用户交互导致的状态变化。数据从外部传入或从数据库读取,不需要在页面内部管理状态。

页面布局的四个部分

页面分成四块:概览卡片、习惯图表、财务图表、任务图表。用SizedBox隔开,间距设置为24,看起来不会太挤。

外层用SingleChildScrollView包裹,这样内容超出屏幕时可以滚动。

概览卡片的设计

页面顶部是三个概览卡片,显示关键指标:

Widget _buildOverviewCards() {
  return Row(
    children: [
      Expanded(
        child: _buildCard('总习惯数', '12', Colors.green),
      ),
      SizedBox(width: 12.w),
      Expanded(
        child: _buildCard('总支出', '¥3.2K', Colors.red),
      ),
      SizedBox(width: 12.w),
      Expanded(
        child: _buildCard('完成率', '85%', Colors.blue),
      ),
    ],
  );
}

Widget _buildCard(String label, String value, Color color) {
  return Container(
    padding: EdgeInsets.all(16.w),
    decoration: BoxDecoration(
      color: color.withOpacity(0.1),
      borderRadius: BorderRadius.circular(12.r),
    ),
    child: Column(
      children: [
        Text(
          value,
          style: TextStyle(
            fontSize: 24.sp,
            fontWeight: FontWeight.bold,
            color: color,
          ),
        ),
        SizedBox(height: 4.h),
        Text(
          label,
          style: TextStyle(fontSize: 12.sp, color: Colors.grey),
        ),
      ],
    ),
  );
}

Row的均分布局

三个卡片用Row横向排列,每个卡片用Expanded包裹,这样它们会均分宽度。卡片之间用SizedBox隔开,间距12。

卡片的颜色设计

每个卡片有自己的主题色:

  • 总习惯数用绿色,绿色代表健康、成长
  • 总支出用红色,红色代表支出、警示
  • 完成率用蓝色,蓝色代表理性、稳定

背景色是主题色的10%透明度,这样既有颜色区分,又不会太刺眼。

数值的视觉层级

数值用24号大字,加粗,主题色。标签用12号小字,灰色。这种层次关系让用户的视线自然地被数值吸引。

数值放在上面,标签放在下面,符合从重要到次要的阅读顺序。

习惯完成趋势图

第一个图表是习惯完成趋势,用折线图展示:

Widget _buildHabitChart() {
  return Container(
    padding: EdgeInsets.all(16.w),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(12.r),
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          '习惯完成趋势',
          style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
        ),
        SizedBox(height: 20.h),
        SizedBox(
          height: 200.h,
          child: LineChart(
            LineChartData(
              gridData: const FlGridData(show: false),
              titlesData: const FlTitlesData(
                leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
                rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
                topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
                bottomTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
              ),
              borderData: FlBorderData(show: false),
              lineBarsData: [
                LineChartBarData(
                  spots: const [
                    FlSpot(0, 3),
                    FlSpot(1, 4),
                    FlSpot(2, 3.5),
                    FlSpot(3, 5),
                    FlSpot(4, 4),
                    FlSpot(5, 6),
                    FlSpot(6, 5.5),
                  ],
                  isCurved: true,
                  color: Colors.green,
                  barWidth: 3,
                  dotData: const FlDotData(show: true),
                ),
              ],
            ),
          ),
        ),
      ],
    ),
  );
}

为什么选择折线图

习惯完成趋势是时间序列数据,折线图最合适。折线的走向能清楚地展示趋势:上升表示进步,下降表示退步,平稳表示保持。

LineChart的关键参数

gridData: const FlGridData(show: false)隐藏网格线,让图表更简洁。网格线虽然能帮助读数,但在这个场景下不是必需的,反而会让图表显得杂乱。

titlesData设置所有轴的标签都不显示。因为这是个趋势图,重点是看走向,不是看具体数值。

borderData: FlBorderData(show: false)隐藏边框,和网格线一样,去掉不必要的元素。

折线的样式

isCurved: true让折线变成曲线,看起来更平滑。color: Colors.green用绿色,和习惯的主题色一致。barWidth: 3设置线宽为3,不会太粗也不会太细。

dotData: const FlDotData(show: true)显示数据点,让用户知道每个点的位置。

数据点的设计

数据点用FlSpot表示,第一个参数是X坐标,第二个参数是Y坐标。这里有7个点,代表一周的数据。

从数据可以看出,习惯完成数量在波动,但整体趋势是上升的。这种可视化比单纯的数字列表直观多了。

收支对比图

第二个图表是收支对比,用柱状图展示:

Widget _buildFinanceChart() {
  return Container(
    padding: EdgeInsets.all(16.w),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(12.r),
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          '收支对比',
          style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
        ),
        SizedBox(height: 20.h),
        SizedBox(
          height: 200.h,
          child: BarChart(
            BarChartData(
              alignment: BarChartAlignment.spaceAround,
              maxY: 10000,
              barTouchData: BarTouchData(enabled: false),
              titlesData: const FlTitlesData(
                show: false,
              ),
              borderData: FlBorderData(show: false),
              barGroups: [
                BarChartGroupData(x: 0, barRods: [
                  BarChartRodData(toY: 8000, color: Colors.green),
                  BarChartRodData(toY: 3000, color: Colors.red),
                ]),
                BarChartGroupData(x: 1, barRods: [
                  BarChartRodData(toY: 8200, color: Colors.green),
                  BarChartRodData(toY: 3200, color: Colors.red),
                ]),
                BarChartGroupData(x: 2, barRods: [
                  BarChartRodData(toY: 8500, color: Colors.green),
                  BarChartRodData(toY: 3500, color: Colors.red),
                ]),
              ],
            ),
          ),
        ),
      ],
    ),
  );
}

为什么选择柱状图

收支对比是两组数据的对比,柱状图最合适。两根柱子并排,高度对比很直观。绿色柱子是收入,红色柱子是支出,颜色也有明确的含义。

BarChart的关键参数

alignment: BarChartAlignment.spaceAround让柱子均匀分布,两边留白。maxY: 10000设置Y轴最大值,这个值要根据实际数据动态调整。

barTouchData: BarTouchData(enabled: false)禁用触摸交互,因为这个图表只是展示,不需要交互。

分组柱状图的实现

每个BarChartGroupData代表一组柱子,x是位置,barRods是这组里的柱子。

每组有两根柱子,第一根是收入(绿色),第二根是支出(红色)。这样三个月的收支对比一目了然。

从数据可以看出,收入在稳步增长,支出也在增长,但增长幅度小于收入。这是个好趋势。

任务完成情况图

第三个图表是任务完成情况,用饼图展示:

Widget _buildTaskChart() {
  return Container(
    padding: EdgeInsets.all(16.w),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(12.r),
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          '任务完成情况',
          style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
        ),
        SizedBox(height: 20.h),
        SizedBox(
          height: 200.h,
          child: PieChart(
            PieChartData(
              sectionsSpace: 2,
              centerSpaceRadius: 40.r,
              sections: [
                PieChartSectionData(
                  value: 85,
                  title: '85%',
                  color: Colors.green,
                  radius: 50.r,
                ),
                PieChartSectionData(
                  value: 15,
                  title: '15%',
                  color: Colors.grey,
                  radius: 50.r,
                ),
              ],
            ),
          ),
        ),
        SizedBox(height: 16.h),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            _buildLegend('已完成', Colors.green),
            SizedBox(width: 20.w),
            _buildLegend('未完成', Colors.grey),
          ],
        ),
      ],
    ),
  );
}

Widget _buildLegend(String label, Color color) {
  return Row(
    children: [
      Container(
        width: 12.w,
        height: 12.w,
        decoration: BoxDecoration(
          color: color,
          shape: BoxShape.circle,
        ),
      ),
      SizedBox(width: 4.w),
      Text(label, style: TextStyle(fontSize: 12.sp)),
    ],
  );
}

为什么选择饼图

任务完成情况是占比数据,饼图最合适。扇区的大小直观地展示了占比关系。绿色扇区占85%,一眼就能看出完成率很高。

PieChart的关键参数

sectionsSpace: 2设置扇区之间的间隔为2,让扇区之间有个小缝隙,看起来更清晰。

centerSpaceRadius: 40.r设置中心空白区域的半径为40,这样饼图就变成了环形图。环形图比实心饼图更现代,也更容易看清占比。

扇区的设计

每个PieChartSectionData代表一个扇区,value是数值,title是显示的文字,color是颜色,radius是半径。

这里有两个扇区:已完成(绿色,85%)和未完成(灰色,15%)。绿色表示正向,灰色表示中性。

图例的必要性

饼图下方加了图例,说明每个颜色代表什么。虽然颜色的含义比较明显,但加上图例更规范,避免歧义。

图例用小圆点和文字组成,简洁明了。

数据的动态加载

实际项目中,数据应该从数据库读取,而不是硬编码。可以用FutureBuilder来处理异步数据加载:

FutureBuilder<StatisticsData>(
  future: _loadStatisticsData(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(child: CircularProgressIndicator());
    }
    
    if (snapshot.hasError) {
      return Center(child: Text('加载失败:${snapshot.error}'));
    }
    
    if (!snapshot.hasData) {
      return const Center(child: Text('暂无数据'));
    }
    
    final data = snapshot.data!;
    return _buildCharts(data);
  },
)

三种状态的处理

  • 加载中:显示loading指示器
  • 加载失败:显示错误信息
  • 加载成功:显示图表

这种处理方式很规范,用户体验也好。

图表的交互增强

虽然目前图表只是展示,但可以加一些交互,让用户能看到更多信息。

折线图的触摸交互

用户触摸折线图时,显示该点的具体数值:

LineTouchData(
  touchTooltipData: LineTouchTooltipData(
    tooltipBgColor: Colors.blueGrey.withOpacity(0.8),
    getTooltipItems: (touchedSpots) {
      return touchedSpots.map((spot) {
        return LineTooltipItem(
          '${spot.y.toInt()} 个',
          const TextStyle(color: Colors.white),
        );
      }).toList();
    },
  ),
)

柱状图的触摸交互

用户触摸柱状图时,显示该柱子的具体数值:

BarTouchData(
  touchTooltipData: BarTouchTooltipData(
    tooltipBgColor: Colors.blueGrey.withOpacity(0.8),
    getTooltipItem: (group, groupIndex, rod, rodIndex) {
      return BarTooltipItem(
        ${rod.toY.toInt()}',
        const TextStyle(color: Colors.white),
      );
    },
  ),
)

饼图的触摸交互

用户触摸饼图时,该扇区会稍微突出:

PieTouchData(
  touchCallback: (FlTouchEvent event, pieTouchResponse) {
    setState(() {
      if (event is FlTapUpEvent && pieTouchResponse != null) {
        final touchedIndex = pieTouchResponse.touchedSection?.touchedSectionIndex;
        _touchedIndex = touchedIndex;
      }
    });
  },
)

实际使用体验

这个数据统计页面我自己用了一段时间,感觉很有用。每周看一次,就能知道自己的进步情况。

概览卡片很直观,一眼就能看到关键指标。习惯完成趋势图让我知道自己是在进步还是退步。收支对比图让我知道财务状况是否健康。任务完成情况图让我知道自己的执行力如何。

这些图表给了我很多正向反馈。看到习惯完成率从60%提升到85%,会很有成就感。看到收入曲线上升,会更有动力工作。

可以改进的地方

如果要做得更完善,可以考虑以下几点。

时间范围的筛选

用户可能想看不同时间段的数据,比如本周、本月、本年。可以加个时间选择器,让用户自己选择。

更多维度的统计

除了习惯、财务、任务,还可以统计健康数据、学习数据等。每个维度都有自己的图表。

数据的导出

用户可能想把数据导出成Excel或PDF,方便分析或分享。可以加个导出按钮。

对比分析

可以加个同比环比的功能,比如"本月收入比上月增长10%",“本周习惯完成率比上周提高5%”。这种对比能让用户更清楚地看到变化。

目标设定

用户可以设定目标,比如"本月收入目标10000元",“习惯完成率目标90%”。图表上显示目标线,让用户知道距离目标还有多远。

小结

今天实现了数据统计可视化功能,用到了折线图、柱状图、饼图等图表。核心是用fl_chart包绘制图表,让数据以可视化的方式呈现。

这个功能虽然不是最核心的,但对用户价值很大。数据可视化能让用户更直观地了解自己的情况,发现问题,获得激励。

在实现过程中,我特别注重图表类型的选择。趋势数据用折线图,对比数据用柱状图,占比数据用饼图。选对图表类型,能让数据更清晰。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

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

更多推荐