在这里插入图片描述

数据可视化是现代应用中非常重要的功能,它将枯燥的数字转化为直观的图表,让用户能够快速理解自己的游戏数据。在游戏中心应用中,数据统计可视化可以展示玩家的游戏时长、游戏次数、分类统计等信息,帮助玩家了解自己的游戏习惯。本文将详细介绍数据统计可视化页面的实现,包括总体数据展示、时长趋势图表、分类统计等功能。

数据可视化的设计原则

数据可视化的核心是让数据说话。好的可视化设计应该简洁明了,让用户一眼就能看出数据的趋势和重点。避免使用过于复杂的图表类型,选择最适合数据特点的图表形式。

我们的统计页面采用卡片式布局,将不同类型的统计信息分组展示。顶部是总体数据卡片,使用渐变色背景突出显示,包含总游戏数、总时长、胜率等关键指标。中间是时长趋势图表,使用折线图展示每日游戏时长的变化。底部是分类统计,使用列表形式展示不同游戏分类的游玩次数。

颜色的使用要有意义。不同的数据系列使用不同的颜色,让用户能够快速区分。同时要注意颜色的对比度,确保在深色背景下也能清晰可见。我们使用了紫色、蓝色、绿色、橙色等鲜艳的颜色,让图表更加生动。

页面组件的定义

StatisticsPage是一个无状态组件,负责展示统计数据和图表。

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

  
  Widget build(BuildContext context) {

使用StatelessWidget让组件保持简单。统计数据的计算和管理可以通过状态管理方案来处理,页面本身只负责展示。这种设计符合单一职责原则,让代码更容易理解和维护。

const构造函数表示这个Widget是编译时常量,可以提高性能。super.key传递给父类,用于Widget的标识。虽然这些都是基础知识,但正确使用它们可以让应用运行得更加流畅。

在实际应用中,统计数据应该从数据库查询并计算得到。这里我们先使用模拟数据来展示页面效果,后续可以很容易地替换为真实数据。

页面框架的构建

页面使用Scaffold作为基本框架,body部分使用SingleChildScrollView支持滚动。

    return Scaffold(
      appBar: AppBar(
        title: const Text('数据统计'),
        backgroundColor: const Color(0xFF16213e),
      ),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(16.w),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _buildOverallStats(),
            SizedBox(height: 20.h),
            _buildSectionTitle('每日游戏时长'),
            _buildChart(),
            SizedBox(height: 20.h),
            _buildSectionTitle('游戏分类统计'),
            _buildCategoryStats(),
          ],
        ),
      ),
    );
  }

Scaffold提供了标准的Material Design页面结构。AppBar显示页面标题"数据统计",backgroundColor设置为深蓝色,与应用的整体主题保持一致。

body使用SingleChildScrollView包裹Column,让页面内容可以滚动。当统计信息很多时,用户可以向下滚动查看所有内容。padding设置为EdgeInsets.all(16.w),在页面四周添加内边距。

Column垂直排列各个统计模块。crossAxisAlignment设置为start,让内容左对齐。children数组包含了总体数据、时长图表、分类统计等模块,使用SizedBox添加间距分隔。

这种模块化的布局设计让页面结构清晰,每个模块负责展示一类统计信息。如果需要添加新的统计模块,只需要在children数组中添加新的Widget即可。

总体数据卡片的实现

