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,
|
|
|
|
}
|
|
|
|
|
2018-05-09 18:24:40 +00:00
|
|
|
/// 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,
|
2018-05-09 18:24:40 +00:00
|
|
|
|
|
|
|
/// Segments begin and end with a semi-circle.
|
2018-05-09 20:15:34 +00:00
|
|
|
round,
|
2018-05-09 18:24:40 +00:00
|
|
|
}
|
|
|
|
|
2017-11-02 23:48:58 +00:00
|
|
|
class AnimatedCircularChart extends StatefulWidget {
|
|
|
|
AnimatedCircularChart({
|
2024-07-02 06:13:07 +00:00
|
|
|
Key? key,
|
|
|
|
required this.size,
|
|
|
|
required this.initialChartData,
|
2017-11-02 23:48:58 +00:00
|
|
|
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,
|
2018-05-09 18:24:40 +00:00
|
|
|
}) : 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.
|
2024-07-02 06:13:07 +00:00
|
|
|
final List<CircularStackEntry>? initialChartData;
|
2017-11-02 23:48:58 +00:00
|
|
|
|
|
|
|
/// 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.
|
2024-07-02 06:13:07 +00:00
|
|
|
final double? holeRadius;
|
2017-11-02 23:48:58 +00:00
|
|
|
|
|
|
|
/// 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.
|
2024-07-02 06:13:07 +00:00
|
|
|
final String? holeLabel;
|
2018-03-21 00:36:07 +00:00
|
|
|
|
|
|
|
/// The style used when rendering the [holeLabel].
|
|
|
|
///
|
|
|
|
/// Defaults to the active [ThemeData]'s
|
|
|
|
/// [ThemeData.textTheme.body2] text style.
|
2024-07-02 06:13:07 +00:00
|
|
|
final TextStyle? labelStyle;
|
2018-03-21 00:36:07 +00:00
|
|
|
|
2018-05-09 18:24:40 +00:00
|
|
|
/// The type of segment edges to be drawn.
|
|
|
|
///
|
2018-05-09 20:15:34 +00:00
|
|
|
/// Defaults to [SegmentEdgeStyle.flat].
|
2018-05-09 18:24:40 +00:00
|
|
|
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);
|
|
|
|
/// ```
|
2024-07-02 06:13:07 +00:00
|
|
|
static AnimatedCircularChartState? of(BuildContext context, {bool nullOk: false}) {
|
2017-11-02 23:48:58 +00:00
|
|
|
assert(context != null);
|
|
|
|
assert(nullOk != null);
|
|
|
|
|
2024-07-02 06:13:07 +00:00
|
|
|
final AnimatedCircularChartState? result =
|
2021-03-08 17:37:23 +00:00
|
|
|
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 {
|
2024-07-02 06:13:07 +00:00
|
|
|
late CircularChartTween _tween;
|
|
|
|
late 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,
|
|
|
|
);
|
|
|
|
|
2024-07-02 06:13:07 +00:00
|
|
|
_assignRanks(widget.initialChartData!);
|
2017-11-02 23:48:58 +00:00
|
|
|
|
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,
|
2024-07-02 06:13:07 +00:00
|
|
|
data: widget.initialChartData!,
|
2017-11-02 23:48:58 +00:00
|
|
|
chartType: widget.chartType,
|
|
|
|
stackRanks: _stackRanks,
|
|
|
|
entryRanks: _entryRanks,
|
|
|
|
percentageValues: widget.percentageValues,
|
|
|
|
holeRadius: widget.holeRadius,
|
|
|
|
startAngle: widget.startAngle,
|
2018-05-09 18:24:40 +00:00
|
|
|
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) {
|
2024-07-02 06:13:07 +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,
|
2018-05-09 18:24:40 +00:00
|
|
|
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
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|