← Back to Documentation

Statistics & Analytics

Track your anime and manga journey with detailed statistics and beautiful visualizations

Overview

MiyoList provides comprehensive statistics about your anime and manga consumption, including watch time, reading progress, genre preferences, and more. All data is calculated from your AniList entries and cached locally for instant access.

💡 Tip: Statistics are updated automatically when you sync your lists or make changes to your entries.

Available Statistics

📺 Anime Statistics

  • Total Entries: Count by status (Watching, Completed, etc.)
  • Episodes Watched: Total episodes across all anime
  • Days Watched: Total time spent watching anime
  • Mean Score: Average rating you gave to anime
  • Genre Breakdown: Top genres with count and percentage
  • Format Distribution: TV, Movie, OVA, etc.
  • Release Year Distribution: Anime by release decade
  • Score Distribution: How you rate anime (1-10)

📚 Manga Statistics

  • Total Entries: Count by status (Reading, Completed, etc.)
  • Chapters Read: Total chapters across all manga
  • Volumes Read: Total volumes completed
  • Mean Score: Average rating you gave to manga
  • Genre Breakdown: Top genres with count and percentage
  • Format Distribution: Manga, Light Novel, One-shot, etc.
  • Release Year Distribution: Manga by release decade
  • Score Distribution: How you rate manga (1-10)

📊 Visual Charts

  • Genre Distribution: Horizontal bar chart showing top genres
  • Score Distribution: Bar chart showing rating patterns
  • Format Distribution: Pie chart or bar chart
  • Timeline: Entries by release year

🎨 Note: Charts use fl_chart package for beautiful, interactive visualizations.

Implementation Guide

1. Statistics Model

Create models to store statistics data:

// lib/features/statistics/domain/entities/anime_statistics.dart

class AnimeStatistics {
  final int totalEntries;
  final int episodesWatched;
  final double daysWatched;
  final double meanScore;
  final Map<String, int> statusDistribution;
  final List<GenreStat> genreDistribution;
  final Map<String, int> formatDistribution;
  final Map<int, int> scoreDistribution;
  final Map<String, int> yearDistribution;

  AnimeStatistics({
    required this.totalEntries,
    required this.episodesWatched,
    required this.daysWatched,
    required this.meanScore,
    required this.statusDistribution,
    required this.genreDistribution,
    required this.formatDistribution,
    required this.scoreDistribution,
    required this.yearDistribution,
  });
}

class GenreStat {
  final String genre;
  final int count;
  final double percentage;

  GenreStat({
    required this.genre,
    required this.count,
    required this.percentage,
  });
}

2. Statistics Calculator

Calculate statistics from media entries:

// lib/features/statistics/domain/usecases/calculate_anime_statistics.dart

class CalculateAnimeStatistics {
  AnimeStatistics call(List<MediaEntry> entries) {
    final totalEntries = entries.length;
    
    // Episodes watched
    final episodesWatched = entries
        .map((e) => e.progress ?? 0)
        .fold<int>(0, (sum, progress) => sum + progress);
    
    // Days watched (assuming 24 min per episode)
    final daysWatched = (episodesWatched * 24) / (60 * 24);
    
    // Mean score
    final scoredEntries = entries.where((e) => e.score != null);
    final meanScore = scoredEntries.isEmpty
        ? 0.0
        : scoredEntries
            .map((e) => e.score!)
            .fold<double>(0, (sum, score) => sum + score) /
            scoredEntries.length;
    
    // Status distribution
    final statusDistribution = <String, int>{};
    for (final entry in entries) {
      final status = entry.status.name;
      statusDistribution[status] = (statusDistribution[status] ?? 0) + 1;
    }
    
    // Genre distribution
    final genreMap = <String, int>{};
    for (final entry in entries) {
      for (final genre in entry.genres ?? []) {
        genreMap[genre] = (genreMap[genre] ?? 0) + 1;
      }
    }
    
    final genreDistribution = genreMap.entries
        .map((e) => GenreStat(
              genre: e.key,
              count: e.value,
              percentage: (e.value / totalEntries) * 100,
            ))
        .toList()
      ..sort((a, b) => b.count.compareTo(a.count));
    
    // Format distribution
    final formatDistribution = <String, int>{};
    for (final entry in entries) {
      final format = entry.format ?? 'Unknown';
      formatDistribution[format] = (formatDistribution[format] ?? 0) + 1;
    }
    
    // Score distribution
    final scoreDistribution = <int, int>{};
    for (final entry in scoredEntries) {
      final score = entry.score!.toInt();
      scoreDistribution[score] = (scoreDistribution[score] ?? 0) + 1;
    }
    
    // Year distribution
    final yearDistribution = <String, int>{};
    for (final entry in entries) {
      final year = entry.startDate?.year;
      if (year != null) {
        final decade = '${(year ~/ 10) * 10}s';
        yearDistribution[decade] = (yearDistribution[decade] ?? 0) + 1;
      }
    }
    
    return AnimeStatistics(
      totalEntries: totalEntries,
      episodesWatched: episodesWatched,
      daysWatched: daysWatched,
      meanScore: meanScore,
      statusDistribution: statusDistribution,
      genreDistribution: genreDistribution,
      formatDistribution: formatDistribution,
      scoreDistribution: scoreDistribution,
      yearDistribution: yearDistribution,
    );
  }
}