总体数据卡片展示最重要的统计指标,使用渐变色背景突出显示。

  Widget _buildOverallStats() {
    return Container(
      padding: EdgeInsets.all(20.w),
      decoration: BoxDecoration(
        gradient: const LinearGradient(
          colors: [Color(0xFF6a11cb), Color(0xFF2575fc)],
        ),
        borderRadius: BorderRadius.circular(16.r),
      ),

Container是卡片的容器,padding设置为20.w,让内容有足够的呼吸空间。decoration使用BoxDecoration定义装饰样式。

gradient使用LinearGradient创建渐变色背景,从紫色渐变到蓝色。渐变色比纯色更有层次感,视觉效果更加丰富。这两个颜色都是冷色调,给人科技、专业的感觉,符合数据统计的主题。

borderRadius设置为16.r,创建圆角效果。圆角让卡片看起来更加柔和,符合现代UI设计的趋势。使用flutter_screenutil的适配单位,确保在不同设备上保持一致的视觉效果。

卡片标题的显示

卡片顶部显示"总体数据"标题,让用户知道这是什么内容。

      child: Column(
        children: [
          Text('总体数据', style: TextStyle(fontSize: 20.sp, fontWeight: FontWeight.bold, color: Colors.white)),
          SizedBox(height: 20.h),

Column垂直排列标题和统计项。标题使用Text显示,fontSize设置为20.sp,fontWeight设置为bold,让标题醒目突出。color设置为白色,在渐变色背景上有很好的对比度。

SizedBox添加了20.h的垂直间距,将标题和统计项分开。适当的间距让布局更加清晰,不会显得拥挤。这个间距比较大,因为标题和统计项是两个不同的内容区域。

标题的设计要简洁明了,让用户一眼就能理解这个卡片的内容。"总体数据"这个标题准确地描述了卡片的功能,不需要额外的说明。

统计项的布局

统计项使用Row水平排列,展示总游戏数、总时长、胜率三个指标。

          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              _buildStatItem('总游戏数', '156'),
              _buildStatItem('总时长', '24h'),
              _buildStatItem('胜率', '68%'),
            ],
          ),
        ],
      ),
    );
  }

Row的mainAxisAlignment设置为spaceAround,让三个统计项均匀分布。每个统计项之间有相等的间距,视觉上非常平衡。

三个统计项分别展示不同的指标。总游戏数显示玩家玩过的游戏总次数,总时长显示累计游戏时间,胜率显示获胜的百分比。这三个指标从不同角度反映了玩家的游戏情况。

使用_buildStatItem方法构建每个统计项,传入标签和数值。这种方法复用的方式让代码更加简洁,避免重复代码。如果需要修改统计项的样式,只需要修改一个方法即可。

统计项的实现

_buildStatItem方法创建一个统计项,包含数值和标签。

  Widget _buildStatItem(String label, String value) {
    return Column(
      children: [
        Text(value, style: TextStyle(fontSize: 24.sp, fontWeight: FontWeight.bold, color: Colors.white)),
        SizedBox(height: 4.h),
        Text(label, style: TextStyle(fontSize: 12.sp, color: Colors.white70)),
      ],
    );
  }

Column垂直排列数值和标签。数值在上,标签在下,这是统计项的标准布局方式。数值是最重要的信息,应该最先被用户注意到。

数值使用24.sp的大字号和粗体,非常醒目。颜色使用白色,在渐变色背景上清晰可见。这个字号比标题小一些,但仍然足够大,让用户可以快速扫描数值。

SizedBox添加了4.h的垂直间距,将数值和标签分开。这个间距比较小,因为数值和标签是紧密相关的信息,不需要太大的分隔。

标签使用12.sp的小字号,表明这是次要信息。颜色使用半透明的白色,让它看起来更柔和。标签的作用是说明数值的含义,所以应该清晰可读,但不需要像数值那样突出。

章节标题的实现

在图表和分类统计之前,显示章节标题,让页面结构更加清晰。

  Widget _buildSectionTitle(String title) {
    return Padding(
      padding: EdgeInsets.only(bottom: 12.h),
      child: Text(title, style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold)),
    );
  }

_buildSectionTitle方法创建一个章节标题。使用Padding添加底部内边距12.h,让标题和下面的内容有一定的间距。

Text显示标题文字,fontSize设置为18.sp,fontWeight设置为bold。这个字号比总体数据的标题小一些,表明这是次级标题。粗体让标题醒目,用户可以快速定位到不同的统计模块。

颜色使用默认的白色,与页面的整体风格保持一致。章节标题的设计要简洁,不需要过多的装饰,让用户专注于内容本身。

