From ef130eb820a5282d329974a8d27f2cd4c46c5c4b Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 30 Mar 2020 10:12:40 +0900 Subject: [PATCH] stats: top countries and tags --- lib/widgets/album/filter_bar.dart | 4 +- lib/widgets/common/aves_filter_chip.dart | 2 + lib/widgets/stats.dart | 101 ++++++++++++++++++++++- 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/lib/widgets/album/filter_bar.dart b/lib/widgets/album/filter_bar.dart index 8f1a1c3c0..e3531bd58 100644 --- a/lib/widgets/album/filter_bar.dart +++ b/lib/widgets/album/filter_bar.dart @@ -4,10 +4,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class FilterBar extends StatelessWidget implements PreferredSizeWidget { - static final double preferredHeight = kMinInteractiveDimension; + static const double preferredHeight = kMinInteractiveDimension; @override - final Size preferredSize = Size.fromHeight(preferredHeight); + final Size preferredSize = const Size.fromHeight(preferredHeight); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/aves_filter_chip.dart b/lib/widgets/common/aves_filter_chip.dart index 5fe875608..3a6286275 100644 --- a/lib/widgets/common/aves_filter_chip.dart +++ b/lib/widgets/common/aves_filter_chip.dart @@ -4,6 +4,8 @@ import 'package:outline_material_icons/outline_material_icons.dart'; typedef FilterCallback = void Function(CollectionFilter filter); +typedef FilterBuilder = CollectionFilter Function(String label); + class AvesFilterChip extends StatefulWidget { final CollectionFilter filter; final bool removable; diff --git a/lib/widgets/stats.dart b/lib/widgets/stats.dart index 900d7c446..40cfa15a8 100644 --- a/lib/widgets/stats.dart +++ b/lib/widgets/stats.dart @@ -1,6 +1,12 @@ import 'package:aves/model/collection_lens.dart'; +import 'package:aves/model/filters/country.dart'; +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/utils/color_utils.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/album/collection_page.dart'; +import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; import 'package:charts_flutter/flutter.dart' as charts; import 'package:collection/collection.dart'; @@ -11,8 +17,19 @@ import 'package:percent_indicator/linear_percent_indicator.dart'; class StatsPage extends StatelessWidget { final CollectionLens collection; + final Map entryCountPerCountry = Map(), entryCountPerTag = Map(); - const StatsPage({this.collection}); + StatsPage({this.collection}) { + entries.forEach((entry) { + final country = entry.addressDetails?.countryName; + if (country != null) { + entryCountPerCountry[country] = (entryCountPerCountry[country] ?? 0) + 1; + } + entry.xmpSubjects.forEach((tag) { + entryCountPerTag[tag] = (entryCountPerTag[tag] ?? 0) + 1; + }); + }); + } List get entries => collection.sortedEntries; @@ -20,7 +37,7 @@ class StatsPage extends StatelessWidget { Widget build(BuildContext context) { final catalogued = entries.where((entry) => entry.isCatalogued); final withGps = catalogued.where((entry) => entry.hasGps); - final withGpsPercent = withGps.length / entries.length; + final withGpsPercent = withGps.length / collection.entryCount; final Map byMimeTypes = groupBy(entries, (entry) => entry.mimeType).map((k, v) => MapEntry(k, v.length)); final imagesByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('image/'))); final videoByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('video/'))); @@ -59,6 +76,8 @@ class StatsPage extends StatelessWidget { ], ), ), + ..._buildTopFilters(context, 'Top countries', entryCountPerCountry, (s) => CountryFilter(s)), + ..._buildTopFilters(context, 'Top tags', entryCountPerTag, (s) => TagFilter(s)), ], ), ), @@ -72,7 +91,10 @@ class StatsPage extends StatelessWidget { final sum = byMimeTypes.values.fold(0, (prev, v) => prev + v); final seriesData = byMimeTypes.entries.map((kv) => StringNumDatum(kv.key.replaceFirst(RegExp('.*/'), '').toUpperCase(), kv.value)).toList(); - seriesData.sort((kv1, kv2) => kv2.value.compareTo(kv1.value)); + seriesData.sort((kv1, kv2) { + final c = kv2.value.compareTo(kv1.value); + return c != 0 ? c : compareAsciiUpperCase(kv1.key, kv2.key); + }); final series = [ charts.Series( @@ -132,6 +154,79 @@ class StatsPage extends StatelessWidget { ); }); } + + List _buildTopFilters(BuildContext context, String title, Map entryCountMap, FilterBuilder filterBuilder) { + if (entryCountMap.isEmpty) return []; + + final maxCount = collection.entryCount; + final sortedEntries = entryCountMap.entries.toList() + ..sort((kv1, kv2) { + final c = kv2.value.compareTo(kv1.value); + return c != 0 ? c : compareAsciiUpperCase(kv1.key, kv2.key); + }); + return [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + title, + style: Constants.titleTextStyle, + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: AvesFilterChip.buttonBorderWidth / 2 + 6, end: 8), + child: Table( + children: sortedEntries.take(5).map((kv) { + final label = kv.key; + final count = kv.value; + final percent = count / maxCount; + return TableRow( + children: [ + Align( + alignment: AlignmentDirectional.centerStart, + child: AvesFilterChip( + filter: filterBuilder(label), + onPressed: (filter) => _goToFilteredCollection(context, filter), + ), + ), + Expanded( + child: LinearPercentIndicator( + percent: percent, + lineHeight: 16, + backgroundColor: Colors.white24, + progressColor: stringToColor(label), + animation: true, + padding: const EdgeInsets.symmetric(horizontal: 16), + center: Text(NumberFormat.percentPattern().format(percent)), + ), + ), + Text( + '${count}', + style: const TextStyle(color: Colors.white70), + textAlign: TextAlign.end, + ), + ], + ); + }).toList(), + columnWidths: const { + 0: IntrinsicColumnWidth(), + 2: IntrinsicColumnWidth(), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + ), + ), + ]; + } + + void _goToFilteredCollection(BuildContext context, CollectionFilter filter) { + if (collection == null) return; + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => CollectionPage(collection.derive(filter)), + ), + (route) => false, + ); + } } class StringNumDatum {