202 lines
6.8 KiB
Dart
202 lines
6.8 KiB
Dart
|
import 'package:flutter/foundation.dart';
|
||
|
import 'package:flutter/material.dart';
|
||
|
import 'package:flutter_circular_chart/src/circular_chart.dart';
|
||
|
import 'package:flutter_circular_chart/src/entry.dart';
|
||
|
import 'package:flutter_circular_chart/src/painter.dart';
|
||
|
|
||
|
// 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,
|
||
|
}
|
||
|
|
||
|
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,
|
||
|
})
|
||
|
: assert(size != null),
|
||
|
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;
|
||
|
|
||
|
/// 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);
|
||
|
/// ```
|
||
|
static AnimatedCircularChartState of(BuildContext context,
|
||
|
{bool nullOk: false}) {
|
||
|
assert(context != null);
|
||
|
assert(nullOk != null);
|
||
|
|
||
|
final AnimatedCircularChartState result = context
|
||
|
.ancestorStateOfType(const TypeMatcher<AnimatedCircularChartState>());
|
||
|
|
||
|
if (nullOk || result != null) return result;
|
||
|
|
||
|
throw new FlutterError(
|
||
|
'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
|
||
|
AnimatedCircularChartState createState() => new AnimatedCircularChartState();
|
||
|
}
|
||
|
|
||
|
/// 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
|
||
|
/// GlobalKey<AnimatedCircularChartState> chartKey = new GlobalKey<AnimatedCircularChartState>();
|
||
|
/// ...
|
||
|
/// new AnimatedCircularChart(key: chartKey, ...);
|
||
|
/// ...
|
||
|
/// 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>{};
|
||
|
|
||
|
@override
|
||
|
void initState() {
|
||
|
super.initState();
|
||
|
_animation = new AnimationController(
|
||
|
duration: widget.duration,
|
||
|
vsync: this,
|
||
|
);
|
||
|
|
||
|
_assignRanks(widget.initialChartData);
|
||
|
|
||
|
_tween = new CircularChartTween(
|
||
|
new CircularChart.empty(chartType: widget.chartType),
|
||
|
new CircularChart.fromData(
|
||
|
size: widget.size,
|
||
|
data: widget.initialChartData,
|
||
|
chartType: widget.chartType,
|
||
|
stackRanks: _stackRanks,
|
||
|
entryRanks: _entryRanks,
|
||
|
percentageValues: widget.percentageValues,
|
||
|
holeRadius: widget.holeRadius,
|
||
|
startAngle: widget.startAngle,
|
||
|
),
|
||
|
);
|
||
|
_animation.forward();
|
||
|
}
|
||
|
|
||
|
@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);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// 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(() {
|
||
|
_tween = new CircularChartTween(
|
||
|
_tween.evaluate(_animation),
|
||
|
new CircularChart.fromData(
|
||
|
size: widget.size,
|
||
|
data: data,
|
||
|
chartType: widget.chartType,
|
||
|
stackRanks: _stackRanks,
|
||
|
entryRanks: _entryRanks,
|
||
|
percentageValues: widget.percentageValues,
|
||
|
holeRadius: widget.holeRadius,
|
||
|
startAngle: widget.startAngle,
|
||
|
),
|
||
|
);
|
||
|
_animation.forward(from: 0.0);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
return new CustomPaint(
|
||
|
size: widget.size,
|
||
|
painter: new AnimatedCircularChartPainter(_tween.animate(_animation)),
|
||
|
);
|
||
|
}
|
||
|
}
|