这种章节标题的设计让页面结构更加清晰。用户可以通过标题快速了解每个模块的内容,不需要仔细阅读就能找到自己感兴趣的信息。

图表容器的构建

时长趋势图表使用fl_chart库绘制,首先创建图表的容器。

  Widget _buildChart() {
    return Container(
      height: 200.h,
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(
        color: const Color(0xFF16213e),
        borderRadius: BorderRadius.circular(12.r),
      ),

Container是图表的容器,height设置为200.h,这是一个合适的高度,既能清晰地展示数据,又不会占据太多空间。padding设置为16.w,让图表不会紧贴容器边缘。

decoration使用BoxDecoration定义装饰样式。color设置为深蓝色,与AppBar的颜色一致,形成了统一的视觉风格。borderRadius设置为12.r,创建圆角效果。

这个容器为图表提供了一个清晰的边界,让图表在视觉上独立于其他内容。深蓝色的背景与页面的深色主题协调,让图表看起来是页面的有机组成部分。

折线图的实现

使用fl_chart的LineChart组件绘制时长趋势折线图。

      child: LineChart(
        LineChartData(
          gridData: const FlGridData(show: false),
          titlesData: const FlTitlesData(show: false),
          borderData: FlBorderData(show: false),

LineChart是fl_chart库提供的折线图组件。LineChartData定义了图表的各种配置。

gridData控制网格线的显示,设置为show: false隐藏网格线。网格线在某些情况下有助于读取数据,但在这个简洁的设计中,我们选择隐藏它,让图表更加干净。

titlesData控制坐标轴标题的显示,也设置为show: false。这个图表主要用于展示趋势,不需要精确的坐标值,所以隐藏标题让图表更加简洁。

borderData控制图表边框的显示,同样设置为show: false。没有边框的图表看起来更加现代,与容器的圆角边框配合,形成了统一的视觉效果。

这些配置让图表保持简洁,用户可以专注于数据的趋势,而不是被各种辅助线和标签分散注意力。

折线数据的定义

lineBarsData定义了折线的数据点和样式。

          lineBarsData: [
            LineChartBarData(
              spots: [
                const FlSpot(0, 1),
                const FlSpot(1, 3),
                const FlSpot(2, 2),
                const FlSpot(3, 4),
                const FlSpot(4, 3),
                const FlSpot(5, 5),
                const FlSpot(6, 4),
              ],

LineChartBarData定义了一条折线。spots是数据点的列表,每个FlSpot包含x和y坐标。这里我们定义了7个数据点,代表一周的游戏时长数据。

数据点的x坐标从0到6,代表周一到周日。y坐标代表游戏时长,单位可以是小时。这些数据点连接起来,形成了一条折线,展示了游戏时长的变化趋势。

在实际应用中,这些数据应该从数据库查询得到。可以统计每天的游戏时长,然后转换为FlSpot列表。数据的时间范围可以是最近7天、最近30天等,让用户选择查看不同时间段的数据。

折线样式的配置

配置折线的颜色、宽度、曲线效果等样式。

              isCurved: true,
              color: Colors.purpleAccent,
              barWidth: 3,
              dotData: const FlDotData(show: false),
            ),
          ],
        ),
      ),
    );
  }

isCurved设置为true,让折线变成平滑的曲线。曲线比折线更加美观,视觉效果更加柔和。这种平滑的曲线让数据的趋势更加明显,用户可以更容易地看出数据的变化规律。

color设置为Colors.purpleAccent,这是一个鲜艳的紫色。紫色与页面的整体配色协调,同时在深蓝色背景上有很好的对比度,让折线清晰可见。

barWidth设置为3,这是折线的宽度。3个像素的宽度既能清晰地展示折线,又不会显得太粗。如果折线太细,在小屏幕上可能看不清;如果太粗,会显得笨重。

dotData控制数据点的显示,设置为show: false隐藏数据点。隐藏数据点让折线更加简洁,用户可以专注于整体趋势。如果需要精确读取某个数据点的值,可以显示数据点并添加点击交互。

