flutter_circular_chart/lib/src/animated_circular_chart.dart

264 lines
8.6 KiB
Dart
Raw Normal View History

2017-11-02 23:48:58 +00:00
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
2021-03-08 17:37:23 +00:00
import 'package:flutter_circular_chart_two/src/circular_chart.dart';
import 'package:flutter_circular_chart_two/src/entry.dart';
import 'package:flutter_circular_chart_two/src/painter.dart';
2017-11-02 23:48:58 +00:00
// The default chart tween animation duration.
const Duration _kDuration = const Duration(milliseconds: 300);
// The default angle the chart is oriented at.
const double _kStartAngle = -90.0;
enum CircularChartType {
Pie,
Radial,
}
/// Determines how the ends of a chart's segments should be drawn.
enum SegmentEdgeStyle {
/// Segments begin and end with a flat edge.
2018-05-09 20:15:34 +00:00
flat,
/// Segments begin and end with a semi-circle.
2018-05-09 20:15:34 +00:00
round,
}
2017-11-02 23:48:58 +00:00
class AnimatedCircularChart extends StatefulWidget {
AnimatedCircularChart({
Key key,
@required this.size,
@required this.initialChartData,
this.chartType = CircularChartType.Radial,
this.duration = _kDuration,
this.percentageValues = false,
this.holeRadius,
this.startAngle = _kStartAngle,
2018-03-21 00:36:07 +00:00
this.holeLabel,
this.labelStyle,
2018-05-09 20:15:34 +00:00
this.edgeStyle = SegmentEdgeStyle.flat,
}) : assert(size != null),
2017-11-02 23:48:58 +00:00
super(key: key);
/// The size of the bounding box this chart will be constrained to.
final Size size;
/// The data used to build the chart displayed when the widget is first placed.
/// Each [CircularStackEntry] in the list defines an individual stack of data:
/// For a Pie chart that corresponds to individual slices in the chart.
/// For a Radial chart it corresponds to individual segments on the same arc.
///
/// If length > 1 and [chartType] is [CircularChartType.Radial] then the stacks
/// will be grouped together as concentric circles.
///
/// If [chartType] is [CircularChartType.Pie] then length cannot be > 1.
final List<CircularStackEntry> initialChartData;
/// The type of chart to be rendered.
/// Use [CircularChartType.Pie] for a circle divided into slices for each entry.
/// Use [CircularChartType.Radial] for one or more arcs with a hole in the center.
final CircularChartType chartType;
/// The duration of the chart animation when [AnimatedCircularChartState.updateData]
/// is called.
final Duration duration;
/// If true then the data values provided will determine what percentage of the circle
/// this segment occupies [i.e: a value of 100 is the full circle].
///
/// Otherwise the data is normalized such that the sum of all values in each stack
/// is considered to encompass 100% of the circle.
///
/// defaults to false.
final bool percentageValues;
/// For [CircularChartType.Radial] charts this defines the circle in the center
/// of the canvas, around which the chart is drawn. If not provided then it will
/// be automatically calculated to accommodate all the data.
///
/// Has no effect in [CircularChartType.Pie] charts.
final double holeRadius;
/// The chart gets drawn and animates clockwise from [startAngle], defaulting to the
/// top/center point or -90.0. In terms of a clock face these would be:
/// - -90.0: 12 o'clock
/// - 0.0: 3 o'clock
/// - 90.0: 6 o'clock
/// - 180.0: 9 o'clock
final double startAngle;
2018-03-21 00:36:07 +00:00
/// A label to show in the hole of a radial chart.
///
/// It is used to display the value of a radial slider, and it is displayed
/// in the center of the chart's hole.
///
/// See also [labelStyle] which is used to render the label.
final String holeLabel;
/// The style used when rendering the [holeLabel].
///
/// Defaults to the active [ThemeData]'s
/// [ThemeData.textTheme.body2] text style.
final TextStyle labelStyle;
/// The type of segment edges to be drawn.
///
2018-05-09 20:15:34 +00:00
/// Defaults to [SegmentEdgeStyle.flat].
final SegmentEdgeStyle edgeStyle;
2017-11-02 23:48:58 +00:00
/// The state from the closest instance of this class that encloses the given context.
///
/// This method is typically used by [AnimatedCircularChart] item widgets that insert or
/// remove items in response to user input.
///
/// ```dart
/// AnimatedCircularChartState animatedCircularChart = AnimatedCircularChart.of(context);
/// ```
2021-03-08 17:37:23 +00:00
static AnimatedCircularChartState of(BuildContext context, {bool nullOk: false}) {
2017-11-02 23:48:58 +00:00
assert(context != null);
assert(nullOk != null);
2021-03-08 17:37:23 +00:00
final AnimatedCircularChartState result =
context.findAncestorStateOfType<AnimatedCircularChartState>();
2017-11-02 23:48:58 +00:00
if (nullOk || result != null) return result;
2021-03-08 17:37:23 +00:00
throw FlutterError(
2017-11-02 23:48:58 +00:00
'AnimatedCircularChart.of() called with a context that does not contain a AnimatedCircularChart.\n'
'No AnimatedCircularChart ancestor could be found starting from the context that was passed to AnimatedCircularChart.of(). '
'This can happen when the context provided is from the same StatefulWidget that '
'built the AnimatedCircularChart.\n'
'The context used was:\n'
' $context');
}
@override
2021-03-08 17:37:23 +00:00
AnimatedCircularChartState createState() => AnimatedCircularChartState();
2017-11-02 23:48:58 +00:00
}
/// The state for a circular chart that animates when its data is updated.
///
/// When the chart data changes with [updateData] an animation begins running.
///
/// An app that needs to update its data in response to an event
/// can refer to the [AnimatedCircularChart]'s state with a global key:
///
/// ```dart
2021-03-08 17:37:23 +00:00
/// GlobalKey<AnimatedCircularChartState> chartKey = GlobalKey<AnimatedCircularChartState>();
2017-11-02 23:48:58 +00:00
/// ...
2021-03-08 17:37:23 +00:00
/// AnimatedCircularChart(key: chartKey, ...);
2017-11-02 23:48:58 +00:00
/// ...
/// chartKey.currentState.updateData(newData);
/// ```
class AnimatedCircularChartState extends State<AnimatedCircularChart>
with TickerProviderStateMixin {
CircularChartTween _tween;
AnimationController _animation;
final Map<String, int> _stackRanks = <String, int>{};
final Map<String, int> _entryRanks = <String, int>{};
2021-03-08 17:37:23 +00:00
final TextPainter _labelPainter = TextPainter();
2017-11-02 23:48:58 +00:00
@override
void initState() {
super.initState();
2021-03-08 17:37:23 +00:00
_animation = AnimationController(
2017-11-02 23:48:58 +00:00
duration: widget.duration,
vsync: this,
);
_assignRanks(widget.initialChartData);
2021-03-08 17:37:23 +00:00
_tween = CircularChartTween(
CircularChart.empty(chartType: widget.chartType),
CircularChart.fromData(
2017-11-02 23:48:58 +00:00
size: widget.size,
data: widget.initialChartData,
chartType: widget.chartType,
stackRanks: _stackRanks,
entryRanks: _entryRanks,
percentageValues: widget.percentageValues,
holeRadius: widget.holeRadius,
startAngle: widget.startAngle,
edgeStyle: widget.edgeStyle,
2017-11-02 23:48:58 +00:00
),
);
_animation.forward();
}
2018-03-21 00:36:07 +00:00
@override
void didUpdateWidget(AnimatedCircularChart oldWidget) {
super.didUpdateWidget(oldWidget);
2021-03-08 17:37:23 +00:00
if (oldWidget.holeLabel != widget.holeLabel || oldWidget.labelStyle != widget.labelStyle) {
2018-03-21 00:36:07 +00:00
_updateLabelPainter();
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateLabelPainter();
}
2017-11-02 23:48:58 +00:00
@override
void dispose() {
_animation.dispose();
super.dispose();
}
void _assignRanks(List<CircularStackEntry> data) {
for (CircularStackEntry stackEntry in data) {
_stackRanks.putIfAbsent(stackEntry.rankKey, () => _stackRanks.length);
for (CircularSegmentEntry entry in stackEntry.entries) {
_entryRanks.putIfAbsent(entry.rankKey, () => _entryRanks.length);
}
}
}
2018-03-21 00:36:07 +00:00
void _updateLabelPainter() {
if (widget.holeLabel != null) {
2021-03-08 17:37:23 +00:00
TextStyle _labelStyle = widget.labelStyle ?? Theme.of(context).textTheme.bodyText1;
2018-03-21 00:36:07 +00:00
_labelPainter
2021-03-08 17:37:23 +00:00
..text = TextSpan(style: _labelStyle, text: widget.holeLabel)
2018-03-21 00:36:07 +00:00
..textDirection = Directionality.of(context)
..textScaleFactor = MediaQuery.of(context).textScaleFactor
..layout();
} else {
_labelPainter.text = null;
}
}
2017-11-02 23:48:58 +00:00
/// Update the data this chart represents and start an animation that will tween
/// between the old data and this one.
void updateData(List<CircularStackEntry> data) {
_assignRanks(data);
setState(() {
2021-03-08 17:37:23 +00:00
_tween = CircularChartTween(
2017-11-02 23:48:58 +00:00
_tween.evaluate(_animation),
2021-03-08 17:37:23 +00:00
CircularChart.fromData(
2017-11-02 23:48:58 +00:00
size: widget.size,
data: data,
chartType: widget.chartType,
stackRanks: _stackRanks,
entryRanks: _entryRanks,
percentageValues: widget.percentageValues,
holeRadius: widget.holeRadius,
startAngle: widget.startAngle,
edgeStyle: widget.edgeStyle,
2017-11-02 23:48:58 +00:00
),
);
_animation.forward(from: 0.0);
});
}
@override
Widget build(BuildContext context) {
2021-03-08 17:37:23 +00:00
return CustomPaint(
2017-11-02 23:48:58 +00:00
size: widget.size,
2021-03-08 17:37:23 +00:00
painter: AnimatedCircularChartPainter(
2018-03-21 00:36:07 +00:00
_tween.animate(_animation),
2018-03-21 01:45:17 +00:00
widget.holeLabel != null ? _labelPainter : null,
2018-03-21 00:36:07 +00:00
),
2017-11-02 23:48:58 +00:00
);
}
}