From a9a86df57949b55df59de89b7464baec864523fd Mon Sep 17 00:00:00 2001 From: Faucon <49079695+FauconSpartiate@users.noreply.github.com> Date: Tue, 19 Mar 2024 00:58:17 +0100 Subject: [PATCH] Add charts to subjects --- lib/calculations/manager.dart | 30 ++++ lib/main.dart | 7 +- lib/ui/routes/chart_route.dart | 144 +++++++++++++++++++ lib/ui/routes/main_route.dart | 66 +++++---- lib/ui/routes/subject_route.dart | 78 +++++++---- lib/ui/utilities/chart_utilities.dart | 148 ++++++++++++++++++++ lib/ui/widgets/charts.dart | 192 ++++++++++++++++++++++++++ pubspec.lock | 34 +++-- pubspec.yaml | 1 + 9 files changed, 639 insertions(+), 61 deletions(-) create mode 100644 lib/ui/routes/chart_route.dart create mode 100644 lib/ui/utilities/chart_utilities.dart create mode 100644 lib/ui/widgets/charts.dart diff --git a/lib/calculations/manager.dart b/lib/calculations/manager.dart index fabe01f2..723e8d6c 100644 --- a/lib/calculations/manager.dart +++ b/lib/calculations/manager.dart @@ -1,6 +1,9 @@ // Flutter imports: import "package:flutter/material.dart"; +// Package imports: +import "package:collection/collection.dart"; + // Project imports: import "package:graded/calculations/subject.dart"; import "package:graded/calculations/term.dart"; @@ -161,6 +164,33 @@ class Manager { return yearOverview; } + //TODO make more use of this function + static List getSubjectAcrossTerms(Subject subject) { + final List result = []; + + for (final term in getCurrentYear().terms) { + final Subject? s = getSubjectInTerm(subject, term); + if (s == null) continue; + result.add(s); + } + return result; + } + + static Subject? getSubjectInTerm(Subject? subject, Term term) { + if (subject == null) return null; + + final Subject? result = term.subjects.firstWhereOrNull((s) => s.name == subject.name); + + if (result != null) return result; + + for (final s in term.subjects) { + final Subject? child = s.children.firstWhereOrNull((c) => c.name == subject.name); + if (child != null) return child; + } + + throw ArgumentError("Subject not found in term"); + } + Map toJson() => { "years": years, }; diff --git a/lib/main.dart b/lib/main.dart index 89156b8d..f72f7dde 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import "package:device_info_plus/device_info_plus.dart"; import "package:dynamic_color/dynamic_color.dart"; import "package:flutter_displaymode/flutter_displaymode.dart"; import "package:flutter_localizations/flutter_localizations.dart"; +import "package:intl/date_symbol_data_local.dart"; import "package:provider/provider.dart"; // Project imports: @@ -89,8 +90,11 @@ class _AppContainerState extends State { supportedLocales: TranslationsClass.delegate.supportedLocales, localeResolutionCallback: (deviceLocale, supportedLocales) { if (supportedLocales.map((e) => e.languageCode).contains(deviceLocale?.languageCode)) { + initializeDateFormatting(deviceLocale?.languageCode); return deviceLocale; } + + initializeDateFormatting("en_GB"); return const Locale("en", "GB"); }, locale: provider.locale, @@ -128,6 +132,7 @@ Route createRoute(RouteSettings settings) { final CreationType type = (settings.arguments as CreationType?) ?? CreationType.edit; route = SubjectEditRoute(creationType: type); case "/subject": + case "/chart": if (settings.arguments == null) { throw ArgumentError("No arguments passed to route"); } @@ -136,7 +141,7 @@ Route createRoute(RouteSettings settings) { final Subject subject = arguments[1]!; route = RouteWidget( - routeType: RouteType.subject, + routeType: settings.name == "/subject" ? RouteType.subject : RouteType.chart, title: subject.name, arguments: arguments, ); diff --git a/lib/ui/routes/chart_route.dart b/lib/ui/routes/chart_route.dart new file mode 100644 index 00000000..1bf9d65f --- /dev/null +++ b/lib/ui/routes/chart_route.dart @@ -0,0 +1,144 @@ +// Dart imports: +import "dart:math"; + +// Flutter imports: +import "package:flutter/material.dart"; + +// Package imports: +import "package:sliver_tools/sliver_tools.dart"; + +// Project imports: +import "package:graded/calculations/manager.dart"; +import "package:graded/calculations/subject.dart"; +import "package:graded/calculations/term.dart"; +import "package:graded/localization/translations.dart"; +import "package:graded/ui/utilities/chart_utilities.dart"; +import "package:graded/ui/widgets/charts.dart"; +import "package:graded/ui/widgets/custom_safe_area.dart"; +import "package:graded/ui/widgets/list_widgets.dart"; + +class ChartRoute extends StatefulWidget { + const ChartRoute({ + super.key, + required this.term, + required this.subject, + this.parent, + }); + + final Term term; + final Subject subject; + final Subject? parent; + + @override + State createState() => _ChartRouteState(); +} + +class _ChartRouteState extends State { + final ScrollController scrollController = ScrollController(); + + void rebuild() { + setState(() {}); + } + + void refreshYearOverview() { + Manager.refreshYearOverview(); + rebuild(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + body: Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + CustomSliverSafeArea( + top: false, + maintainBottomViewPadding: true, + sliver: MultiSliver( + children: [ + SliverToBoxAdapter( + child: ResultRow( + result: widget.subject.getResult(), + preciseResult: widget.subject.getResult(precise: true), + leading: Text( + translations.yearly_average, + overflow: TextOverflow.fade, + softWrap: false, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + const SliverPadding(padding: EdgeInsets.only(top: 8)), + SliverToBoxAdapter( + child: StandardLineChart( + title: translations.average_over_time, + spots: getSubjectResultSpots( + subject: widget.subject, + ), + maxY: max(getHighestAverage(subject: widget.subject), getCurrentYear().maxGrade), + maxX: getCurrentYear().termCount - 1, + xLabelInterval: 1, + getBottomTitleWidget: getTermBottomWidgets, + getLeftTitleWidget: getLeftTitleWidgets, + showRollingAverage: true, + ), + ), + const SliverPadding( + padding: EdgeInsets.symmetric(vertical: 12), + sliver: SliverToBoxAdapter( + child: Divider(), + ), + ), + SliverToBoxAdapter( + child: Builder( + builder: (context) { + final spots = getSubjectTestSpots( + subject: widget.subject, + ); + + double lowestX = spots[0].x; + final DateTime lowestDate = DateTime.fromMillisecondsSinceEpoch(lowestX.toInt()); + if (lowestDate.month >= 9) { + lowestX = DateTime(lowestDate.year, 9).millisecondsSinceEpoch.toDouble(); + } else { + lowestX = DateTime(lowestDate.year - 1, 9).millisecondsSinceEpoch.toDouble(); + } + + double highestX = spots[0].x; + final DateTime highestDate = DateTime.fromMillisecondsSinceEpoch(highestX.toInt()); + if (highestDate.month >= 9) { + highestX = DateTime(highestDate.year + 1, 9).millisecondsSinceEpoch.toDouble(); + } else { + highestX = DateTime(highestDate.year, 9).millisecondsSinceEpoch.toDouble(); + } + + return StandardLineChart( + title: translations.testOther, + spots: spots, + minX: lowestX, + maxX: highestX, + maxY: max(getHighestTest(subject: widget.subject), getCurrentYear().maxGrade), + xGridInterval: 2629746000, // 1 month + xLabelInterval: Duration.millisecondsPerDay, + getBottomTitleWidget: getDateBottomWidgets, + getLeftTitleWidget: getLeftTitleWidgets, + showRollingAverage: true, + ); + }, + ), + ), + ], + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/ui/routes/main_route.dart b/lib/ui/routes/main_route.dart index 67820ce1..caa7f4ae 100644 --- a/lib/ui/routes/main_route.dart +++ b/lib/ui/routes/main_route.dart @@ -10,6 +10,7 @@ import "package:graded/calculations/subject.dart"; import "package:graded/calculations/term.dart"; import "package:graded/localization/translations.dart"; import "package:graded/misc/enums.dart"; +import "package:graded/ui/routes/chart_route.dart"; import "package:graded/ui/routes/home_route.dart"; import "package:graded/ui/routes/subject_route.dart"; import "package:graded/ui/utilities/haptics.dart"; @@ -20,6 +21,7 @@ import "package:graded/ui/widgets/popup_menus.dart"; enum RouteType { home, subject, + chart, } class RouteWidget extends StatefulWidget { @@ -63,7 +65,7 @@ class RouteWidgetState extends State with TickerProviderStateMixin tabController = TabController( length: children.length, - initialIndex: Manager.currentTerm, + initialIndex: widget.routeType != RouteType.chart ? Manager.currentTerm : 0, vsync: this, )..addListener(() { if (widget.routeType != RouteType.home) return; @@ -234,33 +236,47 @@ class RouteWidgetState extends State with TickerProviderStateMixin List children = []; - if (widget.routeType == RouteType.home) { - children = List.generate( - tabCount, - (index) => HomePage(term: getTerm(index)), - ); - } else { - if (widget.arguments == null) { - throw ArgumentError("No arguments passed to route"); - } - - final List arguments = widget.arguments! as List; - final Subject? parent = arguments[0]; - final Subject subject = arguments[1]!; + switch (widget.routeType) { + case RouteType.home: + children = List.generate( + tabCount, + (index) => HomePage(term: getTerm(index)), + ); - int tabCount = getCurrentYear().termCount; - if (getCurrentYear().validatedYear == 1) tabCount++; - if (tabCount > 1) tabCount++; + case RouteType.subject: + case RouteType.chart: + if (widget.arguments == null) { + throw ArgumentError("No arguments passed to route"); + } + + final List arguments = widget.arguments! as List; + final Subject? parent = arguments[0]; + final Subject subject = arguments[1]!; + + int tabCount = 1; + if (widget.routeType == RouteType.subject) { + tabCount = getCurrentYear().termCount; + if (getCurrentYear().validatedYear == 1) tabCount++; + if (tabCount > 1) tabCount++; + } + + children = List.generate(tabCount, (index) { + Term term = getTerm(index); + if (widget.routeType == RouteType.chart) { + term = getYearOverview(); + } - children = List.generate(tabCount, (index) { - final Term term = getTerm(index); - final Subject? newParent = parent != null ? term.subjects.firstWhere((element) => element.name == parent.name) : null; - final Subject newSubject = newParent != null - ? newParent.children.firstWhere((element) => element.name == subject.name) - : term.subjects.firstWhere((element) => element.name == subject.name); + final Subject? newParent = parent != null ? Manager.getSubjectInTerm(parent, term) : null; + final Subject newSubject = newParent != null + ? newParent.children.firstWhere((element) => element.name == subject.name) + : term.subjects.firstWhere((element) => element.name == subject.name); - return SubjectRoute(term: term, parent: newParent, subject: newSubject); - }); + if (widget.routeType == RouteType.subject) { + return SubjectRoute(term: term, parent: newParent, subject: newSubject); + } else { + return ChartRoute(term: term, parent: newParent, subject: newSubject); + } + }); } return children; diff --git a/lib/ui/routes/subject_route.dart b/lib/ui/routes/subject_route.dart index 59d67ae6..d59e2b99 100644 --- a/lib/ui/routes/subject_route.dart +++ b/lib/ui/routes/subject_route.dart @@ -1,3 +1,6 @@ +// Dart imports: +import "dart:math"; + // Flutter imports: import "package:flutter/material.dart"; @@ -92,34 +95,57 @@ class _SubjectRouteState extends State { child: ResultRow( result: widget.subject.getResult(), preciseResult: widget.subject.getResult(precise: true), - leading: !widget.term.isYearOverview - ? FadingEdgeScrollView.fromSingleChildScrollView( - child: SingleChildScrollView( - controller: scrollController, - scrollDirection: Axis.horizontal, - child: Row( - children: [ - ElevatedButton( - onPressed: () { - showBonusDialog(context, widget.subject).then((_) => refreshYearOverview()); - }, - style: getTonalButtonStyle(context), - child: Text("${translations.bonus}: ${Calculator.format( - widget.subject.bonus, - leadingZero: false, - showPlusSign: true, - )}"), - ), - ], + leading: FadingEdgeScrollView.fromSingleChildScrollView( + child: SingleChildScrollView( + controller: scrollController, + scrollDirection: Axis.horizontal, + child: Row( + children: [ + if (!widget.term.isYearOverview) + ElevatedButton( + onPressed: () { + showBonusDialog(context, widget.subject).then((_) => refreshYearOverview()); + }, + style: getTonalButtonStyle(context), + child: Text( + "${translations.bonus}: ${Calculator.format( + widget.subject.bonus, + leadingZero: false, + showPlusSign: true, + )}", + ), + ) + else + Text( + translations.yearly_average, + overflow: TextOverflow.fade, + softWrap: false, + style: Theme.of(context).textTheme.titleLarge, + ), + const Padding(padding: EdgeInsets.symmetric(horizontal: 4)), + IconButton.filledTonal( + onPressed: () { + Navigator.of(context).pushNamed( + "/chart", + arguments: [ + Manager.getSubjectInTerm(widget.parent, getYearOverview()), + Manager.getSubjectInTerm(widget.subject, getYearOverview()), + ], + ); + }, + icon: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(pi), + child: const Icon( + Icons.stacked_bar_chart, + ), ), + tooltip: translations.chartOther, ), - ) - : Text( - translations.yearly_average, - overflow: TextOverflow.fade, - softWrap: false, - style: Theme.of(context).textTheme.titleLarge, - ), + ], + ), + ), + ), ), ), ), diff --git a/lib/ui/utilities/chart_utilities.dart b/lib/ui/utilities/chart_utilities.dart new file mode 100644 index 00000000..8b967ef3 --- /dev/null +++ b/lib/ui/utilities/chart_utilities.dart @@ -0,0 +1,148 @@ +// Dart imports: +import "dart:math"; + +// Flutter imports: +import "package:flutter/material.dart"; + +// Package imports: +import "package:fl_chart/fl_chart.dart"; +import "package:intl/intl.dart"; + +// Project imports: +import "package:graded/calculations/manager.dart"; +import "package:graded/calculations/subject.dart"; +import "package:graded/ui/utilities/hints.dart"; + +double getHighestAverage({required Subject subject}) { + double highest = 0; + + Manager.getSubjectAcrossTerms(subject).forEach((subject) { + if (subject.result != null && subject.result! > highest) { + highest = subject.result!; + } + }); + + return highest; +} + +double getHighestTest({required Subject subject}) { + double highest = 0; + + Manager.getSubjectAcrossTerms(subject).forEach((subject) { + for (final test in subject.tests) { + if (test.result != null && test.result! > highest) { + highest = test.result!; + } + } + }); + + return highest; +} + +List getSubjectResultSpots({required Subject subject}) { + final List spots = []; + final List subjects = Manager.getSubjectAcrossTerms(subject); + + for (int i = 0; i < subjects.length; i++) { + final subject = subjects[i]; + if (subject.result == null) continue; + + spots.add(FlSpot(i.toDouble(), subject.result!)); + } + return spots; +} + +List getSubjectTestSpots({required Subject subject}) { + final List spots = []; + + Manager.getSubjectAcrossTerms(subject).forEach((s) { + for (final test in s.tests) { + if (test.result == null) continue; + + double truncatedTimestamp = test.timestamp.toDouble(); + + while (spots.any((element) => element.x == truncatedTimestamp)) { + truncatedTimestamp += 0.1; + } + + spots.add(FlSpot(truncatedTimestamp, test.result!)); + } + }); + + spots.sort((a, b) => a.x.compareTo(b.x)); + + return spots; +} + +Widget getBottomWidget(String value, TitleMeta meta) { + const style = TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ); + Widget text; + + text = Text(value, style: style); + + return SideTitleWidget( + axisSide: meta.axisSide, + child: text, + ); +} + +Widget getTermBottomWidgets(double value, TitleMeta meta) { + return getBottomWidget(getTermNameShort(termIndex: value.toInt()), meta); +} + +Widget getDateBottomWidgets(double value, TitleMeta meta) { + final DateTime date = DateTime.fromMillisecondsSinceEpoch(value.toInt()); + + if (date.day != 1 || (date.month % 2) == 0) return const SizedBox(); + + return getBottomWidget(DateFormat.MMM().format(date), meta); +} + +Widget getLeftTitleWidgets(double value, TitleMeta meta) { + const style = TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ); + + final String text = "${value.toInt()}"; + return Text(text, style: style, textAlign: TextAlign.left); +} + +List calculateRollingAverage(List data, int windowSize, {double? minX, double? maxX}) { + final List rollingAverage = []; + final List dataCopy = []; + + dataCopy.addAll(data); + + if (maxX != null && dataCopy.last.x != maxX) { + dataCopy.add(FlSpot(maxX, dataCopy.last.y)); + } + + if (minX != null && dataCopy.first.x != minX) { + rollingAverage.insert(0, FlSpot(minX, dataCopy.first.y)); + } + + for (int i = 0; i < dataCopy.length; i++) { + double weightedSum = 0; + double weightSum = 0; + double lastResult = 0; + + for (int j = i; j >= 0 && j > i - windowSize; j--) { + final bool isHigher = dataCopy[j].y >= lastResult; + + double weight = 1 + log(windowSize - (i - j)); + weight *= isHigher ? 1.5 : 1; + + weightedSum += dataCopy[j].y * weight; + weightSum += weight; + lastResult = dataCopy[j].y; + } + + rollingAverage.add(FlSpot(dataCopy[i].x, weightedSum / weightSum)); + } + + return rollingAverage; +} diff --git a/lib/ui/widgets/charts.dart b/lib/ui/widgets/charts.dart new file mode 100644 index 00000000..03f33974 --- /dev/null +++ b/lib/ui/widgets/charts.dart @@ -0,0 +1,192 @@ +// Dart imports: +import "dart:math"; + +// Flutter imports: +import "package:flutter/material.dart"; + +// Package imports: +import "package:fl_chart/fl_chart.dart"; + +// Project imports: +import "package:graded/calculations/calculator.dart"; +import "package:graded/ui/utilities/chart_utilities.dart"; + +class StandardLineChart extends StatefulWidget { + const StandardLineChart({ + super.key, + this.title = "", + required this.spots, + this.minX = 0, + this.maxX, + this.minY = 0, + this.maxY, + this.xLabelInterval, + this.xGridInterval = 1, + this.yGridInterval = 10, + this.getBottomTitleWidget, + this.getLeftTitleWidget, + this.autoMinX = false, + this.autoMinY = false, + this.showRollingAverage = false, + }); + + final String title; + + final List spots; + final double minX; + final double? maxX; + final double minY; + final double? maxY; + final int? xLabelInterval; + final int xGridInterval; + final int yGridInterval; + + final bool autoMinX; + final bool autoMinY; + final bool showRollingAverage; + + final Widget Function(double, TitleMeta)? getBottomTitleWidget; + final Widget Function(double, TitleMeta)? getLeftTitleWidget; + + @override + State createState() => _StandardLineChartState(); +} + +class _StandardLineChartState extends State { + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + widget.title, + style: Theme.of(context).textTheme.titleLarge, + softWrap: true, + overflow: TextOverflow.fade, + ), + ), + AspectRatio( + aspectRatio: 1.70, + child: Padding( + padding: const EdgeInsets.only( + right: 24, + left: 16, + top: 16, + bottom: 8, + ), + child: LineChart( + mainData(), + ), + ), + ), + ], + ); + } + + LineChartData mainData() { + final double? minX = widget.autoMinX ? null : widget.minX; + final double maxX = () { + final double spotMax = widget.spots.fold(0, (previousValue, element) => max(previousValue, element.x)); + if (widget.maxX == null) return spotMax; + return max(widget.maxX!, spotMax); + }(); + final double? minY = widget.autoMinY ? null : 0; + final double? maxY = widget.maxY; + + return LineChartData( + gridData: FlGridData( + horizontalInterval: widget.yGridInterval.toDouble(), + verticalInterval: widget.xGridInterval.toDouble(), + getDrawingHorizontalLine: (value) { + return FlLine( + color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1), + strokeWidth: 1, + ); + }, + getDrawingVerticalLine: (value) { + return FlLine( + color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1), + strokeWidth: 1, + ); + }, + ), + titlesData: FlTitlesData( + rightTitles: const AxisTitles(), + topTitles: const AxisTitles(), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + interval: widget.xLabelInterval?.toDouble(), + getTitlesWidget: widget.getBottomTitleWidget ?? defaultGetTitle, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: maxY != null ? maxY / 2 : null, + getTitlesWidget: widget.getLeftTitleWidget ?? defaultGetTitle, + reservedSize: 42, + ), + ), + ), + borderData: FlBorderData( + show: true, + border: Border.all(color: Theme.of(context).colorScheme.surfaceVariant), + ), + minX: minX, + maxX: maxX, + minY: minY, + maxY: maxY, + lineTouchData: LineTouchData( + touchSpotThreshold: 30, + touchTooltipData: LineTouchTooltipData( + fitInsideVertically: true, + tooltipBgColor: Theme.of(context).colorScheme.primary, + getTooltipItems: (List touchedSpots) { + return touchedSpots.map((LineBarSpot touchedSpot) { + if (touchedSpot.barIndex != 0) return null; + final TextStyle textStyle = Theme.of(context).textTheme.titleSmall!.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + ); + return LineTooltipItem( + Calculator.format(touchedSpot.y), + textStyle, + ); + }).toList(); + }, + ), + ), + lineBarsData: [ + LineChartBarData( + spots: widget.spots, + isCurved: true, + color: Theme.of(context).colorScheme.primary, + barWidth: 5, + isStrokeCapRound: true, + preventCurveOverShooting: true, + belowBarData: BarAreaData( + show: true, + color: Theme.of(context).colorScheme.primary.withOpacity(0.25), + ), + ), + if (widget.showRollingAverage) + LineChartBarData( + spots: calculateRollingAverage(widget.spots, 2, minX: minX, maxX: maxX), + isCurved: true, + color: Theme.of(context).colorScheme.secondary.withOpacity(0.5), + barWidth: 3, + isStrokeCapRound: true, + preventCurveOverShooting: true, + dashArray: const [5, 8], + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + color: Theme.of(context).colorScheme.primary.withOpacity(0.25), + ), + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index ffc9977b..1503ed5c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -181,10 +181,18 @@ packages: dependency: "direct main" description: name: dynamic_color - sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b + sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d url: "https://pub.dev" source: hosted - version: "1.6.9" + version: "1.7.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fading_edge_scrollview: dependency: "direct main" description: @@ -217,14 +225,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "00b74ae680df6b1135bdbea00a7d1fc072a9180b7c3f3702e4b19a9943f5ed7d" + url: "https://pub.dev" + source: hosted + version: "0.66.2" flex_color_picker: dependency: "direct main" description: name: flex_color_picker - sha256: "0871edc170153cfc3de316d30625f40a85daecfa76ce541641f3cc0ec7757cbf" + sha256: "5c846437069fb7afdd7ade6bf37e628a71d2ab0787095ddcb1253bf9345d5f3a" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.1" flex_color_scheme: dependency: "direct main" description: @@ -279,10 +295,10 @@ packages: dependency: "direct dev" description: name: flutter_native_splash - sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e" + sha256: edf39bcf4d74aca1eb2c1e43c3e445fd9f494013df7f0da752fefe72020eedc0 url: "https://pub.dev" source: hosted - version: "2.3.10" + version: "2.4.0" flutter_svg: dependency: "direct main" description: @@ -982,10 +998,10 @@ packages: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" win32_registry: dependency: transitive description: @@ -1019,5 +1035,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0-279.1.beta <4.0.0" + dart: ">=3.3.0 <4.0.0" flutter: ">=3.16.6" diff --git a/pubspec.yaml b/pubspec.yaml index ea68c230..d54e40ae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: diacritic: ^0.1.0 dynamic_color: ^1.4.0 fading_edge_scrollview: ^4.0.0 + fl_chart: ^0.66.0 flex_color_picker: ^3.3.0 flex_color_scheme: ^7.3.1 flutter: