Skip to content

Commit

Permalink
feat: add recent performance chart to flashcard set info screen
Browse files Browse the repository at this point in the history
  • Loading branch information
Moseco committed Sep 29, 2024
1 parent e8b1a49 commit d1a6ab7
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 1 deletion.
156 changes: 156 additions & 0 deletions lib/ui/views/flashcard_set_info/flashcard_set_info_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ class FlashcardSetInfoView extends StackedView<FlashcardSetInfoViewModel> {
flashcardCount: viewModel.flashcardCount,
),
),
if (viewModel.maxDueFlashcardsCompleted != 0)
_HistoricalPerformance(
viewModel.flashcardSetReports,
viewModel.maxDueFlashcardsCompleted,
),
if (viewModel.challengingFlashcards.isNotEmpty)
const _Challenging(),
],
Expand Down Expand Up @@ -284,6 +289,157 @@ class _Indicator extends StatelessWidget {
}
}

class _HistoricalPerformance extends StatelessWidget {
final int maxFlashcardsCompleted;
final List<FlashcardSetReport?> flashcardSetReports;

const _HistoricalPerformance(
this.flashcardSetReports,
this.maxFlashcardsCompleted,
);

@override
Widget build(BuildContext context) {
const double barsWidth = 8;

return CardWithTitleSection(
title: 'Recent performance',
child: Padding(
padding: const EdgeInsets.only(
left: 16,
top: 24,
right: 16,
bottom: 8,
),
child: Column(
children: [
SizedBox(
height: 200,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceEvenly,
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 32,
getTitlesWidget: (number, __) => Text(
DateFormat.Md().format(DateTime.now()
.subtract(Duration(days: 6 - number.toInt()))),
style: const TextStyle(color: Colors.grey),
),
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 24,
interval: (maxFlashcardsCompleted / 2).ceilToDouble(),
getTitlesWidget: (number, __) => Text(
'${number.toInt()}',
style: const TextStyle(color: Colors.grey),
),
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
gridData: const FlGridData(show: false),
borderData: FlBorderData(show: false),
barGroups: List.generate(
flashcardSetReports.length,
(index) {
final current = flashcardSetReports[index];
if (current == null) {
return BarChartGroupData(
x: index,
barRods: [
BarChartRodData(
toY: 0,
rodStackItems: [
BarChartRodStackItem(0, 0, Colors.transparent),
],
width: barsWidth,
),
BarChartRodData(
toY: 0,
rodStackItems: [
BarChartRodStackItem(0, 0, Colors.transparent),
],
width: barsWidth,
),
],
);
} else {
return BarChartGroupData(
x: index,
barRods: [
BarChartRodData(
toY: current.dueFlashcardsCompleted.toDouble(),
rodStackItems: [
BarChartRodStackItem(
0,
current.dueFlashcardsCompleted.toDouble(),
Colors.green,
),
],
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
width: barsWidth,
),
BarChartRodData(
toY: current.dueFlashcardsGotWrong.toDouble(),
rodStackItems: [
BarChartRodStackItem(
0,
current.dueFlashcardsGotWrong.toDouble(),
Colors.red,
),
],
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
width: barsWidth,
),
],
);
}
},
),
),
),
),
const Wrap(
spacing: 8,
runSpacing: 4,
alignment: WrapAlignment.center,
children: [
_Indicator(
color: Colors.green,
text: 'Due flashcards completed',
),
_Indicator(
color: Colors.red,
text: 'Due flashcards got wrong',
),
],
),
],
),
),
);
}
}

class _Challenging extends ViewModelWidget<FlashcardSetInfoViewModel> {
const _Challenging();

Expand Down
32 changes: 32 additions & 0 deletions lib/ui/views/flashcard_set_info/flashcard_set_info_viewmodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ class FlashcardSetInfoViewModel extends FutureViewModel {
List<double> flashcardIntervalCounts = [0, 0, 0, 0, 0];
// Top challenging flashcards
List<DictionaryItem> challengingFlashcards = [];
// Historical performance
int _maxDueFlashcardsCompleted = 0;
int get maxDueFlashcardsCompleted => _maxDueFlashcardsCompleted;
late List<FlashcardSetReport?> flashcardSetReports;

bool _showIntervalAsPercent = false;
bool get showIntervalAsPercent => _showIntervalAsPercent;
Expand Down Expand Up @@ -103,6 +107,34 @@ class FlashcardSetInfoViewModel extends FutureViewModel {
flashcardIntervalCounts[0]++;
}
}

await _getFlashcardSetReports(today);
}

