Reputation: 141
Is it possible to have this same effect using the package charts_flutter? In this case the user can rotate the pie chart.
Upvotes: 3
Views: 1574
Reputation: 40443
It's not possible with the current implementation of the charting library you're using unless you used their code and changed. You might be able to get it to work with the flutter circular chart plugin by hooking up your gesture detecting code and animating the value for startAngle
, but I'm not sure it would do exactly what you want (or might try to redraw the entire thing each time which isn't overly performant).
I had some old code lying around that implemented most of what you want so I fixed it up a little - here's an example of just writing your own pie chart. You can copy/paste it into a file and run it as-is.
Your mileage may vary with this - I haven't tested it extensively or anything, but you're welcome to use it at least as a starting point - it has the code for drawing a pie chart and rotating according to gestures at least.
There's quite a lot of stuff in here so I'd encourage you to give it a deep read-through to see exactly what I'm doing. I don't have time to add documentation right now, but if you have any questions feel free to ask.
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: SafeArea(
child: Material(
child: RotatingPieChart(
items: [
PieChartItem(30, "one", Colors.red),
PieChartItem(210, "two", Colors.green),
PieChartItem(60, "three", Colors.blue),
PieChartItem(35, "four", Colors.teal),
PieChartItem(25, "five", Colors.orange)
],
toText: (item, _) => TextPainter(
textAlign: TextAlign.center,
text: TextSpan(
style: TextStyle(color: Colors.black, fontSize: 8.0),
text: "${item.name}\n${item.val}",
),
textDirection: TextDirection.ltr),
),
),
),
);
}
}
class PieChartItem {
final num val;
final String name;
final Color color;
PieChartItem(this.val, this.name, this.color) : assert(val != 0);
}
typedef TextPainter PieChartItemToText(PieChartItem item, double total);
class RotatingPieChart extends StatelessWidget {
final double accellerationFactor;
final List<PieChartItem> items;
final PieChartItemToText toText;
const RotatingPieChart({Key key, this.accellerationFactor = 1.0, @required this.items, @required this.toText})
: super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: AspectRatio(
aspectRatio: 1.0,
child: _RotatingPieChartInternal(
items: items,
toText: toText,
accellerationFactor: accellerationFactor,
),
),
);
}
}
class _RotationEndSimulation extends Simulation {
final double initialVelocity;
final double initialPosition;
final double accelleration;
_RotationEndSimulation({
@required this.initialVelocity,
@required double decelleration,
@required this.initialPosition,
}) : accelleration = decelleration * -1.0;
@override
double dx(double time) => initialVelocity + (accelleration * time);
@override
bool isDone(double time) => initialVelocity > 0 ? dx(time) < 0.001 : dx(time) > -0.001;
@override
double x(double time) => (initialPosition + (initialVelocity * time) + (accelleration * time * time / 2)) % 1.0;
}
class _RotatingPieChartInternal extends StatefulWidget {
final double accellerationFactor;
final List<PieChartItem> items;
final PieChartItemToText toText;
const _RotatingPieChartInternal(
{Key key, this.accellerationFactor = 1.0, @required this.items, @required this.toText})
: super(key: key);
@override
_RotatingPieChartInternalState createState() => _RotatingPieChartInternalState();
}
class _RotatingPieChartInternalState extends State<_RotatingPieChartInternal> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _animation;
@override
void initState() {
_controller = AnimationController(vsync: this);
_animation = new Tween(begin: 0.0, end: 2.0 * pi).animate(_controller);
_controller.animateTo(2 * pi, duration: Duration(seconds: 10));
super.initState();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Offset lastDirection;
Offset getDirection(Offset globalPosition) {
RenderBox box = context.findRenderObject();
Offset offset = box.globalToLocal(globalPosition);
Offset center = Offset(context.size.width / 2.0, context.size.height / 2.0);
return offset - center;
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanDown: (details) {
lastDirection = getDirection(details.globalPosition);
},
onPanUpdate: (details) {
Offset newDirection = getDirection(details.globalPosition);
double diff = newDirection.direction - lastDirection.direction;
var value = _controller.value + (diff / pi / 2);
_controller.value = value % 1.0;
lastDirection = newDirection;
},
onPanEnd: (details) {
// non-angular velocity
Offset velocity = details.velocity.pixelsPerSecond;
var top = (lastDirection.dx * velocity.dy) - (lastDirection.dy * velocity.dx);
var bottom = (lastDirection.dx * lastDirection.dx) + (lastDirection.dy * lastDirection.dy);
var angularVelocity = top / bottom;
var angularRotation = angularVelocity / pi / 2;
var decelleration = angularRotation * widget.accellerationFactor;
_controller.animateWith(
_RotationEndSimulation(
decelleration: decelleration,
initialPosition: _controller.value,
initialVelocity: angularRotation,
),
);
},
child: AnimatedBuilder(
animation: _animation,
builder: (context, widget) {
return Stack(
fit: StackFit.passthrough,
children: [
Transform.rotate(
angle: _animation.value,
child: widget,
),
CustomPaint(
painter:
_PieTextPainter(items: this.widget.items, rotation: _animation.value, toText: this.widget.toText),
)
],
);
},
child: CustomPaint(
painter: _PieChartPainter(
items: widget.items,
),
),
),
);
}
}
abstract class _AlignedCustomPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// for convenience I'm doing all the drawing in a 100x100 square then moving it rather than worrying
// about the actual size.
// Also, using a 100x100 square for convenience so we can hardcode values.
FittedSizes fittedSizes = applyBoxFit(BoxFit.contain, Size(100.0, 100.0), size);
var dest = fittedSizes.destination;
canvas.translate((size.width - dest.width) / 2 + 1, (size.height - dest.height) / 2 + 1);
canvas.scale((dest.width - 2) / 100.0);
alignedPaint(canvas, Size(100.0, 100.0));
}
void alignedPaint(Canvas canvas, Size size);
}
class _PieChartPainter extends _AlignedCustomPainter {
final List<PieChartItem> items;
final double total;
final double rotation;
_PieChartPainter({this.rotation = 0.0, @required this.items})
: total = items.fold(0.0, (total, el) => total + el.val);
@override
void alignedPaint(Canvas canvas, Size size) {
Rect rect = Offset.zero & size;
double soFar = rotation;
Paint outlinePaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke;
for (int i = 0; i < items.length; ++i) {
PieChartItem item = items[i];
double arcRad = item.val / total * 2 * pi;
canvas.drawArc(rect, soFar, arcRad, true, Paint()..color = item.color);
canvas.drawArc(rect, soFar, arcRad, true, outlinePaint);
soFar += arcRad;
}
}
@override
bool shouldRepaint(_PieChartPainter oldDelegate) {
return oldDelegate.rotation != rotation || oldDelegate.items != items;
}
}
class _PieTextPainter extends _AlignedCustomPainter {
final List<PieChartItem> items;
final double total;
final double rotation;
final List<double> middles;
final PieChartItemToText toText;
static final double textDisplayCenter = 0.7;
_PieTextPainter._(this.items, this.total, this.rotation, this.middles, this.toText);
factory _PieTextPainter(
{double rotation = 0.0, @required List<PieChartItem> items, @required PieChartItemToText toText}) {
double total = items.fold(0.0, (prev, el) => prev + el.val);
var middles = (() {
double soFar = rotation;
return items.map((item) {
double arcRad = item.val / total * 2 * pi;
double middleRad = (soFar) + (arcRad / 2);
soFar += arcRad;
return middleRad;
}).toList(growable: false);
})();
return _PieTextPainter._(items, total, rotation, middles, toText);
}
@override
void alignedPaint(Canvas canvas, Size size) {
for (int i = 0; i < items.length; ++i) {
var middleRad = middles[i];
var item = items[i];
var rad = size.width / 2;
var middleX = rad + rad * textDisplayCenter * cos(middleRad);
var middleY = rad + rad * textDisplayCenter * sin(middleRad);
TextPainter textPainter = toText(item, total)..layout();
textPainter.paint(canvas, Offset(middleX - (textPainter.width / 2), middleY - (textPainter.height / 2)));
}
}
@override
bool shouldRepaint(_PieTextPainter oldDelegate) {
// note that just checking items != items might not be enough.
return oldDelegate.rotation != rotation || oldDelegate.items != items || oldDelegate.toText != toText;
}
}
Upvotes: 2