← 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