分类统计的数据定义

分类统计展示不同游戏分类的游玩次数,首先定义分类数据。

  Widget _buildCategoryStats() {
    final categories = [
      {'name': '益智', 'count': 45, 'color': Colors.blue},
      {'name': '记忆', 'count': 32, 'color': Colors.green},
      {'name': '反应', 'count': 28, 'color': Colors.orange},
      {'name': '知识', 'count': 25, 'color': Colors.purple},
    ];

categories是一个列表,每个元素是一个Map,包含分类名称、游玩次数和颜色。这四个分类代表了游戏中心的主要游戏类型。

每个分类使用不同的颜色,让用户能够快速区分。蓝色代表益智类,绿色代表记忆类,橙色代表反应类,紫色代表知识类。这些颜色都是鲜艳的颜色,在深色背景上清晰可见。

游玩次数反映了玩家对不同类型游戏的偏好。益智类游戏玩得最多,说明玩家喜欢这类游戏。这种数据可以帮助玩家了解自己的游戏习惯,也可以用于个性化推荐。

在实际应用中,这些数据应该从数据库统计得到。可以查询每个分类的游戏记录数量,然后按数量降序排列,展示玩得最多的分类。

分类列表的构建

使用Column垂直排列所有分类统计项。

    return Column(
      children: categories.map((cat) {
        return Container(
          margin: EdgeInsets.only(bottom: 12.h),
          padding: EdgeInsets.all(16.w),
          decoration: BoxDecoration(
            color: const Color(0xFF16213e),
            borderRadius: BorderRadius.circular(12.r),
          ),

Column的children使用map方法将categories列表转换为Widget列表。每个分类创建一个Container作为卡片。

Container的margin设置了底部间距12.h,让相邻的卡片之间有一定的间隔。padding设置了内边距16.w,让卡片内的内容不会紧贴边缘。

decoration定义了卡片的装饰样式。color设置为深蓝色,与其他卡片的颜色一致。borderRadius创建圆角效果,让卡片看起来更加柔和。

这种卡片式的布局让每个分类统计独立显示,视觉上清晰明了。用户可以快速扫描所有分类,了解自己在不同类型游戏上的投入。

分类卡片的内容布局

卡片内容使用Row水平排列,从左到右依次是颜色条、分类信息和游玩次数。

          child: Row(
            children: [
              Container(
                width: 12.w,
                height: 40.h,
                decoration: BoxDecoration(
                  color: cat['color'] as Color,
                  borderRadius: BorderRadius.circular(6.r),
                ),
              ),
              SizedBox(width: 16.w),

Row水平排列子Widget。第一个子元素是颜色条,使用Container创建。width设置为12.w,height设置为40.h,这是一个竖条的形状。

decoration的color使用分类数据中的颜色,每个分类有不同的颜色。borderRadius设置为6.r,让颜色条的边缘圆润。

这个颜色条是分类的视觉标识,让用户可以快速识别不同的分类。颜色比文字更容易被注意到,用户扫描列表时,首先看到的是颜色条,然后才是文字信息。

SizedBox添加了16.w的水平间距,将颜色条和文字信息分开。适当的间距让布局更加清晰,不会显得拥挤。

分类信息的展示

分类信息包括名称和游玩次数描述,使用Column垂直排列。Expanded让这部分内容占据剩余的水平空间。

              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(cat['name'] as String, style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
                    SizedBox(height: 4.h),
                    Text('${cat['count']} 次游戏', style: TextStyle(fontSize: 12.sp, color: Colors.white60)),
                  ],
                ),
              ),

Expanded让Column占据Row中剩余的水平空间。crossAxisAlignment设置为start,让文本左对齐。

分类名称使用16.sp的字号和粗体,让它醒目突出。这是最重要的信息,应该最先被用户注意到。较大的字号和粗体让名称非常清晰。

SizedBox添加了4.h的垂直间距,将名称和描述分开。这个间距比较小,因为这两个信息是紧密相关的。

游玩次数描述使用字符串插值将数字嵌入到文本中。fontSize设置为12.sp,比名称小一些,表明这是次要信息。color设置为半透明的白色,让它看起来更柔和。

这个描述告诉用户这个分类玩了多少次游戏,让数字更加易读。比起单纯的数字,"45 次游戏"这样的描述更加友好,用户不需要思考数字的含义。

游玩次数的大数字显示

卡片右侧显示游玩次数的大数字,作为视觉焦点。

              Text('${cat['count']}', style: TextStyle(fontSize: 24.sp, fontWeight: FontWeight.bold)),
            ],
          ),
        );
      }).toList(),
    );
  }
}

