I'm developing a Flutter app where I need to visually display data using charts. Specifically, I want to implement:
A Pie Chart to represent proportions or percentages.
A Line Chart to display trends over time or continuous data.
I'm looking for guidance on how to implement these charts in Flutter. I'm open to any of the following approaches:
Using a reliable Flutter charting library (lightweight and customizable).
Drawing charts manually using CustomPaint, if necessary.
Any other practical and performance-friendly solution.
Yes, you can use the fl_chart package to implement both Pie and Line charts in Flutter. It’s lightweight, customizable, and supports animation.
dependencies:
fl_chart: ^0.66.0
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class PieChartSample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PieChart(
PieChartData(
sections: [
PieChartSectionData(
value: 40,
title: '40%',
color: Colors.blue,
radius: 60,
),
PieChartSectionData(
value: 30,
title: '30%',
color: Colors.orange,
radius: 60,
),
PieChartSectionData(
value: 30,
title: '30%',
color: Colors.green,
radius: 60,
),
],
sectionsSpace: 2,
centerSpaceRadius: 30,
),
);
}
}
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class LineChartSample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LineChart(
LineChartData(
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(showTitles: true),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(showTitles: true),
),
),
lineBarsData: [
LineChartBarData(
spots: [
FlSpot(0, 1),
FlSpot(1, 1.5),
FlSpot(2, 1.4),
FlSpot(3, 3.4),
FlSpot(4, 2),
FlSpot(5, 2.2),
FlSpot(6, 1.8),
],
isCurved: true,
barWidth: 3,
color: Colors.blue,
dotData: FlDotData(show: true),
),
],
),
);
}
}
I have made one custom pie chart and line chart below is the code with output hope it will help you.

