From 237b4daababf6f183f61bf636fa51b103cc0a0d1 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Fri, 19 May 2023 20:59:37 +0200 Subject: [PATCH] Further work on pie chart for the nutritional plan --- lib/helpers/colors.dart | 42 +++++++ lib/widgets/core/charts.dart | 42 +++++++ lib/widgets/dashboard/widgets.dart | 2 +- lib/widgets/nutrition/charts.dart | 174 ++++++++++++++--------------- test/utils/colors.dart | 49 ++++++++ 5 files changed, 218 insertions(+), 91 deletions(-) create mode 100644 lib/helpers/colors.dart create mode 100644 test/utils/colors.dart diff --git a/lib/helpers/colors.dart b/lib/helpers/colors.dart new file mode 100644 index 00000000..b7fd2288 --- /dev/null +++ b/lib/helpers/colors.dart @@ -0,0 +1,42 @@ +import 'dart:ui'; + +const LIST_OF_COLORS8 = [ + Color(0xFF2A4C7D), + Color(0xFF5B5291), + Color(0xFF8E5298), + Color(0xFFBF5092), + Color(0xFFE7537E), + Color(0xFFFF6461), + Color(0xFFFF813D), + Color(0xFFFFA600), +]; + +const LIST_OF_COLORS5 = [ + Color(0xFF2A4C7D), + Color(0xFF825298), + Color(0xFFD45089), + Color(0xFFFF6A59), + Color(0xFFFFA600), +]; + +const LIST_OF_COLORS3 = [ + Color(0xFF2A4C7D), + Color(0xFFD45089), + Color(0xFFFFA600), +]; + +Iterable generateChartColors(int nrOfItems) sync* { + final List colors; + + if (nrOfItems <= 3) { + colors = LIST_OF_COLORS3; + } else if (nrOfItems <= 5) { + colors = LIST_OF_COLORS5; + } else { + colors = LIST_OF_COLORS8; + } + + for (final color in colors) { + yield color; + } +} diff --git a/lib/widgets/core/charts.dart b/lib/widgets/core/charts.dart index d1bddd66..6eef78a7 100644 --- a/lib/widgets/core/charts.dart +++ b/lib/widgets/core/charts.dart @@ -57,3 +57,45 @@ class MeasurementChartWidget extends StatelessWidget { ); } } + +class Indicator extends StatelessWidget { + const Indicator({ + super.key, + required this.color, + required this.text, + required this.isSquare, + this.size = 16, + this.textColor, + }); + + final Color color; + final String text; + final bool isSquare; + final double size; + final Color? textColor; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: isSquare ? BoxShape.rectangle : BoxShape.circle, + color: color, + ), + ), + const SizedBox( + width: 4, + ), + Text( + text, + style: TextStyle( + color: textColor, + ), + ) + ], + ); + } +} diff --git a/lib/widgets/dashboard/widgets.dart b/lib/widgets/dashboard/widgets.dart index 63daaf99..893cc2d2 100644 --- a/lib/widgets/dashboard/widgets.dart +++ b/lib/widgets/dashboard/widgets.dart @@ -190,7 +190,7 @@ class _DashboardNutritionWidgetState extends State { Container( padding: const EdgeInsets.all(15), height: 180, - child: FlNutritionalPlanPieChartWidget(_plan!.nutritionalValues), + child: NutritionalPlanPieChartWidget(_plan!.nutritionalValues), ) ], ), diff --git a/lib/widgets/nutrition/charts.dart b/lib/widgets/nutrition/charts.dart index a0ae27d2..6db07f00 100644 --- a/lib/widgets/nutrition/charts.dart +++ b/lib/widgets/nutrition/charts.dart @@ -21,9 +21,11 @@ import 'package:collection/collection.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:wger/helpers/colors.dart'; import 'package:wger/models/nutrition/nutritional_plan.dart'; import 'package:wger/models/nutrition/nutritional_values.dart'; import 'package:wger/theme/theme.dart'; +import 'package:wger/widgets/core/charts.dart'; class NutritionData { final String name; @@ -32,74 +34,119 @@ class NutritionData { NutritionData(this.name, this.value); } -class FlNutritionalPlanPieChartWidget extends StatefulWidget { +class NutritionalPlanPieChartWidget extends StatefulWidget { final NutritionalValues nutritionalValues; - const FlNutritionalPlanPieChartWidget(this.nutritionalValues); + const NutritionalPlanPieChartWidget(this.nutritionalValues); @override State createState() => FlNutritionalPlanPieChartState(); } -class FlNutritionalPlanPieChartState extends State { +class FlNutritionalPlanPieChartState extends State { int touchedIndex = -1; @override Widget build(BuildContext context) { - return PieChart( - PieChartData( - pieTouchData: PieTouchData( - touchCallback: (FlTouchEvent event, pieTouchResponse) { - setState(() { - if (!event.isInterestedForInteractions || - pieTouchResponse == null || - pieTouchResponse.touchedSection == null) { - touchedIndex = -1; - return; - } - touchedIndex = pieTouchResponse.touchedSection!.touchedSectionIndex; - }); - }, + return Row( + children: [ + const SizedBox( + height: 18, ), - borderData: FlBorderData( - show: false, + Expanded( + child: AspectRatio( + aspectRatio: 1, + child: PieChart( + PieChartData( + pieTouchData: PieTouchData( + touchCallback: (FlTouchEvent event, pieTouchResponse) { + setState(() { + if (!event.isInterestedForInteractions || + pieTouchResponse == null || + pieTouchResponse.touchedSection == null) { + touchedIndex = -1; + return; + } + touchedIndex = pieTouchResponse.touchedSection!.touchedSectionIndex; + }); + }, + ), + borderData: FlBorderData( + show: false, + ), + sectionsSpace: 0, + centerSpaceRadius: 0, + sections: showingSections(), + ), + ), + ), ), - sectionsSpace: 0, - centerSpaceRadius: 40, - sections: showingSections(), - ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Indicator( + color: LIST_OF_COLORS3[0], + text: AppLocalizations.of(context).fat, + isSquare: true, + ), + const SizedBox( + height: 4, + ), + Indicator( + color: LIST_OF_COLORS3[1], + text: AppLocalizations.of(context).protein, + isSquare: true, + ), + const SizedBox( + height: 4, + ), + Indicator( + color: LIST_OF_COLORS3[2], + text: AppLocalizations.of(context).carbohydrates, + isSquare: true, + ), + ], + ), + const SizedBox( + width: 28, + ), + ], ); } List showingSections() { + final colors = generateChartColors(3).iterator; + return List.generate(3, (i) { final isTouched = i == touchedIndex; - final fontSize = isTouched ? 25.0 : 16.0; - final radius = isTouched ? 60.0 : 50.0; + final radius = isTouched ? 92.0 : 80.0; + colors.moveNext(); + switch (i) { case 0: return PieChartSectionData( - color: Colors.blue, - value: widget.nutritionalValues.fat, - title: AppLocalizations.of(context).fat, - radius: radius, - titleStyle: TextStyle(fontSize: fontSize), - ); + color: colors.current, + value: widget.nutritionalValues.fat, + title: '${widget.nutritionalValues.fat.toStringAsFixed(1)}g', + titlePositionPercentageOffset: 0.5, + radius: radius, + titleStyle: const TextStyle(color: Colors.white70)); case 1: return PieChartSectionData( - color: Colors.amber, + color: colors.current, value: widget.nutritionalValues.protein, - title: AppLocalizations.of(context).protein, + title: '${widget.nutritionalValues.protein.toStringAsFixed(1)}g', + titlePositionPercentageOffset: 0.5, radius: radius, - titleStyle: TextStyle(fontSize: fontSize), ); case 2: return PieChartSectionData( - color: Colors.purple, + color: colors.current, value: widget.nutritionalValues.carbohydrates, - title: AppLocalizations.of(context).carbohydrates, + title: '${widget.nutritionalValues.carbohydrates.toStringAsFixed(1)}g', + titlePositionPercentageOffset: 0.5, radius: radius, - titleStyle: TextStyle(fontSize: fontSize), ); default: @@ -109,59 +156,6 @@ class FlNutritionalPlanPieChartState extends State([ - charts.Series( - id: 'NutritionalValues', - domainFn: (nutritionEntry, index) => nutritionEntry.name, - measureFn: (nutritionEntry, index) => nutritionEntry.value, - data: [ - NutritionData( - AppLocalizations.of(context).protein, - _nutritionalValues.protein, - ), - NutritionData( - AppLocalizations.of(context).fat, - _nutritionalValues.fat, - ), - NutritionData( - AppLocalizations.of(context).carbohydrates, - _nutritionalValues.carbohydrates, - ), - ], - labelAccessorFn: (NutritionData row, _) => - '${row.name}\n${row.value.toStringAsFixed(0)}${AppLocalizations.of(context).g}', - ) - ], - defaultRenderer: charts.ArcRendererConfig(arcWidth: 60, arcRendererDecorators: [ - charts.ArcLabelDecorator( - labelPosition: charts.ArcLabelPosition.outside, - ) - ]) - /* - defaultRenderer: new charts.ArcRendererConfig( - arcWidth: 60, - arcRendererDecorators: [new charts.ArcLabelDecorator()], - ), - - */ - ); - } -} - class NutritionalDiaryChartWidget extends StatelessWidget { const NutritionalDiaryChartWidget({ Key? key, diff --git a/test/utils/colors.dart b/test/utils/colors.dart new file mode 100644 index 00000000..d3e48d7d --- /dev/null +++ b/test/utils/colors.dart @@ -0,0 +1,49 @@ +import 'dart:ui'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:wger/helpers/colors.dart'; + +void main() { + group('test the color utility', () { + test('3 items or less', () { + final result = generateChartColors(2).iterator; + expect(result.current, equals(const Color(0xFF2A4C7D))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFD45089))); + }); + + test('5 items or less', () { + final result = generateChartColors(5).iterator; + expect(result.current, equals(const Color(0xFF2A4C7D))); + result.moveNext(); + expect(result.current, equals(const Color(0xFF825298))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFD45089))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFFF6A59))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFFFA600))); + }); + + test('8 items or more - last ones undefined', () { + final result = generateChartColors(8).iterator; + expect(result.current, equals(const Color(0xFF2A4C7D))); + result.moveNext(); + expect(result.current, equals(const Color(0xFF5B5291))); + result.moveNext(); + expect(result.current, equals(const Color(0xFF8E5298))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFBF5092))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFE7537E))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFFF6461))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFFF813D))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFFFA600))); + result.moveNext(); + expect(result.current, isNull); + }); + }); +}