From da6d64972b60519a66464365aaa89b286d6a06a4 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 1 Nov 2024 15:19:52 -0400 Subject: [PATCH] add categories to analytics display --- .../analytics/construct_list_model.dart | 31 +++- .../models/analytics/constructs_model.dart | 4 + .../analytics_summary/analytics_popup.dart | 147 +++++++++++++----- 3 files changed, 138 insertions(+), 44 deletions(-) diff --git a/lib/pangea/models/analytics/construct_list_model.dart b/lib/pangea/models/analytics/construct_list_model.dart index 23763824c..747a54bb4 100644 --- a/lib/pangea/models/analytics/construct_list_model.dart +++ b/lib/pangea/models/analytics/construct_list_model.dart @@ -23,8 +23,8 @@ class ConstructListModel { List get uses => _uses.where((use) => use.constructType == type || type == null).toList(); - /// All unique lemmas used in the construct events - List get lemmas => constructList.map((e) => e.lemma).toSet().toList(); + // /// All unique lemmas used in the construct events + // List get lemmas => constructList.map((e) => e.lemma).toSet().toList(); /// All unique lemmas used in the construct events with non-zero points List get lemmasWithPoints => @@ -36,8 +36,13 @@ class ConstructListModel { final Map> lemmaToUses = {}; for (final use in uses) { if (use.lemma == null) continue; - lemmaToUses[use.lemma! + use.constructType.string] ??= []; - lemmaToUses[use.lemma! + use.constructType.string]!.add(use); + lemmaToUses[use.lemma! + + use.constructType.string + + (use.category ?? "Other")] ??= []; + lemmaToUses[use.lemma! + + use.constructType.string + + (use.category ?? "Other")]! + .add(use); } _constructMap = lemmaToUses.map( @@ -47,6 +52,7 @@ class ConstructListModel { uses: value, constructType: value.first.constructType, lemma: value.first.lemma!, + category: value.first.category, ), ), ); @@ -68,7 +74,7 @@ class ConstructListModel { _constructList = _constructMap!.values.toList(); _constructList!.sort((a, b) { - final comp = b.uses.length.compareTo(a.uses.length); + final comp = b.points.compareTo(a.points); if (comp != 0) return comp; return a.lemma.compareTo(b.lemma); }); @@ -79,6 +85,15 @@ class ConstructListModel { List get constructListWithPoints => constructList.where((constructUse) => constructUse.points > 0).toList(); + Map> get categoriesToUses { + final Map> categoriesMap = {}; + for (final use in constructListWithPoints) { + categoriesMap[use.category] ??= []; + categoriesMap[use.category]!.add(use); + } + return categoriesMap; + } + get maxXPPerLemma { return type != null ? type!.maxXPPerLemma @@ -141,12 +156,14 @@ class ConstructUses { final List uses; final ConstructTypeEnum constructType; final String lemma; + final String? _category; ConstructUses({ required this.uses, required this.constructType, required this.lemma, - }); + required category, + }) : _category = category; // Total points for all uses of this lemma int get points { @@ -165,6 +182,8 @@ class ConstructUses { }); return _lastUsed = lastUse; } + + String get category => _category ?? "Other"; } /// One lemma, a use type, and a list of uses diff --git a/lib/pangea/models/analytics/constructs_model.dart b/lib/pangea/models/analytics/constructs_model.dart index 95a1ae101..85bd16d40 100644 --- a/lib/pangea/models/analytics/constructs_model.dart +++ b/lib/pangea/models/analytics/constructs_model.dart @@ -74,7 +74,11 @@ class ConstructAnalyticsModel { class OneConstructUse { String? lemma; String? form; + + /// For vocab constructs, this is the POS. For morph + /// constructs, this is the morphological category. String? category; + ConstructTypeEnum constructType; ConstructUseTypeEnum useType; diff --git a/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart b/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart index d7978af73..ff29fc118 100644 --- a/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart +++ b/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart @@ -16,8 +16,76 @@ class AnalyticsPopup extends StatelessWidget { super.key, }); + List>> get categoriesToUses { + final entries = constructsModel.categoriesToUses.entries.toList(); + // Sort the list with custom logic + entries.sort((a, b) { + // Check if one of the keys is 'Other' + if (a.key == 'Other') return 1; + if (b.key == 'Other') return -1; + + // Sort by the length of the list in descending order + final aTotalPoints = a.value.fold( + 0, + (previousValue, element) => previousValue + element.points, + ); + final bTotalPoints = b.value.fold( + 0, + (previousValue, element) => previousValue + element.points, + ); + return bTotalPoints.compareTo(aTotalPoints); + }); + return entries; + } + @override Widget build(BuildContext context) { + Widget? dialogContent; + final bool hasNoData = constructsModel.constructListWithPoints.isEmpty; + final bool hasNoCategories = constructsModel.categoriesToUses.length == 1 && + constructsModel.categoriesToUses.keys.first == "Other"; + + if (hasNoData) { + dialogContent = Center(child: Text(L10n.of(context)!.noDataFound)); + } else if (hasNoCategories) { + dialogContent = ListView.builder( + itemCount: constructsModel.constructListWithPoints.length, + itemBuilder: (context, index) { + return ConstructUsesXPTile( + indicator: indicator, + constructsModel: constructsModel, + constructUses: constructsModel.constructListWithPoints[index], + ); + }, + ); + } else { + dialogContent = ListView.builder( + itemCount: categoriesToUses.length, + itemBuilder: (context, index) { + final category = categoriesToUses[index]; + return Column( + children: [ + ExpansionTile( + title: Text( + category.key != 'Other' + ? getGrammarCopy(category.key, context) + : category.key, + ), + children: category.value.map((constructUses) { + return ConstructUsesXPTile( + indicator: indicator, + constructsModel: constructsModel, + constructUses: constructUses, + ); + }).toList(), + ), + const Divider(height: 1), + ], + ); + }, + ); + } + return Dialog( child: ConstrainedBox( constraints: const BoxConstraints( @@ -36,44 +104,7 @@ class AnalyticsPopup extends StatelessWidget { ), body: Padding( padding: const EdgeInsets.symmetric(vertical: 20), - child: constructsModel.constructListWithPoints.isEmpty - ? Center( - child: Text(L10n.of(context)!.noDataFound), - ) - : ListView.builder( - itemCount: constructsModel.constructListWithPoints.length, - itemBuilder: (context, index) { - return Tooltip( - message: - "${constructsModel.constructListWithPoints[index].points} / ${constructsModel.maxXPPerLemma}", - child: ListTile( - onTap: () {}, - title: Text( - constructsModel.type == ConstructTypeEnum.morph - ? getGrammarCopy( - constructsModel - .constructListWithPoints[index].lemma, - context, - ) - : constructsModel - .constructListWithPoints[index].lemma, - ), - subtitle: LinearProgressIndicator( - value: constructsModel - .constructListWithPoints[index].points / - constructsModel.maxXPPerLemma, - minHeight: 20, - borderRadius: const BorderRadius.all( - Radius.circular(AppConfig.borderRadius), - ), - color: indicator.color(context), - ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 20), - ), - ); - }, - ), + child: dialogContent, ), ), ), @@ -81,3 +112,43 @@ class AnalyticsPopup extends StatelessWidget { ); } } + +class ConstructUsesXPTile extends StatelessWidget { + final ProgressIndicatorEnum indicator; + final ConstructListModel constructsModel; + final ConstructUses constructUses; + + const ConstructUsesXPTile({ + required this.indicator, + required this.constructsModel, + required this.constructUses, + super.key, + }); + + @override + Widget build(BuildContext context) { + final lemma = constructUses.lemma; + return Tooltip( + message: "${constructUses.points} / ${constructsModel.maxXPPerLemma}", + child: ListTile( + onTap: () {}, + title: Text( + constructsModel.type == ConstructTypeEnum.morph + ? getGrammarCopy(lemma, context) + : lemma, + ), + subtitle: LinearProgressIndicator( + value: constructUses.points / constructsModel.maxXPPerLemma, + minHeight: 20, + borderRadius: const BorderRadius.all( + Radius.circular(AppConfig.borderRadius), + ), + color: indicator.color(context), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + ), + ), + ); + } +}