Text显示游玩次数,fontSize设置为24.sp,这是一个很大的字号。fontWeight设置为bold,让数字非常醒目。这个大数字是卡片的视觉焦点,用户可以快速扫描所有分类的游玩次数。

使用字符串插值将数字转换为字符串。虽然左侧已经有了游玩次数的描述,但右侧的大数字提供了更直观的视觉对比。用户可以通过数字的大小快速比较不同分类的游玩次数。

map方法返回一个Iterable,需要调用toList()转换为List。这是Dart的标准做法,Column的children参数需要一个List。

整个分类统计的设计使用了颜色、文字、数字三重视觉元素,让信息的传达非常清晰。用户可以快速了解自己在不同类型游戏上的投入,发现自己的游戏偏好。

数据的实时更新

在实际应用中,统计数据应该实时更新,反映玩家最新的游戏情况。

可以使用状态管理方案(如GetX)来管理统计数据:

class StatisticsController extends GetxController {
  final RxInt totalGames = 0.obs;
  final RxInt totalHours = 0.obs;
  final RxDouble winRate = 0.0.obs;
  final RxList<FlSpot> chartData = <FlSpot>[].obs;
  final RxList<Map<String, dynamic>> categoryStats = <Map<String, dynamic>>[].obs;

  
  void onInit() {
    super.onInit();
    loadStatistics();
  }

  Future<void> loadStatistics() async {
    // 从数据库查询统计数据
    final stats = await DatabaseHelper.getStatistics();
    totalGames.value = stats['totalGames'];
    totalHours.value = stats['totalHours'];
    winRate.value = stats['winRate'];
    chartData.value = stats['chartData'];
    categoryStats.value = stats['categoryStats'];
  }
}

这个控制器管理所有的统计数据,使用响应式变量让数据变化时自动更新UI。onInit方法在控制器初始化时调用loadStatistics加载数据。

loadStatistics方法从数据库查询统计数据,然后更新响应式变量。当这些变量改变时,所有使用它们的Widget都会自动重新构建,显示最新的数据。

这种响应式的数据管理让统计页面始终显示最新的数据。当玩家完成一局游戏后,统计数据会自动更新,不需要手动刷新页面。

统计数据的计算

统计数据需要从游戏记录中计算得到,可以定义一个统计服务类:

class StatisticsService {
  static Future<Map<String, dynamic>> calculateStatistics() async {
    final db = await DatabaseHelper.database;
    
    // 查询总游戏数
    final totalGames = Sqflite.firstIntValue(
      await db.rawQuery('SELECT COUNT(*) FROM game_history')
    ) ?? 0;
    
    // 查询总时长
    final totalSeconds = Sqflite.firstIntValue(
      await db.rawQuery('SELECT SUM(duration) FROM game_history')
    ) ?? 0;
    final totalHours = (totalSeconds / 3600).round();
    
    // 计算胜率
    final wins = Sqflite.firstIntValue(
      await db.rawQuery('SELECT COUNT(*) FROM game_history WHERE result = "win"')
    ) ?? 0;
    final winRate = totalGames > 0 ? (wins / totalGames * 100).round() : 0;
    
    // 查询每日时长数据
    final dailyData = await db.rawQuery('''
      SELECT date, SUM(duration) as total
      FROM game_history
      WHERE date >= date('now', '-7 days')
      GROUP BY date
      ORDER BY date
    ''');
    
    // 查询分类统计
    final categoryData = await db.rawQuery('''
      SELECT category, COUNT(*) as count
      FROM game_history
      GROUP BY category
      ORDER BY count DESC
    ''');
    
    return {
      'totalGames': totalGames,
      'totalHours': totalHours,
      'winRate': winRate,
      'chartData': _convertToChartData(dailyData),
      'categoryStats': categoryData,
    };
  }
}

