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 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()); 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 chartKey = new GlobalKey(); /// ... /// new AnimatedCircularChart(key: chartKey, ...); /// ... /// chartKey.currentState.updateData(newData); /// ``` class AnimatedCircularChartState extends State with TickerProviderStateMixin { CircularChartTween _tween; AnimationController _animation; final Map _stackRanks = {}; final Map _entryRanks = {}; @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 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 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)), ); } }