Skip to content

Commit

Permalink
Added initial animation for LineChart / BarChart / RadarChart
Browse files Browse the repository at this point in the history
  • Loading branch information
Dartek12 committed Oct 1, 2023
1 parent 04b18d2 commit 2278ab3
Show file tree
Hide file tree
Showing 17 changed files with 623 additions and 34 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ class _LineChartSample10State extends State<LineChartSample10> {
show: false,
),
),
initialAnimationConfiguration:
const InitialAnimationConfiguration(enabled: false),
),
),
)
Expand Down
12 changes: 3 additions & 9 deletions example/lib/presentation/samples/line/line_chart_sample8.dart
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -22,12 +20,8 @@ class _LineChartSample8State extends State<LineChartSample8> {

bool showAvg = false;

Future<ui.Image> 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<SizedPicture> _loadSvgFuture = loadSvg();

Future<SizedPicture> loadSvg() async {
const rawSvg =
Expand All @@ -43,7 +37,7 @@ class _LineChartSample8State extends State<LineChartSample8> {
@override
Widget build(BuildContext context) {
return FutureBuilder<SizedPicture>(
future: loadSvg(),
future: _loadSvgFuture,
builder: (BuildContext context, imageSnapshot) {
if (imageSnapshot.connectionState == ConnectionState.done) {
return Stack(
Expand Down
11 changes: 7 additions & 4 deletions example/lib/presentation/samples/radar/radar_chart_sample1.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,13 @@ class _RadarChartSample1State extends State<RadarChartSample1> {
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,
Expand Down
1 change: 1 addition & 0 deletions lib/fl_chart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
57 changes: 53 additions & 4 deletions lib/src/chart/bar_chart/bar_chart.dart
Original file line number Diff line number Diff line change
@@ -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].
Expand All @@ -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,
Expand All @@ -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<BarChart> {
class _BarChartState extends AnimatedWidgetBaseState<BarChart>
with InitialAnimationMixin<BarChartData, BarChart> {
/// we handle under the hood animations (implicit animations) via this tween,
/// it lerps between the old [BarChartData] to the new one.
BarChartDataTween? _barChartDataTween;
Expand All @@ -44,6 +50,13 @@ class _BarChartState extends AnimatedWidgetBaseState<BarChart> {

final Map<int, List<int>> _showingTouchedTooltips = {};

@override
InitialAnimationConfiguration get initialAnimationConfiguration =>
widget.initialAnimationConfiguration;

@override
Tween? get tween => _barChartDataTween;

@override
Widget build(BuildContext context) {
final showingData = _getData();
Expand Down Expand Up @@ -117,13 +130,49 @@ class _BarChartState extends AnimatedWidgetBaseState<BarChart> {
});
}

@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<BarChartRodData> _getAppearanceBarRods(
double startingY,
List<BarChartRodData> barRods,
) {
final rods = <BarChartRodData>[];
BarChartRodData? previousRod;
var offset = 0.0;

for (final rod in barRods) {
if (previousRod != null) {
offset += rod.fromY - previousRod.toY;

Check warning on line 155 in lib/src/chart/bar_chart/bar_chart.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/chart/bar_chart/bar_chart.dart#L155

Added line #L155 was not covered by tests
}
final y = startingY + offset;
rods.add(rod.copyWith(fromY: y, toY: y));
previousRod = rod;
}
return rods;
}

@override
void forEachTween(TweenVisitor<dynamic> 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?;
}
}
18 changes: 18 additions & 0 deletions lib/src/chart/base/base_chart/initial_animation_configuration.dart
Original file line number Diff line number Diff line change
@@ -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<Object?> get props => [enabled, initialValue];

Check warning on line 17 in lib/src/chart/base/base_chart/initial_animation_configuration.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/chart/base/base_chart/initial_animation_configuration.dart#L16-L17

Added lines #L16 - L17 were not covered by tests
}
55 changes: 55 additions & 0 deletions lib/src/chart/base/base_chart/initial_animation_mixin.dart
Original file line number Diff line number Diff line change
@@ -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<T, D extends ImplicitlyAnimatedWidget>
on AnimatedWidgetBaseState<D> {
InitialAnimationConfiguration get initialAnimationConfiguration;
Tween<dynamic>? 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;

Check warning on line 53 in lib/src/chart/base/base_chart/initial_animation_mixin.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/chart/base/base_chart/initial_animation_mixin.dart#L53

Added line #L53 was not covered by tests
}
}
41 changes: 38 additions & 3 deletions lib/src/chart/line_chart/line_chart.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand All @@ -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<LineChart> {
class _LineChartState extends AnimatedWidgetBaseState<LineChart>
with InitialAnimationMixin<LineChartData, LineChart> {
/// we handle under the hood animations (implicit animations) via this tween,
/// it lerps between the old [LineChartData] to the new one.
LineChartDataTween? _lineChartDataTween;
Expand All @@ -44,6 +50,13 @@ class _LineChartState extends AnimatedWidgetBaseState<LineChart> {

final Map<int, List<int>> _showingTouchedIndicators = {};

@override
InitialAnimationConfiguration get initialAnimationConfiguration =>
widget.initialAnimationConfiguration;

@override
Tween? get tween => _lineChartDataTween;

@override
Widget build(BuildContext context) {
final showingData = _getData();
Expand Down Expand Up @@ -123,13 +136,35 @@ class _LineChartState extends AnimatedWidgetBaseState<LineChart> {
});
}

@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<dynamic> 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?;
}
}
Loading

0 comments on commit 2278ab3

Please sign in to comment.