这个方法使用SQL查询计算各种统计数据。总游戏数使用COUNT函数,总时长使用SUM函数,胜率通过计算获胜次数占总次数的百分比得到。

每日时长数据使用GROUP BY按日期分组,然后对每天的时长求和。WHERE子句限制查询最近7天的数据,让图表显示最近一周的趋势。

分类统计也使用GROUP BY按分类分组,然后计数每个分类的游戏次数。ORDER BY按次数降序排列,让玩得最多的分类排在前面。

这些SQL查询高效地从数据库中提取统计数据,避免了在应用层进行复杂的计算。数据库的聚合函数和分组功能非常适合统计分析。

图表数据的转换

将数据库查询结果转换为图表需要的数据格式:

static List<FlSpot> _convertToChartData(List<Map<String, dynamic>> dailyData) {
  final spots = <FlSpot>[];
  for (int i = 0; i < dailyData.length; i++) {
    final hours = (dailyData[i]['total'] as int) / 3600;
    spots.add(FlSpot(i.toDouble(), hours));
  }
  return spots;
}

这个方法遍历每日数据,将时长从秒转换为小时,然后创建FlSpot对象。x坐标是索引,y坐标是时长。

如果某天没有游戏记录,数据库查询结果中不会包含这一天。可以在转换时填充缺失的数据,让图表显示完整的7天数据,缺失的天数显示为0。

这种数据转换让数据库查询结果可以直接用于图表绘制,实现了数据层和展示层的分离。如果需要更换图表库,只需要修改转换方法即可。

下拉刷新功能

统计页面可以添加下拉刷新功能,让用户可以手动刷新统计数据:

RefreshIndicator(
  onRefresh: () async {
    final controller = Get.find<StatisticsController>();
    await controller.loadStatistics();
  },
  child: SingleChildScrollView(
    // 页面内容...
  ),
)

RefreshIndicator包裹SingleChildScrollView,提供下拉刷新功能。用户下拉页面时,会显示一个加载指示器,同时调用onRefresh回调。

onRefresh回调中,我们获取StatisticsController实例,然后调用loadStatistics方法重新加载统计数据。这个方法返回Future,RefreshIndicator会等待Future完成后才隐藏加载指示器。

下拉刷新让用户可以主动更新统计数据,确保看到的是最新的信息。虽然统计数据通常会自动更新,但提供手动刷新功能可以让用户更有控制感。

总结

本文详细介绍了数据统计可视化页面的实现。我们从设计原则开始,确定了卡片式布局和简洁的图表设计。然后实现了StatisticsPage页面,包括总体数据展示、时长趋势图表、分类统计等核心功能。

我们使用了fl_chart库绘制折线图,展示游戏时长的变化趋势。图表的配置保持简洁,隐藏了网格线、坐标轴等辅助元素,让用户专注于数据本身。平滑的曲线和鲜艳的颜色让图表既美观又易读。

我们还讨论了数据的实时更新、统计计算、数据转换、下拉刷新等扩展功能。这些功能让统计系统更加完善,为用户提供准确、及时的游戏数据分析。

数据可视化是帮助用户理解数据的重要工具,好的可视化设计可以让复杂的数据变得简单易懂。通过本文的学习,你掌握了数据统计可视化的实现方法,这些知识可以应用到各种需要数据展示的应用中。

在下一篇文章中,我们将实现排行榜主页功能,展示全球排行、好友排行等多种排行榜。排行榜会涉及到数据排序、分页加载、用户排名等内容,敬请期待。


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

Logo

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

更多推荐