Future<void> _getFlashcardSetReports(DateTime endDateTime) async {
// Get one week flashcard set reports and space out nulls in list
final startDateTime = endDateTime.subtract(const Duration(days: 6));

var flashcardSetReportMap = {
for (var v in await _dictionaryService.getFlashcardSetReportRange(
flashcardSet,
startDateTime.toInt(),
endDateTime.toInt(),
))
v.date: v
};

flashcardSetReports = [];
for (int i = 0; i < 7; i++) {
final dateTime = startDateTime.add(Duration(days: i));
flashcardSetReports.add(flashcardSetReportMap[dateTime.toInt()]);
if (flashcardSetReports.last != null &&
flashcardSetReports.last!.dueFlashcardsCompleted >
_maxDueFlashcardsCompleted) {
_maxDueFlashcardsCompleted =
flashcardSetReports.last!.dueFlashcardsCompleted;
}
}
}

void navigateToVocab(Vocab vocab) {
Expand Down
3 changes: 3 additions & 0 deletions test/helpers/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ MockDictionaryService getAndRegisterDictionaryService({
FlashcardSetReport? createFlashcardSetReport,
FlashcardSetReport? getFlashcardSetReport,
FlashcardSetReport? getRecentFlashcardSetReport,
List<FlashcardSetReport>? getFlashcardSetReportRange,
}) {
_removeRegistrationIfExists<DictionaryService>();
final service = MockDictionaryService();
Expand Down Expand Up @@ -138,6 +139,8 @@ MockDictionaryService getAndRegisterDictionaryService({
.thenAnswer((_) async => getFlashcardSetReport);
when(service.getRecentFlashcardSetReport(any))
.thenAnswer((_) async => getRecentFlashcardSetReport);
when(service.getFlashcardSetReportRange(any, any, any))
.thenAnswer((_) async => getFlashcardSetReportRange!);

locator.registerSingleton<DictionaryService>(service);
return service;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ void main() {
},
),
).expand((element) => element).toList(),
getFlashcardSetReportRange: [],
);

await tester.pumpWidget(
Expand Down Expand Up @@ -102,6 +103,7 @@ void main() {
totalWrongAnswers: 8,
),
],
getFlashcardSetReportRange: [],
);

await tester.pumpWidget(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ void main() {
),
getVocab10(),
],
getFlashcardSetReportRange: [],
);

final navigationService = getAndRegisterNavigationService();
Expand Down Expand Up @@ -101,6 +102,7 @@ void main() {
),
getVocab6(),
],
getFlashcardSetReportRange: [],
);

final navigationService = getAndRegisterNavigationService();
Expand All @@ -120,7 +122,65 @@ void main() {
expect(viewModel.flashcardIntervalCounts[4], 1);
});

test('Flashcard challenging flashcards', () async {
test('Historical performance', () async {
getAndRegisterDictionaryService(
getFlashcardSetFlashcards: [getVocab1()],
getFlashcardSetReportRange: [
FlashcardSetReport(
id: 0,
flashcardSetId: 1,
date: DateTime.now().subtract(const Duration(days: 5)).toInt(),
dueFlashcardsCompleted: 1,
dueFlashcardsGotWrong: 2,
newFlashcardsCompleted: 3,
),
FlashcardSetReport(
id: 1,
flashcardSetId: 1,
date: DateTime.now().subtract(const Duration(days: 3)).toInt(),
dueFlashcardsCompleted: 3,
dueFlashcardsGotWrong: 2,
newFlashcardsCompleted: 1,
),
FlashcardSetReport(
id: 2,
flashcardSetId: 1,
date: DateTime.now().toInt(),
dueFlashcardsCompleted: 20,
dueFlashcardsGotWrong: 0,
newFlashcardsCompleted: 0,
),
],
);

final navigationService = getAndRegisterNavigationService();

// Initialize viewmodel
var viewModel = FlashcardSetInfoViewModel(createDefaultFlashcardSet());
await viewModel.futureToRun();

// Verify that back was not called
verifyNever(navigationService.back());

// Check contents
expect(viewModel.maxDueFlashcardsCompleted, 20);
expect(viewModel.flashcardSetReports.length, 7);
expect(viewModel.flashcardSetReports[0], null);
expect(
viewModel.flashcardSetReports[1]!.date,
DateTime.now().subtract(const Duration(days: 5)).toInt(),
);
expect(viewModel.flashcardSetReports[2], null);
expect(
viewModel.flashcardSetReports[3]!.date,
DateTime.now().subtract(const Duration(days: 3)).toInt(),
);
expect(viewModel.flashcardSetReports[4], null);
expect(viewModel.flashcardSetReports[5], null);
expect(viewModel.flashcardSetReports[6]!.date, DateTime.now().toInt());
});

test('Top challenging flashcards', () async {
// Flashcards that fall within and outside of challenging flashcards limit
getAndRegisterDictionaryService(
getFlashcardSetFlashcards: List.generate(
Expand Down Expand Up @@ -173,6 +233,7 @@ void main() {
dueDate: DateTime.now().toInt(),
),
],
getFlashcardSetReportRange: [],
);

// Initialize viewmodel
Expand Down

0 comments on commit d1a6ab7

Please sign in to comment.