import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: ChartScreen(),
debugShowCheckedModeBanner: false,
);
}
}
class ChartScreen extends StatefulWidget {
const ChartScreen({super.key});
@override
State<ChartScreen> createState() => _ChartScreenState();
}
class _ChartScreenState extends State<ChartScreen> with SingleTickerProviderStateMixin {
late final AnimationController _controller;
final List<PieChartItem> pieItems = [
PieChartItem(value: 40, color: Colors.blue),
PieChartItem(value: 30, color: Colors.red),
PieChartItem(value: 20, color: Colors.green),
PieChartItem(value: 10, color: Colors.orange),
];
final List<LineChartPoint> lineData = [
LineChartPoint(x: 0, y: 20),
LineChartPoint(x: 20, y: 40),
LineChartPoint(x: 40, y: 10),
LineChartPoint(x: 60, y: 70),
LineChartPoint(x: 80, y: 30),
LineChartPoint(x: 100, y: 90),
];
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 2));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pie & Line Chart Example')),
body: SingleChildScrollView(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Card(
elevation: 6,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: SizedBox(
width: 200,
height: 200,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: PieChartPainter(pieItems, _controller.value),
child: Container(),
);
},
),
),
),
const SizedBox(height: 30),
Card(
elevation: 6,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: SizedBox(
width: 200,
height: 200,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: LineChartPainter(lineData, _controller.value),
child: Container(),
);
},
),
),
),
const SizedBox(height: 30),
],
),
),
),
);
}
}
// Data class for Pie
class PieChartItem {
final double value;
final Color color;
PieChartItem({required this.value, required this.color});
}
// Painter for Pie Chart
class PieChartPainter extends CustomPainter {
final List<PieChartItem> items;
final double animationPercent;
PieChartPainter(this.items, this.animationPercent);
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = min(size.width, size.height) / 2 * 0.9;
final total = items.fold<double>(0, (sum, item) => sum + item.value);
double startAngle = -pi / 2;
final textPainter = TextPainter(textAlign: TextAlign.center, textDirection: TextDirection.ltr);
for (var item in items) {
final sweepAngle = (item.value / total) * 2 * pi * animationPercent;
final paint = Paint()..color = item.color;
// Draw slice
canvas.drawArc(Rect.fromCircle(center: center, radius: radius),
startAngle, sweepAngle, true, paint);
// Draw percentage text if sweepAngle > 0 (visible)
if (sweepAngle > 0) {
final middleAngle = startAngle + sweepAngle / 2;
final labelRadius = radius * 0.6;
final labelX = center.dx + labelRadius * cos(middleAngle);
final labelY = center.dy + labelRadius * sin(middleAngle);
final percent = ((item.value / total) * 100).toStringAsFixed(1) + '%';
final textSpan = TextSpan(
text: percent,
style: TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.bold,
shadows: [Shadow(blurRadius: 2, color: Colors.black.withOpacity(0.7))],
),
);
textPainter.text = textSpan;
textPainter.layout();
final offset = Offset(labelX - textPainter.width / 2, labelY - textPainter.height / 2);
textPainter.paint(canvas, offset);
}
startAngle += sweepAngle;
}
}
@override
bool shouldRepaint(covariant PieChartPainter oldDelegate) =>
oldDelegate.animationPercent != animationPercent || oldDelegate.items != items;
}
// Data class for Line Chart
class LineChartPoint {
final double x;
final double y;
LineChartPoint({required this.x, required this.y});
}
// Painter for Line Chart
class LineChartPainter extends CustomPainter {
final List<LineChartPoint> data;
final double animationPercent;
LineChartPainter(this.data, this.animationPercent);
@override
void paint(Canvas canvas, Size size) {
final paintLine = Paint()
..color = Colors.blue
..strokeWidth = 2
..style = PaintingStyle.stroke;
final paintDot = Paint()
..color = Colors.red
..style = PaintingStyle.fill;
const paddingLeft = 40.0;
const paddingBottom = 40.0;
const paddingTop = 20.0;
const paddingRight = 20.0;
final chartWidth = size.width - paddingLeft - paddingRight;
final chartHeight = size.height - paddingTop - paddingBottom;
final path = Path();
for (int i = 0; i < data.length; i++) {
final x = paddingLeft + (chartWidth * (data[i].x / 100));
final y = paddingTop + chartHeight - (chartHeight * (data[i].y / 100));
if (i == 0) {
path.moveTo(x, y);
} else {
// Only draw lines up to animationPercent
if ((i / (data.length - 1)) <= animationPercent) {
path.lineTo(x, y);
}
}
}
canvas.drawPath(path, paintLine);
// Draw dots for visible points
for (int i = 0; i < data.length; i++) {
if (data.length == 1 || (i / (data.length - 1)) <= animationPercent) {
final x = paddingLeft + (chartWidth * (data[i].x / 100));
final y = paddingTop + chartHeight - (chartHeight * (data[i].y / 100));
canvas.drawCircle(Offset(x, y), 4, paintDot);
}
}
// Draw X and Y axis
final axisPaint = Paint()
..color = Colors.black54
..strokeWidth = 1;
// Y axis
canvas.drawLine(Offset(paddingLeft, paddingTop),
Offset(paddingLeft, size.height - paddingBottom), axisPaint);
// X axis
canvas.drawLine(Offset(paddingLeft, size.height - paddingBottom),
Offset(size.width - paddingRight, size.height - paddingBottom), axisPaint);
// Draw Y labels (0, 50, 100)
final textPainter = TextPainter(textAlign: TextAlign.right, textDirection: TextDirection.ltr);
for (int i = 0; i <= 2; i++) {
final yVal = i * 50;
final y = paddingTop + chartHeight - (chartHeight * (yVal / 100));
final textSpan = TextSpan(
text: yVal.toString(),
style: const TextStyle(fontSize: 12, color: Colors.black54),
);
textPainter.text = textSpan;
textPainter.layout();
textPainter.paint(canvas, Offset(paddingLeft - 8 - textPainter.width, y - textPainter.height / 2));
}
// Draw X labels (0, 50, 100)
for (int i = 0; i <= 2; i++) {
final xVal = i * 50;
final x = paddingLeft + chartWidth * (xVal / 100);
final textSpan = TextSpan(
text: xVal.toString(),
style: const TextStyle(fontSize: 12, color: Colors.black54),
);
textPainter.text = textSpan;
textPainter.layout();
textPainter.paint(canvas, Offset(x - textPainter.width / 2, size.height - paddingBottom + 4));
}
}
@override
bool shouldRepaint(covariant LineChartPainter oldDelegate) =>
oldDelegate.animationPercent != animationPercent || oldDelegate.data != data;
}
See if the following example helps. It makes use of fl_chart package:
Get it with:
flutter pub add fl_chart
Code:
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
void main() {
runApp(
const MaterialApp(home: FlChartsPage(), debugShowCheckedModeBanner: false),
);
}
class FlChartsPage extends StatelessWidget {
const FlChartsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pie and Line Charts with fl_chart')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Pie Chart', style: Theme.of(context).textTheme.bodyMedium),
const SizedBox(height: 16),
SizedBox(
height: 250,
child: PieChart(
PieChartData(
sectionsSpace: 2,
centerSpaceRadius: 0, // Full pie (no donut hole)
sections: [
PieChartSectionData(
value: 40,
color: Colors.blue,
title: 'A 40%',
radius: 100,
titleStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
value: 30,
color: Colors.red,
title: 'B 30%',
radius: 100,
titleStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
value: 20,
color: Colors.green,
title: 'C 20%',
radius: 100,
titleStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
value: 10,
color: Colors.orange,
title: 'D 10%',
radius: 100,
titleStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
),
const SizedBox(height: 32),
Text('Line Chart', style: Theme.of(context).textTheme.bodyMedium),
const SizedBox(height: 16),
AspectRatio(
aspectRatio: 1.5,
child: LineChart(
LineChartData(
lineBarsData: [
LineChartBarData(
isCurved: true,
spots: const [
FlSpot(0, 1),
FlSpot(1, 3),
FlSpot(2, 2),
FlSpot(3, 5),
FlSpot(4, 3),
FlSpot(5, 4),
],
barWidth: 3,
color: Colors.blue,
dotData: FlDotData(show: true),
),
],
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 32,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(showTitles: true),
),
),
gridData: FlGridData(show: true),
borderData: FlBorderData(show: true),
),
),
),
const SizedBox(height: 32),
],
),
),
);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With