diff --git a/CHANGELOG.md b/CHANGELOG.md index 99114db565..c9d049a8d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## nextVersion +* **FEATURE** (by @Dartek12) Implemented animations on start for LineChart, BarChart and RadarChart, #406 * **BUGFIX** (by @Anas35) Fix Tooltip not displaying when value from BackgroundBarChartRodData is less than zero. #1345. * **BUGFIX** (by @imaNNeo) Fix Negative BarChartRodStackItem are not drawn correctly bug, #1347 * **BUGFIX** (by @imaNNeo) Fix bar_chart_helper minY calculation bug, #1388 diff --git a/example/lib/presentation/samples/line/line_chart_sample10.dart b/example/lib/presentation/samples/line/line_chart_sample10.dart index a90fd0f1e7..1a0a5a3363 100644 --- a/example/lib/presentation/samples/line/line_chart_sample10.dart +++ b/example/lib/presentation/samples/line/line_chart_sample10.dart @@ -100,6 +100,8 @@ class _LineChartSample10State extends State { show: false, ), ), + initialAnimationConfiguration: + const InitialAnimationConfiguration(enabled: false), ), ), ) diff --git a/example/lib/presentation/samples/line/line_chart_sample8.dart b/example/lib/presentation/samples/line/line_chart_sample8.dart index 75b629a1ce..71d650cfc1 100644 --- a/example/lib/presentation/samples/line/line_chart_sample8.dart +++ b/example/lib/presentation/samples/line/line_chart_sample8.dart @@ -1,10 +1,8 @@ import 'dart:async'; -import 'dart:ui' as ui; import 'package:fl_chart_app/presentation/resources/app_resources.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show rootBundle; import 'package:flutter_svg/flutter_svg.dart'; class LineChartSample8 extends StatefulWidget { @@ -22,12 +20,8 @@ class _LineChartSample8State extends State { bool showAvg = false; - Future loadImage(String asset) async { - final data = await rootBundle.load(asset); - final codec = await ui.instantiateImageCodec(data.buffer.asUint8List()); - final fi = await codec.getNextFrame(); - return fi.image; - } + // Caching the future to prevent rebuilding the widget + late final Future _loadSvgFuture = loadSvg(); Future loadSvg() async { const rawSvg = @@ -43,7 +37,7 @@ class _LineChartSample8State extends State { @override Widget build(BuildContext context) { return FutureBuilder( - future: loadSvg(), + future: _loadSvgFuture, builder: (BuildContext context, imageSnapshot) { if (imageSnapshot.connectionState == ConnectionState.done) { return Stack( diff --git a/example/lib/presentation/samples/radar/radar_chart_sample1.dart b/example/lib/presentation/samples/radar/radar_chart_sample1.dart index a8c5ab792a..ad2090a4fe 100644 --- a/example/lib/presentation/samples/radar/radar_chart_sample1.dart +++ b/example/lib/presentation/samples/radar/radar_chart_sample1.dart @@ -45,10 +45,13 @@ class _RadarChartSample1State extends State { color: AppColors.mainTextColor2, ), ), - Slider( - value: angleValue, - max: 360, - onChanged: (double value) => setState(() => angleValue = value), + Expanded( + child: Slider( + value: angleValue, + max: 360, + onChanged: (double value) => + setState(() => angleValue = value), + ), ), Checkbox( value: relativeAngleMode, diff --git a/lib/fl_chart.dart b/lib/fl_chart.dart index d3597fb81e..6c345741aa 100644 --- a/lib/fl_chart.dart +++ b/lib/fl_chart.dart @@ -7,6 +7,7 @@ export 'src/chart/base/axis_chart/axis_chart_data.dart'; export 'src/chart/base/axis_chart/axis_chart_widgets.dart'; export 'src/chart/base/base_chart/base_chart_data.dart'; export 'src/chart/base/base_chart/fl_touch_event.dart'; +export 'src/chart/base/base_chart/initial_animation_configuration.dart'; export 'src/chart/line_chart/line_chart.dart'; export 'src/chart/line_chart/line_chart_data.dart'; export 'src/chart/pie_chart/pie_chart.dart'; diff --git a/lib/src/chart/bar_chart/bar_chart.dart b/lib/src/chart/bar_chart/bar_chart.dart index 83c19e5cdb..665f38ed67 100644 --- a/lib/src/chart/bar_chart/bar_chart.dart +++ b/lib/src/chart/bar_chart/bar_chart.dart @@ -1,6 +1,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/src/chart/bar_chart/bar_chart_renderer.dart'; import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart'; +import 'package:fl_chart/src/chart/base/base_chart/initial_animation_mixin.dart'; import 'package:flutter/cupertino.dart'; /// Renders a bar chart as a widget, using provided [BarChartData]. @@ -13,6 +14,7 @@ class BarChart extends ImplicitlyAnimatedWidget { const BarChart( this.data, { this.chartRendererKey, + this.initialAnimationConfiguration = const InitialAnimationConfiguration(), super.key, Duration swapAnimationDuration = const Duration(milliseconds: 150), Curve swapAnimationCurve = Curves.linear, @@ -28,12 +30,16 @@ class BarChart extends ImplicitlyAnimatedWidget { /// render the chart itself (without anything around the chart). final Key? chartRendererKey; + /// Determines if the initial animation is enabled. + final InitialAnimationConfiguration initialAnimationConfiguration; + /// Creates a [_BarChartState] @override _BarChartState createState() => _BarChartState(); } -class _BarChartState extends AnimatedWidgetBaseState { +class _BarChartState extends AnimatedWidgetBaseState + with InitialAnimationMixin { /// we handle under the hood animations (implicit animations) via this tween, /// it lerps between the old [BarChartData] to the new one. BarChartDataTween? _barChartDataTween; @@ -44,6 +50,13 @@ class _BarChartState extends AnimatedWidgetBaseState { final Map> _showingTouchedTooltips = {}; + @override + InitialAnimationConfiguration get initialAnimationConfiguration => + widget.initialAnimationConfiguration; + + @override + Tween? get tween => _barChartDataTween; + @override Widget build(BuildContext context) { final showingData = _getData(); @@ -117,13 +130,49 @@ class _BarChartState extends AnimatedWidgetBaseState { }); } + @override + BarChartData getAppearanceAnimationData(BarChartData data) { + final startingY = getAppearanceValue(data.minY, data.maxY); + return data.copyWith( + barGroups: data.barGroups.map((barGroup) { + return barGroup.copyWith( + barRods: _getAppearanceBarRods(startingY, barGroup.barRods), + ); + }).toList(), + ); + } + + List _getAppearanceBarRods( + double startingY, + List barRods, + ) { + final rods = []; + BarChartRodData? previousRod; + var offset = 0.0; + + for (final rod in barRods) { + if (previousRod != null) { + offset += rod.fromY - previousRod.toY; + } + final y = startingY + offset; + rods.add(rod.copyWith(fromY: y, toY: y)); + previousRod = rod; + } + return rods; + } + @override void forEachTween(TweenVisitor visitor) { _barChartDataTween = visitor( _barChartDataTween, - widget.data, - (dynamic value) => - BarChartDataTween(begin: value as BarChartData, end: widget.data), + _getData(), + (dynamic value) { + final initialData = constructInitialData(value as BarChartData); + return BarChartDataTween( + begin: initialData, + end: initialData, + ); + }, ) as BarChartDataTween?; } } diff --git a/lib/src/chart/base/base_chart/initial_animation_configuration.dart b/lib/src/chart/base/base_chart/initial_animation_configuration.dart new file mode 100644 index 0000000000..057f38ffcf --- /dev/null +++ b/lib/src/chart/base/base_chart/initial_animation_configuration.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; + +/// It holds configuration for initial animation +class InitialAnimationConfiguration with EquatableMixin { + const InitialAnimationConfiguration({ + this.enabled = true, + this.initialValue, + }); + + /// Determines if the initial animation is enabled. + final bool enabled; + + /// Initial value of the animation. If null then max(0, minY) is used. + final double? initialValue; + + @override + List get props => [enabled, initialValue]; +} diff --git a/lib/src/chart/base/base_chart/initial_animation_mixin.dart b/lib/src/chart/base/base_chart/initial_animation_mixin.dart new file mode 100644 index 0000000000..25cc57d42f --- /dev/null +++ b/lib/src/chart/base/base_chart/initial_animation_mixin.dart @@ -0,0 +1,55 @@ +import 'package:fl_chart/src/chart/base/base_chart/initial_animation_configuration.dart'; +import 'package:flutter/material.dart'; + +@optionalTypeArgs +mixin InitialAnimationMixin + on AnimatedWidgetBaseState { + InitialAnimationConfiguration get initialAnimationConfiguration; + Tween? get tween; + + T? _targetValue; + + @override + void initState() { + super.initState(); + _handleInitialAnimation(); + } + + void _handleInitialAnimation() { + if (!initialAnimationConfiguration.enabled) { + return; + } + + if (tween == null || _targetValue == null) { + return; + } + tween! + ..begin = tween!.evaluate(animation) + ..end = _targetValue; + + controller + ..value = 0.0 + ..forward(); + didUpdateTweens(); + } + + T constructInitialData(T data) { + if (!initialAnimationConfiguration.enabled) { + return data; + } + _targetValue = data; + + return getAppearanceAnimationData(data); + } + + T getAppearanceAnimationData(T data); + + double getAppearanceValue(double minY, double maxY) { + final initialValue = initialAnimationConfiguration.initialValue; + if (initialValue != null) { + return initialValue; + } + + return (minY <= 0 && maxY >= 0) ? 0.0 : minY; + } +} diff --git a/lib/src/chart/line_chart/line_chart.dart b/lib/src/chart/line_chart/line_chart.dart index cee6c9d60d..211e83d172 100644 --- a/lib/src/chart/line_chart/line_chart.dart +++ b/lib/src/chart/line_chart/line_chart.dart @@ -1,5 +1,6 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart'; +import 'package:fl_chart/src/chart/base/base_chart/initial_animation_mixin.dart'; import 'package:fl_chart/src/chart/line_chart/line_chart_renderer.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -14,6 +15,7 @@ class LineChart extends ImplicitlyAnimatedWidget { const LineChart( this.data, { this.chartRendererKey, + this.initialAnimationConfiguration = const InitialAnimationConfiguration(), super.key, super.duration = const Duration(milliseconds: 150), super.curve = Curves.linear, @@ -26,12 +28,16 @@ class LineChart extends ImplicitlyAnimatedWidget { /// render the chart itself (without anything around the chart). final Key? chartRendererKey; + /// Determines if the initial animation is enabled. + final InitialAnimationConfiguration initialAnimationConfiguration; + /// Creates a [_LineChartState] @override _LineChartState createState() => _LineChartState(); } -class _LineChartState extends AnimatedWidgetBaseState { +class _LineChartState extends AnimatedWidgetBaseState + with InitialAnimationMixin { /// we handle under the hood animations (implicit animations) via this tween, /// it lerps between the old [LineChartData] to the new one. LineChartDataTween? _lineChartDataTween; @@ -44,6 +50,13 @@ class _LineChartState extends AnimatedWidgetBaseState { final Map> _showingTouchedIndicators = {}; + @override + InitialAnimationConfiguration get initialAnimationConfiguration => + widget.initialAnimationConfiguration; + + @override + Tween? get tween => _lineChartDataTween; + @override Widget build(BuildContext context) { final showingData = _getData(); @@ -123,13 +136,35 @@ class _LineChartState extends AnimatedWidgetBaseState { }); } + @override + LineChartData getAppearanceAnimationData(LineChartData data) { + final startingY = getAppearanceValue(data.minY, data.maxY); + return data.copyWith( + lineBarsData: data.lineBarsData.map((barData) { + return barData.copyWith( + spots: barData.spots.map((spot) { + if (spot.isNull()) { + return FlSpot.nullSpot; + } + return FlSpot(spot.x, startingY); + }).toList(), + ); + }).toList(), + ); + } + @override void forEachTween(TweenVisitor visitor) { _lineChartDataTween = visitor( _lineChartDataTween, _getData(), - (dynamic value) => - LineChartDataTween(begin: value as LineChartData, end: widget.data), + (dynamic value) { + final initialData = constructInitialData(value as LineChartData); + return LineChartDataTween( + begin: initialData, + end: initialData, + ); + }, ) as LineChartDataTween?; } } diff --git a/lib/src/chart/radar_chart/radar_chart.dart b/lib/src/chart/radar_chart/radar_chart.dart index cab84a9f8f..0ca0d44766 100644 --- a/lib/src/chart/radar_chart/radar_chart.dart +++ b/lib/src/chart/radar_chart/radar_chart.dart @@ -1,3 +1,5 @@ +import 'package:fl_chart/src/chart/base/base_chart/initial_animation_configuration.dart'; +import 'package:fl_chart/src/chart/base/base_chart/initial_animation_mixin.dart'; import 'package:fl_chart/src/chart/radar_chart/radar_chart_data.dart'; import 'package:fl_chart/src/chart/radar_chart/radar_chart_renderer.dart'; import 'package:flutter/material.dart'; @@ -11,6 +13,8 @@ class RadarChart extends ImplicitlyAnimatedWidget { /// which default is [Curves.linear]. const RadarChart( this.data, { + this.chartRendererKey, + this.initialAnimationConfiguration = const InitialAnimationConfiguration(), super.key, Duration swapAnimationDuration = const Duration(milliseconds: 150), Curve swapAnimationCurve = Curves.linear, @@ -22,36 +26,59 @@ class RadarChart extends ImplicitlyAnimatedWidget { /// Determines how the [RadarChart] should be look like. final RadarChartData data; + /// We pass this key to our renderers which are supposed to + /// render the chart itself (without anything around the chart). + final Key? chartRendererKey; + + /// Determines if the initial animation is enabled. + final InitialAnimationConfiguration initialAnimationConfiguration; + @override _RadarChartState createState() => _RadarChartState(); } -class _RadarChartState extends AnimatedWidgetBaseState { +class _RadarChartState extends AnimatedWidgetBaseState + with InitialAnimationMixin { /// we handle under the hood animations (implicit animations) via this tween, /// it lerps between the old [RadarChartData] to the new one. RadarChartDataTween? _radarChartDataTween; + @override + InitialAnimationConfiguration get initialAnimationConfiguration => + widget.initialAnimationConfiguration; + + @override + Tween? get tween => _radarChartDataTween; + @override Widget build(BuildContext context) { - final showingData = _getDate(); + final showingData = _getData(); return RadarChartLeaf( + key: widget.chartRendererKey, data: _radarChartDataTween!.evaluate(animation), targetData: showingData, ); } - RadarChartData _getDate() { + RadarChartData _getData() { return widget.data; } + @override + RadarChartData getAppearanceAnimationData(RadarChartData data) { + return data.copyWith(scaleFactor: 0); + } + @override void forEachTween(TweenVisitor visitor) { - _radarChartDataTween = visitor( - _radarChartDataTween, - widget.data, - (dynamic value) => - RadarChartDataTween(begin: value as RadarChartData, end: widget.data), - ) as RadarChartDataTween?; + _radarChartDataTween = + visitor(_radarChartDataTween, _getData(), (dynamic value) { + final initialData = constructInitialData(value as RadarChartData); + return RadarChartDataTween( + begin: initialData, + end: initialData, + ); + }) as RadarChartDataTween?; } } diff --git a/lib/src/chart/radar_chart/radar_chart_data.dart b/lib/src/chart/radar_chart/radar_chart_data.dart index d737b93498..771a153d01 100644 --- a/lib/src/chart/radar_chart/radar_chart_data.dart +++ b/lib/src/chart/radar_chart/radar_chart_data.dart @@ -71,6 +71,7 @@ class RadarChartData extends BaseChartData with EquatableMixin { BorderSide? gridBorderData, RadarTouchData? radarTouchData, super.borderData, + double? scaleFactor, }) : assert(dataSets != null && dataSets.hasEqualDataEntriesLength), assert( tickCount == null || tickCount >= 1, @@ -82,6 +83,7 @@ class RadarChartData extends BaseChartData with EquatableMixin { titlePositionPercentageOffset <= 1, 'titlePositionPercentageOffset must be something between 0 and 1 ', ), + assert(scaleFactor == null || scaleFactor >= 0 && scaleFactor <= 1), dataSets = dataSets ?? const [], radarBackgroundColor = radarBackgroundColor ?? Colors.transparent, radarBorderData = radarBorderData ?? const BorderSide(width: 2), @@ -91,6 +93,7 @@ class RadarChartData extends BaseChartData with EquatableMixin { tickCount = tickCount ?? 1, tickBorderData = tickBorderData ?? const BorderSide(width: 2), gridBorderData = gridBorderData ?? const BorderSide(width: 2), + scaleFactor = scaleFactor ?? 1.0, super( touchData: radarTouchData ?? RadarTouchData(), ); @@ -151,6 +154,9 @@ class RadarChartData extends BaseChartData with EquatableMixin { /// Defines style of showing [RadarChart] grid borders. final BorderSide gridBorderData; + /// Defines scale factor of showing [RadarChart] radars. + final double scaleFactor; + /// Handles touch behaviors and responses. final RadarTouchData radarTouchData; @@ -200,6 +206,7 @@ class RadarChartData extends BaseChartData with EquatableMixin { BorderSide? gridBorderData, RadarTouchData? radarTouchData, FlBorderData? borderData, + double? scaleFactor, }) => RadarChartData( dataSets: dataSets ?? this.dataSets, @@ -216,6 +223,7 @@ class RadarChartData extends BaseChartData with EquatableMixin { gridBorderData: gridBorderData ?? this.gridBorderData, radarTouchData: radarTouchData ?? this.radarTouchData, borderData: borderData ?? this.borderData, + scaleFactor: scaleFactor ?? this.scaleFactor, ); /// Lerps a [BaseChartData] based on [t] value, check [Tween.lerp]. @@ -241,6 +249,7 @@ class RadarChartData extends BaseChartData with EquatableMixin { radarShape: b.radarShape, tickBorderData: BorderSide.lerp(a.tickBorderData, b.tickBorderData, t), borderData: FlBorderData.lerp(a.borderData, b.borderData, t), + scaleFactor: lerpDouble(a.scaleFactor, b.scaleFactor, t), radarTouchData: b.radarTouchData, ); } else { @@ -264,6 +273,7 @@ class RadarChartData extends BaseChartData with EquatableMixin { ticksTextStyle, tickBorderData, gridBorderData, + scaleFactor, radarTouchData, ]; } diff --git a/lib/src/chart/radar_chart/radar_chart_painter.dart b/lib/src/chart/radar_chart/radar_chart_painter.dart index ab09ebc3e2..c6190a149c 100644 --- a/lib/src/chart/radar_chart/radar_chart_painter.dart +++ b/lib/src/chart/radar_chart/radar_chart_painter.dart @@ -444,7 +444,8 @@ class RadarChartPainter extends BaseChartPainter { final xAngle = cos(angle * j - pi / 2); final yAngle = sin(angle * j - pi / 2); - final scaledPoint = getScaledPoint(point, radius, data); + final scaledPoint = + getScaledPoint(point, radius, data) * data.scaleFactor; final entryOffset = Offset( centerX + scaledPoint * xAngle, diff --git a/test/chart/bar_chart/bar_chart_test.dart b/test/chart/bar_chart/bar_chart_test.dart new file mode 100644 index 0000000000..a979e4f9e2 --- /dev/null +++ b/test/chart/bar_chart/bar_chart_test.dart @@ -0,0 +1,125 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/bar_chart/bar_chart_renderer.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + final data = BarChartData( + barTouchData: BarTouchData(enabled: false), + barGroups: [ + BarChartGroupData(x: 0, barRods: [BarChartRodData(toY: 3)]), + BarChartGroupData(x: 1, barRods: [BarChartRodData(toY: 8)]), + BarChartGroupData(x: 2, barRods: [BarChartRodData(toY: 5)]), + ], + ); + + group('BarChart', () { + testWidgets( + 'Provides modified initial data when animation is enabled (by default)', + (tester) async { + final key = GlobalKey(); + final renderKey = GlobalKey(); + + await tester.pumpWidget( + _BarChartScaffold( + chartKey: key, + renderKey: renderKey, + data: data, + initialAnimationConfiguration: + const InitialAnimationConfiguration(initialValue: 0), + ), + ); + + final state = key.currentState! as AnimatedWidgetBaseState; + + // Pre-animation: + expect(state.animation.value, 0.0); + expect(state.animation.status, AnimationStatus.forward); + expect( + _getCurrentBarChartLeaf(renderKey).data, + data.copyWith( + barGroups: [ + BarChartGroupData(x: 0, barRods: [BarChartRodData(toY: 0)]), + BarChartGroupData(x: 1, barRods: [BarChartRodData(toY: 0)]), + BarChartGroupData(x: 2, barRods: [BarChartRodData(toY: 0)]), + ], + ), + ); + + await tester.pumpAndSettle(); + + // Post-animation: + expect(state.animation.value, 1.0); + expect(state.animation.status, AnimationStatus.completed); + expect(_getCurrentBarChartLeaf(renderKey).data, data); + }); + + testWidgets( + "Doesn't provide modified initial data when animation is disabled", + (tester) async { + final key = GlobalKey(); + final renderKey = GlobalKey(); + + await tester.pumpWidget( + _BarChartScaffold( + chartKey: key, + renderKey: renderKey, + data: data, + initialAnimationConfiguration: + const InitialAnimationConfiguration(enabled: false), + ), + ); + + final state = key.currentState! as AnimatedWidgetBaseState; + + // Pre-animation: + expect(state.animation.value, 0.0); + expect(state.animation.status, AnimationStatus.dismissed); + expect(_getCurrentBarChartLeaf(renderKey).data, data); + + await tester.pumpAndSettle(); + + // Post-animation: + expect(state.animation.value, 0.0); + expect(state.animation.status, AnimationStatus.dismissed); + expect(_getCurrentBarChartLeaf(renderKey).data, data); + }); + }); +} + +const viewSize = Size(400, 400); + +BarChartLeaf _getCurrentBarChartLeaf(GlobalKey key) => + key.currentContext!.widget as BarChartLeaf; + +class _BarChartScaffold extends StatelessWidget { + const _BarChartScaffold({ + required this.chartKey, + required this.renderKey, + required this.data, + this.initialAnimationConfiguration = const InitialAnimationConfiguration(), + }); + + final Key chartKey; + final Key renderKey; + final BarChartData data; + final InitialAnimationConfiguration initialAnimationConfiguration; + + @override + Widget build(BuildContext context) => MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: BarChart( + chartRendererKey: renderKey, + key: chartKey, + initialAnimationConfiguration: initialAnimationConfiguration, + data, + ), + ), + ), + ), + ); +} diff --git a/test/chart/line_chart/line_chart_test.dart b/test/chart/line_chart/line_chart_test.dart new file mode 100644 index 0000000000..3cc508950b --- /dev/null +++ b/test/chart/line_chart/line_chart_test.dart @@ -0,0 +1,123 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart_renderer.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + final data = LineChartData( + lineTouchData: const LineTouchData(enabled: false), + lineBarsData: [ + LineChartBarData(spots: const [FlSpot(0, 3), FlSpot(1, 1), FlSpot(2, 2)]), + ], + ); + + group('LineChart', () { + testWidgets( + 'Provides modified initial data when animation is enabled (by default)', + (tester) async { + final key = GlobalKey(); + final renderKey = GlobalKey(); + + await tester.pumpWidget( + _LineChartScaffold( + chartKey: key, + renderKey: renderKey, + data: data, + initialAnimationConfiguration: + const InitialAnimationConfiguration(initialValue: 0), + ), + ); + + final state = key.currentState! as AnimatedWidgetBaseState; + + // Pre-animation: + expect(state.animation.value, 0.0); + expect(state.animation.status, AnimationStatus.forward); + expect( + _getCurrentLineChartLeaf(renderKey).data, + data.copyWith( + lineBarsData: [ + LineChartBarData( + spots: const [FlSpot.zero, FlSpot(1, 0), FlSpot(2, 0)], + ), + ], + ), + ); + + await tester.pumpAndSettle(); + + // Post-animation: + expect(state.animation.value, 1.0); + expect(state.animation.status, AnimationStatus.completed); + expect(_getCurrentLineChartLeaf(renderKey).data, data); + }); + + testWidgets( + "Doesn't provide modified initial data when animation is disabled", + (tester) async { + final key = GlobalKey(); + final renderKey = GlobalKey(); + + await tester.pumpWidget( + _LineChartScaffold( + chartKey: key, + renderKey: renderKey, + data: data, + initialAnimationConfiguration: + const InitialAnimationConfiguration(enabled: false), + ), + ); + + final state = key.currentState! as AnimatedWidgetBaseState; + + // Pre-animation: + expect(state.animation.value, 0.0); + expect(state.animation.status, AnimationStatus.dismissed); + expect(_getCurrentLineChartLeaf(renderKey).data, data); + + await tester.pumpAndSettle(); + + // Post-animation: + expect(state.animation.value, 0.0); + expect(state.animation.status, AnimationStatus.dismissed); + expect(_getCurrentLineChartLeaf(renderKey).data, data); + }); + }); +} + +const viewSize = Size(400, 400); + +LineChartLeaf _getCurrentLineChartLeaf(GlobalKey key) => + key.currentContext!.widget as LineChartLeaf; + +class _LineChartScaffold extends StatelessWidget { + const _LineChartScaffold({ + required this.chartKey, + required this.renderKey, + required this.data, + this.initialAnimationConfiguration = const InitialAnimationConfiguration(), + }); + + final Key chartKey; + final Key renderKey; + final LineChartData data; + final InitialAnimationConfiguration initialAnimationConfiguration; + + @override + Widget build(BuildContext context) => MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: LineChart( + chartRendererKey: renderKey, + key: chartKey, + initialAnimationConfiguration: initialAnimationConfiguration, + data, + ), + ), + ), + ), + ); +} diff --git a/test/chart/radar_chart/radar_chart_data_test.dart b/test/chart/radar_chart/radar_chart_data_test.dart index a45e22e80d..9f7c638c95 100644 --- a/test/chart/radar_chart/radar_chart_data_test.dart +++ b/test/chart/radar_chart/radar_chart_data_test.dart @@ -137,10 +137,7 @@ void main() { ); expect( - radarChartData1 == - radarChartData1Clone.copyWith( - titleTextStyle: radarChartData2.titleTextStyle, - ), + radarChartData1 == radarChartData1Clone.copyWith(scaleFactor: 0.5), false, ); }); diff --git a/test/chart/radar_chart/radar_chart_painter_test.dart b/test/chart/radar_chart/radar_chart_painter_test.dart index d4c2329872..febd14e2db 100644 --- a/test/chart/radar_chart/radar_chart_painter_test.dart +++ b/test/chart/radar_chart/radar_chart_painter_test.dart @@ -1125,6 +1125,38 @@ void main() { ], ); }); + + test('can be factor scaled', () { + const viewSize = Size(400, 300); + + final data = RadarChartData( + scaleFactor: 0.5, + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 1), + const RadarEntry(value: 2), + const RadarEntry(value: 3), + ], + ), + ], + ); + + final radarChartPainter = RadarChartPainter(); + final holder = PaintHolder(data, data, 1); + + final result = + radarChartPainter.calculateDataSetsPosition(viewSize, holder); + expect(result.length, 1); + expect( + result[0].entriesOffset, + [ + const Offset(200, 120), + const Offset(238.97114317029974, 172.5), + const Offset(148.03847577293368, 180.00000000000003), + ], + ); + }); }); group('getDefaultChartCenterValue()', () { diff --git a/test/chart/radar_chart/radar_chart_test.dart b/test/chart/radar_chart/radar_chart_test.dart new file mode 100644 index 0000000000..6ce48c5843 --- /dev/null +++ b/test/chart/radar_chart/radar_chart_test.dart @@ -0,0 +1,116 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/radar_chart/radar_chart_renderer.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + final data = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: const [ + RadarEntry(value: 1), + RadarEntry(value: 2), + RadarEntry(value: 3), + ], + ), + ], + ); + + group('RadarChart', () { + testWidgets( + 'Provides modified initial data when animation is enabled (by default)', + (tester) async { + final key = GlobalKey(); + final renderKey = GlobalKey(); + + await tester.pumpWidget( + _RadarChartScaffold(chartKey: key, renderKey: renderKey, data: data), + ); + + final state = key.currentState! as AnimatedWidgetBaseState; + + // Pre-animation: + expect(state.animation.value, 0.0); + expect(state.animation.status, AnimationStatus.forward); + expect( + _getCurrentRadarChartLeaf(renderKey).data, + data.copyWith(scaleFactor: 0), + ); + + await tester.pumpAndSettle(); + + // Post-animation: + expect(state.animation.value, 1.0); + expect(state.animation.status, AnimationStatus.completed); + expect(_getCurrentRadarChartLeaf(renderKey).data, data); + }); + + testWidgets( + "Doesn't provide modified initial data when animation is disabled", + (tester) async { + final key = GlobalKey(); + final renderKey = GlobalKey(); + + await tester.pumpWidget( + _RadarChartScaffold( + chartKey: key, + renderKey: renderKey, + data: data, + initialAnimationConfiguration: + const InitialAnimationConfiguration(enabled: false), + ), + ); + + final state = key.currentState! as AnimatedWidgetBaseState; + + // Pre-animation: + expect(state.animation.value, 0.0); + expect(state.animation.status, AnimationStatus.dismissed); + expect(_getCurrentRadarChartLeaf(renderKey).data, data); + + await tester.pumpAndSettle(); + + // Post-animation: + expect(state.animation.value, 0.0); + expect(state.animation.status, AnimationStatus.dismissed); + expect(_getCurrentRadarChartLeaf(renderKey).data, data); + }); + }); +} + +const viewSize = Size(400, 400); + +RadarChartLeaf _getCurrentRadarChartLeaf(GlobalKey key) => + key.currentContext!.widget as RadarChartLeaf; + +class _RadarChartScaffold extends StatelessWidget { + const _RadarChartScaffold({ + required this.chartKey, + required this.renderKey, + required this.data, + this.initialAnimationConfiguration = const InitialAnimationConfiguration(), + }); + + final Key chartKey; + final Key renderKey; + final RadarChartData data; + final InitialAnimationConfiguration initialAnimationConfiguration; + + @override + Widget build(BuildContext context) => MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: RadarChart( + chartRendererKey: renderKey, + key: chartKey, + initialAnimationConfiguration: initialAnimationConfiguration, + data, + ), + ), + ), + ), + ); +}