I am trying to build a pie chart that will look like this:
I've tried both Flutter_Charts and FL_Chart, but it seems none of them support a rounded corner and spaced items in the pie chart.
Does anyone know what is the best way to achieve this design as a pie chart?
Thank you!
A very similar version to your chart can easily be achieved with the CustomPaint widget.
Here is the resulting chart
To achieve this you will just need a very rudimentary CustomPainter that draws arcs across its canvas.
The rounding effect is achieved through the strokeCap attribute of the Paint that is used to draw the stroke. Sadly StrokeCap only supports
round and square stroke endings.
A rounded rectangle effect like the one in your screenshot cannot be achieved through this.
Colors are achieved by using a separate Paint for each stroke.
// this is used to pass data about chart values to the widget
class PieChartData {
const PieChartData(this.color, this.percent);
final Color color;
final double percent;
}
// our pie chart widget
class PieChart extends StatelessWidget {
PieChart({
required this.data,
required this.radius,
this.strokeWidth = 8,
this.child,
Key? key,
}) : // make sure sum of data is never ovr 100 percent
assert(data.fold<double>(0, (sum, data) => sum + data.percent) <= 100),
super(key: key);
final List<PieChartData> data;
// radius of chart
final double radius;
// width of stroke
final double strokeWidth;
// optional child; can be used for text for example
final Widget? child;
#override
Widget build(context) {
return CustomPaint(
painter: _Painter(strokeWidth, data),
size: Size.square(radius),
child: SizedBox.square(
// calc diameter
dimension: radius * 2,
child: Center(
child: child,
),
),
);
}
}
// responsible for painting our chart
class _PainterData {
const _PainterData(this.paint, this.radians);
final Paint paint;
final double radians;
}
class _Painter extends CustomPainter {
_Painter(double strokeWidth, List<PieChartData> data) {
// convert chart data to painter data
dataList = data
.map((e) => _PainterData(
Paint()
..color = e.color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round,
// remove padding from stroke
(e.percent - _padding) * _percentInRadians,
))
.toList();
}
static const _percentInRadians = 0.062831853071796;
// this is the gap between strokes in percent
static const _padding = 4;
static const _paddingInRadians = _percentInRadians * _padding;
// 0 radians is to the right, but since we want to start from the top
// we'll use -90 degrees in radians
static const _startAngle = -1.570796 + _paddingInRadians / 2;
late final List<_PainterData> dataList;
#override
void paint(Canvas canvas, Size size) {
final rect = Offset.zero & size;
// keep track of start angle for next stroke
double startAngle = _startAngle;
for (final data in dataList) {
final path = Path()..addArc(rect, startAngle, data.radians);
startAngle += data.radians + _paddingInRadians;
canvas.drawPath(path, data.paint);
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return oldDelegate != this;
}
}
You can check out the dartpad to experiment with a working example.
I am positive that the same chart you provided in that picture can be
achieved with a CustomPainter but that will be a lot more complex.
Related
I am trying to create a parallax effect for the background image of a container.
I followed this tutorial https://docs.flutter.dev/cookbook/effects/parallax-scrolling.
However I would like for the background image to remain still in the viewport ad the user scrolls.
Basically I would like to achieve this behavior https://www.w3schools.com/howto/tryhow_css_parallax_demo.htm.
I believe that in order to achieve this I have to modify the paintChildren method inside ParallaxFlowDelegate
#override
void paintChildren(FlowPaintingContext context) {
// Calculate the position of this list item within the viewport.
final scrollableBox = scrollable.context.findRenderObject() as RenderBox;
final listItemBox = listItemContext.findRenderObject() as RenderBox;
final listItemOffset = listItemBox.localToGlobal(
listItemBox.size.centerLeft(Offset.zero),
ancestor: scrollableBox);
// Determine the percent position of this list item within the
// scrollable area.
final viewportDimension = scrollable.position.viewportDimension;
final scrollFraction =
(listItemOffset.dy / viewportDimension).clamp(0.0, 1.0);
// Convert the background alignment into a pixel offset for
// painting purposes.
final backgroundSize =
(backgroundImageKey.currentContext!.findRenderObject() as RenderBox)
.size;
// Calculate the vertical alignment of the background
// based on the scroll percent.
final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1);
final listItemSize = context.size;
final childRect =
verticalAlignment.inscribe(backgroundSize, Offset.zero & listItemSize);
// Paint the background.
context.paintChild(
0,
transform:
Transform.translate(offset: Offset(0.0, childRect.top)).transform,
);
}
but I can't figure out how.
Any help would be greatly appreciated :)
I was just messing, of course I can figure it out.
For anyone interested simply follow the flutter dev tutorial https://docs.flutter.dev/cookbook/effects/parallax-scrolling but replace their ParallaxFlowDelegate class with the following:
import 'package:flutter/material.dart';
class ParallaxFlowDelegate extends FlowDelegate {
final ScrollableState scrollable;
final BuildContext listItemContext;
final GlobalKey backgroundImageKey;
ParallaxFlowDelegate({
required this.scrollable,
required this.listItemContext,
required this.backgroundImageKey,
}) : super(repaint: scrollable.position);
#override
BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) {
return BoxConstraints.expand(
width: constraints.maxWidth,
height: scrollable.position.viewportDimension
);
}
#override
void paintChildren(FlowPaintingContext context) {
// Calculate the position of this list item within the viewport.
final scrollableBox = scrollable.context.findRenderObject() as RenderBox;
final listItemBox = listItemContext.findRenderObject() as RenderBox;
final listItemSize = context.size;
//Gets the offset of the top of the list item from the top of the viewport
final listItemOffset = listItemBox.localToGlobal(
listItemBox.size.topCenter(Offset.zero),
ancestor: scrollableBox);
// Get the size of the background image
final backgroundSize =
(backgroundImageKey.currentContext!.findRenderObject() as RenderBox)
.size;
//Gets the vertical size of the viewport (excludes appbars)
final viewportDimension = scrollable.position.viewportDimension;
// Determine the percent position of this list item within the
// scrollable area.
// - scrollFraction is 1 if listItem's top edge is at the bottom of the
// viewport.
// - scrollFraction is 0 if listItem's top edge is at the top of the
// viewport
// - scrollFraction is -1 if listItem's bottom edge is at the top of the
// viewport
final double scrollFraction, yOffset;
if(listItemOffset.dy > 0){
scrollFraction = (listItemOffset.dy / viewportDimension).clamp(0, 1.0);
yOffset = -scrollFraction * backgroundSize.height;
}else{
scrollFraction = (listItemOffset.dy / listItemSize.height).clamp(-1, 0);
yOffset = -scrollFraction * listItemSize.height;
}
// Paint the background.
context.paintChild(
0,
transform:
Transform.translate(offset: Offset(0, yOffset)).transform,
);
}
#override
bool shouldRepaint(ParallaxFlowDelegate oldDelegate) {
return scrollable != oldDelegate.scrollable ||
listItemContext != oldDelegate.listItemContext ||
backgroundImageKey != oldDelegate.backgroundImageKey;
}
}
Works like a beauty for vertically scrolling pages :)
For some reason, my CustomPainter does not draw anything to the screen. I'm trying to build a Pie-Chart but the painter only works when I set the sweepAngle to 2*pi.
The Widget where CustomPaint is called has the following structure:
class PieChart extends StatelessWidget {
const PieChart({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return SizedBox(
height: 160,
width: 210,
child: CustomPaint(
painter: PieChartPainter(categories: dataset, width: 10),
),
);
}
}
This is my CustomPainter class:
class PieChartPainter extends CustomPainter {
PieChartPainter({
required this.categories,
required this.width,
});
final List<Category> categories;
final double width;
#override
void paint(Canvas canvas, Size size) {
Offset center = Offset(size.width / 2, size.height / 2);
double radius = min(size.width / 2, size.height / 2);
double total = 0;
// Calculate total amount from each category
for (var expense in categories) {
total += expense.amount;
}
// The angle/radian at 12 o'clock
double startRadian = -pi / 2;
for (var index = 0; index < categories.length; index++) {
final currentCategory = categories.elementAt(index);
final sweepRadian = currentCategory.amount / total * 2 * pi;
final paint = Paint()
..style = PaintingStyle.fill
..strokeWidth = width
..color = categoryColors.elementAt(index % categories.length);
final rect = Rect.fromCenter(
center: center, width: radius * 2, height: radius * 2);
canvas.drawArc(
rect,
startRadian,
2 * pi, // should really be "sweepRadian"
false,
paint,
);
startRadian += sweepRadian;
}
}
#override
bool shouldRepaint(PieChartPainter oldDelegate) {
return true;
}
}
I can almost certainly say that the problem has nothing to do with the data and colors that I provide, because whenever I log the current elements to the console, it gives the correct output.
Here, you can see my Widget structure:
And here is an image of the component itself: (Notice that only the last category-color is shown for the entire circle, because whenever I use a sweepAngle that is less than 2*pi, the entire widget doesn't show colors.)
This is the component when I set a sweepAngle that is less than 2*pi:
I really cannot figure out what might be causing this issue. Does anyone have an Idea what else is influenced by the sweepAngle parameter? I also have no idea why the colored circles to the left of the individual categories are not visible, because they're rendered in an entirely different Widget-branch...
If you have any idea on how to solve this, I would be more than happy to provide more information but as long as I don't know where to look, I don't want to spam this issue with unnecessary information.
For anyone wondering:
The problem had to do with the "--enable-software-rendering" argument. Once I ran flutter with flutter run --enable-software-rendering, it all worked as expected.
I'm trying to draw a line graph with some smooth curves, like this one from fl_chart :
I tried using quadraticBezierTo, but it didn't work :
This is my code, is there any other way?
import 'package:flutter/material.dart';
class DrawLines extends StatefulWidget {
final Coordinates lineCoordinates;
final double yHeight;
DrawLines({required this.lineCoordinates, required this.yHeight});
#override
_DrawLinesState createState() => _DrawLinesState();
}
class _DrawLinesState extends State<DrawLines> {
#override
Widget build(BuildContext context) {
return CustomPaint(
foregroundPainter: LinePainter(
coordinates: widget.lineCoordinates, yHeight: widget.yHeight),
);
}
}
class CoordValues {
late double x;
late double y;
CoordValues({required this.x, required this.y});
}
class Coordinates {
late CoordValues startCoords;
late CoordValues endCoords;
Coordinates({required this.startCoords, required this.endCoords});
}
class LinePainter extends CustomPainter {
late Coordinates coordinates;
late double yHeight;
LinePainter({required this.coordinates, required this.yHeight});
#override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Color(0xFF295AE3)
..strokeCap = StrokeCap.round
..strokeWidth = 2;
//curved
Path path = Path();
path.moveTo(coordinates.startCoords.x, yHeight - coordinates.startCoords.y);
path.quadraticBezierTo(
coordinates.startCoords.x + 15,
coordinates.startCoords.y,
coordinates.endCoords.x + 15,
yHeight - coordinates.endCoords.y);
//for every two points one line is drawn
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
I think my question is pretty straightforward but somehow stackoverflow wants me to write more.
Path drawPath(bool closePath) {
final width = MediaQuery.of(context).size.width;
final height = chartHeight;
final path = Path();
final segmentWidth = width / 3 / 2;
path.moveTo(0, height);
path.cubicTo(segmentWidth, height, 2 * segmentWidth, 0, 3 * segmentWidth, 0);
path.cubicTo(4 * segmentWidth, 0, 5 * segmentWidth, height, 6 * segmentWidth, height);
return path;
}
This tutorial is great for this purpose and everything explained well. just check the link below
https://www.kodeco.com/32557465-curved-line-charts-in-flutter
you can use fl_chart dependency here is the link
Can Flutter's inbuilt Canvas drawing methods be directly used to render variable-width strokes, for example to reflect pressure applied throughout each stroke in a handwriting app?
Ideally, in a manner compatible with saving in the XML-esque format of SVG (example at bottom).
What I think I've noticed / troubles I'm having / current attempts:
canvas.drawPath, canvas.drawPoints, canvas.drawPolygon, canvas.drawLines etc all take only a single Paint object, which can in turn have a single strokeWidth (as opposed to taking lists of Paint objects or strokeWidths, such that parameters of the path besides position could change point to point and be interpolated between).
Drawing lines, polygons or points of varying strokeWidths or radii by iterating over lists of position and pressure data and using the respective Canvas method results in no interpolation / paths not looking continuously stroked.
Screenshot from OneNote showing the behaviour I'd like:
Screenshot from the app the minimal working example below produces:
(Unoptimized) minimal working example:
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(Container(
color: Colors.white,
child: Writeable(),
));
}
class Writeable extends StatefulWidget {
#override
_WriteableState createState() => _WriteableState();
}
class _WriteableState extends State<Writeable> {
List<List<double>> pressures = List<List<double>>();
List<Offset> currentLine = List<Offset>();
List<List<Offset>> lines = List<List<Offset>>();
List<double> currentLinePressures = List<double>();
double pressure;
Offset position;
Color color = Colors.black;
Painter painter;
CustomPaint paintCanvas;
#override
Widget build(BuildContext context) {
painter = Painter(
lines: lines,
currentLine: currentLine,
pressures: pressures,
currentLinePressures: currentLinePressures,
color: color);
paintCanvas = CustomPaint(
painter: painter,
);
return Listener(
onPointerMove: (details) {
setState(() {
currentLinePressures.add(details.pressure);
currentLine.add(details.localPosition);
});
},
onPointerUp: (details) {
setState(() {
lines.add(currentLine.toList());
pressures.add(currentLinePressures.toList());
currentLine.clear();
currentLinePressures.clear();
});
},
child: paintCanvas,
);
}
}
class Painter extends CustomPainter {
Painter(
{#required this.lines,
#required this.currentLine,
#required this.color,
#required this.pressures,
#required this.currentLinePressures});
final List<List<Offset>> lines;
final List<Offset> currentLine;
final Color color;
final List<List<double>> pressures;
final List<double> currentLinePressures;
double scalePressures = 10;
Paint paintStyle = Paint();
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
// Paints here using drawPoints and PointMode.lines, but have also tried
// PointMode.points, PointMode.polygon and drawPath with a path variable and
// moveTo, lineTo methods.
#override
void paint(Canvas canvas, Size size) {
// Paint line currently being drawn (points added since pointer was
// last lifted)
for (int i = 0; i < currentLine.length - 1; i++) {
paintStyle.strokeWidth = currentLinePressures[i] * scalePressures;
canvas.drawPoints(
PointMode.lines, [currentLine[i], currentLine[i + 1]], paintStyle);
}
// Paint all completed lines drawn since app start
for (int i = 0; i < lines.length; i++) {
for (int j = 0; j < lines[i].length - 1; j++) {
paintStyle.strokeWidth = pressures[i][j] * scalePressures;
canvas.drawPoints(
PointMode.lines, [lines[i][j], lines[i][j + 1]], paintStyle);
}
}
}
}
I'm about to try writing my own implementation for rendering aesthetic SVG-friendly data from PointerEvents, but so many of the existing classes feel SVG/pretty-vectors-compatible (e.g. all the lineTos, moveTos, stroke types and endings and other parameters) that I thought it worth checking if there's something I've missed, and these methods can already do this?
Example of a few lines in an SVG file saved by Xournal++, with the stroke-width parameter changing for each line segment, and all other listed parameters presumably also having potential to change. Each line contains a moveTo command (M) and a lineTo command (L), where the latter draws a line from the current position (the last moveTo-ed or lineTo-ed), reminiscent of Flutter's segments/sub-paths and current point, to the specified offset:
<path style="fill:none;stroke-width:0.288794;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,100%,0%);stroke-opacity:1;comp-op:src;clip-to-self:true;stroke-miterlimit:10;" d="M 242.683594 45.519531 L 242.980469 45.476562 "/>
<path style="fill:none;stroke-width:0.295785;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,100%,0%);stroke-opacity:1;comp-op:src;clip-to-self:true;stroke-miterlimit:10;" d="M 242.980469 45.476562 L 243.28125 45.308594 "/>
<path style="fill:none;stroke-width:0.309105;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,100%,0%);stroke-opacity:1;comp-op:src;clip-to-self:true;stroke-miterlimit:10;" d="M 243.28125 45.308594 L 243.601562 45.15625 "/>
The approach seems to be 'draw a very short line, change stroke-width, draw the next very short line starting from the previous position', which I've tried to emulate with the paint method above.
I am using Flutter's CustomPaint to draw a scatter plot the user can add points to by tapping. Since the whole chart cannot fit on a mobile screen, I want the user to be able to pan. How can I enable this? Currently, the chart is not panning, so I can only see a section of it. Here is my code:
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
class Draw extends StatefulWidget {
String eventName;
Draw({this.eventName});
#override
_DrawState createState() => _DrawState();
}
class _DrawState extends State<Draw> {
List<Offset> points = List();
#override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
child: CustomPaint(
size: Size.infinite,
painter: DrawingPainter(),
),
),
);
}
}
class DrawingPainter extends CustomPainter {
DrawingPainter();
Paint tickSettings = Paint()
..strokeCap = (Platform.isAndroid) ? StrokeCap.butt : StrokeCap.round
..isAntiAlias = true
..color = Colors.grey
..strokeWidth = 5.0;
static double axisXCoord = 60;
static double axisHt = 1000;
var ticks = List<Offset>.generate(10, (i) => Offset(axisXCoord, i * (axisHt / 10) + 30));
#override
void paint(Canvas canvas, Size size) {
canvas.drawPoints(PointMode.points, ticks, tickSettings);
}
#override
bool shouldRepaint(DrawingPainter oldDelegate) => true;
}
(Note, I also saw Google's Flutter Charts library, but my understanding is that interactivity is limited to a few things like selecting existing points on the chart.)
Seems I solved it by wrapping the GestureDetector in a SingleChildScrollView.