3. Statistics Page

Display statistics with charts:

// lib/features/statistics/presentation/pages/statistics_page.dart

class StatisticsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Statistics'),
      ),
      body: BlocBuilder<StatisticsBloc, StatisticsState>(
        builder: (context, state) {
          if (state is StatisticsLoading) {
            return Center(child: CircularProgressIndicator());
          }
          
          if (state is StatisticsLoaded) {
            return SingleChildScrollView(
              padding: EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // Overview Cards
                  _buildOverviewCards(state.animeStats, state.mangaStats),
                  
                  SizedBox(height: 24),
                  
                  // Anime Statistics
                  Text('Anime Statistics',
                      style: Theme.of(context).textTheme.headlineSmall),
                  SizedBox(height: 16),
                  _buildAnimeCharts(state.animeStats),
                  
                  SizedBox(height: 24),
                  
                  // Manga Statistics
                  Text('Manga Statistics',
                      style: Theme.of(context).textTheme.headlineSmall),
                  SizedBox(height: 16),
                  _buildMangaCharts(state.mangaStats),
                ],
              ),
            );
          }
          
          return Center(child: Text('Failed to load statistics'));
        },
      ),
    );
  }
  
  Widget _buildOverviewCards(
      AnimeStatistics anime, MangaStatistics manga) {
    return Row(
      children: [
        Expanded(
          child: _StatCard(
            title: 'Episodes Watched',
            value: anime.episodesWatched.toString(),
            subtitle: '${anime.daysWatched.toStringAsFixed(1)} days',
          ),
        ),
        SizedBox(width: 16),
        Expanded(
          child: _StatCard(
            title: 'Chapters Read',
            value: manga.chaptersRead.toString(),
            subtitle: '${manga.volumesRead} volumes',
          ),
        ),
      ],
    );
  }
}

4. Charts with fl_chart

Create beautiful charts using fl_chart:

// lib/features/statistics/presentation/widgets/genre_chart.dart

import 'package:fl_chart/fl_chart.dart';

class GenreChart extends StatelessWidget {
  final List<GenreStat> genres;

  const GenreChart({required this.genres});

  @override
  Widget build(BuildContext context) {
    final topGenres = genres.take(10).toList();
    
    return Container(
      height: 300,
      padding: EdgeInsets.all(16),
      child: BarChart(
        BarChartData(
          alignment: BarChartAlignment.spaceAround,
          maxY: topGenres.first.count.toDouble() * 1.2,
          barGroups: topGenres.asMap().entries.map((entry) {
            return BarChartGroupData(
              x: entry.key,
              barRods: [
                BarChartRodData(
                  toY: entry.value.count.toDouble(),
                  color: Colors.red,
                  width: 16,
                  borderRadius: BorderRadius.circular(4),
                ),
              ],
            );
          }).toList(),
          titlesData: FlTitlesData(
            bottomTitles: AxisTitles(
              sideTitles: SideTitles(
                showTitles: true,
                getTitlesWidget: (value, meta) {
                  if (value.toInt() < topGenres.length) {
                    return Padding(
                      padding: const EdgeInsets.only(top: 8),
                      child: Text(
                        topGenres[value.toInt()].genre,
                        style: TextStyle(fontSize: 10),
                      ),
                    );
                  }
                  return Text('');
                },
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Best Practices

  • Cache calculations: Statistics calculation can be expensive, cache results
  • Update on sync: Recalculate statistics after syncing with AniList
  • Handle edge cases: Check for null values, empty lists, division by zero
  • Optimize charts: Limit to top 10-15 items for better visualization
  • Consider performance: Use compute() for heavy calculations on large lists

Related Resources