Skip to content

Instantly share code, notes, and snippets.

@mqhamdam
Created October 11, 2024 15:55
Show Gist options
  • Select an option

  • Save mqhamdam/3084d919b5eff96393856eba9927903b to your computer and use it in GitHub Desktop.

Select an option

Save mqhamdam/3084d919b5eff96393856eba9927903b to your computer and use it in GitHub Desktop.
Custom Graph Line Chart Example
import 'package:flutter/material.dart';
class LineChart extends StatelessWidget {
final List<List<Offset>> lines;
final List<Color> lineColors;
final double lineWidth;
final EdgeInsets padding;
final String yAxisTitle;
final double? minX;
final double? maxX;
final double? minY;
final double? maxY;
final Color backgroundColor;
final double yStep;
final int? yAxisMaxTicks;
final double horizontalLineWidth;
final Color horizontalLineColor;
final String horizontalLineStyle; // Options: 'solid', 'dotted', 'dashed'
const LineChart({
super.key,
required this.lines,
required this.lineColors,
this.lineWidth = 2.0,
this.padding = const EdgeInsets.all(16.0),
this.yAxisTitle = 'Y Axis',
this.minX,
this.maxX,
this.minY,
this.maxY,
this.backgroundColor = Colors.white,
this.yStep = 10.0,
this.yAxisMaxTicks,
this.horizontalLineWidth = 1.0,
this.horizontalLineColor = Colors.grey,
this.horizontalLineStyle = 'solid',
});
@override
Widget build(BuildContext context) {
final double actualMaxY = maxY ??
(lines.isNotEmpty && lines[0].isNotEmpty
? lines
.expand((line) => line.map((p) => p.dy))
.reduce((a, b) => a > b ? a : b)
: 100);
final int yTicks = yAxisMaxTicks ?? (actualMaxY / yStep).ceil();
final double responsiveYStep = actualMaxY / yTicks;
final double maxXWithPadding = (maxX ??
(lines.isNotEmpty && lines[0].isNotEmpty
? lines
.expand((line) => line.map((p) => p.dx))
.reduce((a, b) => a > b ? a : b)
: 10)) +
10;
final double adjustedMinX =
lines.isNotEmpty && lines[0].isNotEmpty ? lines[0].last.dx - 70 : 0;
return Padding(
padding: padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// X axis labels at the top
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
for (double y = 0; y <= yTicks; y++)
Text((responsiveYStep * y).toStringAsFixed(1),
style: const TextStyle(fontSize: 12)),
].reversed.toList(),
),
Expanded(
child: Container(
color: backgroundColor,
child: CustomPaint(
size: Size.infinite,
painter: LineChartPainter(
lines: lines,
lineColors: lineColors,
lineWidth: lineWidth,
minX: adjustedMinX,
maxX: maxXWithPadding,
minY: minY,
maxY: actualMaxY,
yStep: responsiveYStep,
horizontalLineWidth: horizontalLineWidth,
horizontalLineColor: horizontalLineColor,
horizontalLineStyle: horizontalLineStyle,
),
),
),
),
],
),
),
],
),
);
}
}
class LineChartPainter extends CustomPainter {
final List<List<Offset>> lines;
final List<Color> lineColors;
final double lineWidth;
final double? minX;
final double? maxX;
final double? minY;
final double? maxY;
final double yStep;
final double horizontalLineWidth;
final Color horizontalLineColor;
final String horizontalLineStyle;
LineChartPainter({
required this.lines,
required this.lineColors,
this.lineWidth = 2.0,
this.minX,
this.maxX,
this.minY,
this.maxY,
this.yStep = 10.0,
this.horizontalLineWidth = 1.0,
this.horizontalLineColor = Colors.grey,
this.horizontalLineStyle = 'solid',
});
@override
void paint(Canvas canvas, Size size) {
final double actualMinX = minX ??
(lines.isNotEmpty && lines[0].isNotEmpty
? lines
.expand((line) => line.map((p) => p.dx))
.reduce((a, b) => a < b ? a : b)
: 0);
final double actualMaxX = maxX ??
(lines.isNotEmpty && lines[0].isNotEmpty
? lines
.expand((line) => line.map((p) => p.dx))
.reduce((a, b) => a > b ? a : b)
: size.width);
final double actualMinY = minY ??
(lines.isNotEmpty && lines[0].isNotEmpty
? lines
.expand((line) => line.map((p) => p.dy))
.reduce((a, b) => a < b ? a : b)
: 0);
final double actualMaxY = maxY ??
(lines.isNotEmpty && lines[0].isNotEmpty
? lines
.expand((line) => line.map((p) => p.dy))
.reduce((a, b) => a > b ? a : b)
: 100);
double scaleX = size.width / (actualMaxX - actualMinX);
double scaleY = size.height / (actualMaxY - actualMinY);
// Draw horizontal lines
final horizontalLinePaint = Paint()
..color = horizontalLineColor
..strokeWidth = horizontalLineWidth
..style = PaintingStyle.stroke;
if (horizontalLineStyle == 'dotted') {
double dashWidth = 4, dashSpace = 4;
for (double y = actualMinY; y <= actualMaxY; y += yStep) {
double yPosition = size.height - (y - actualMinY) * scaleY;
double startX = 0;
while (startX < size.width) {
canvas.drawLine(Offset(startX, yPosition),
Offset(startX + dashWidth, yPosition), horizontalLinePaint);
startX += dashWidth + dashSpace;
}
}
} else if (horizontalLineStyle == 'dashed') {
double dashWidth = 8, dashSpace = 8;
for (double y = actualMinY; y <= actualMaxY; y += yStep) {
double yPosition = size.height - (y - actualMinY) * scaleY;
double startX = 0;
while (startX < size.width) {
canvas.drawLine(Offset(startX, yPosition),
Offset(startX + dashWidth, yPosition), horizontalLinePaint);
startX += dashWidth + dashSpace;
}
}
} else {
for (double y = actualMinY; y <= actualMaxY; y += yStep) {
double yPosition = size.height - (y - actualMinY) * scaleY;
canvas.drawLine(Offset(0, yPosition), Offset(size.width, yPosition),
horizontalLinePaint);
}
}
// Draw each line
for (int i = 0; i < lines.length; i++) {
final paint = Paint()
..color = lineColors[i % lineColors.length]
..strokeWidth = lineWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
List<Offset> scaledPoints = lines[i]
.map((p) => Offset(
(p.dx - actualMinX) * scaleX,
size.height - (p.dy - actualMinY) * scaleY,
))
.toList();
if (scaledPoints.isNotEmpty) {
final path = Path();
path.moveTo(scaledPoints[0].dx, scaledPoints[0].dy);
for (int j = 1; j < scaledPoints.length; j++) {
path.lineTo(scaledPoints[j].dx, scaledPoints[j].dy);
}
canvas.drawPath(path, paint);
}
}
}
@override
bool shouldRepaint(covariant LineChartPainter oldDelegate) {
return oldDelegate.lines != lines ||
oldDelegate.minX != minX ||
oldDelegate.maxX != maxX;
}
}
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:my_first_app/custom_chart.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
MainAppState createState() => MainAppState();
}
class MainAppState extends State<MainApp> {
final List<Offset> points = [];
final List<Offset> points2 = [];
Timer? _timer;
double _nextX = 5;
void _startAddingPoints() {
_timer?.cancel();
_timer = Timer.periodic(const Duration(milliseconds: 10), (timer) {
setState(() {
points.add(Offset(_nextX, getRandomInt(0, 100).toDouble()));
_nextX += 1;
if (points.length > 70) {
points.removeAt(0);
}
points2.add(Offset(_nextX, getRandomInt(0, 100).toDouble()));
if (points2.length > 70) {
points2.removeAt(0);
}
});
});
}
int getRandomInt(int min, int max) {
final Random random = Random();
return min + random.nextInt(max - min);
}
void _stopAddingPoints() {
_timer?.cancel();
_timer = null;
}
void _clearPoints() {
setState(() {
points.clear();
points2.clear();
_nextX = 0;
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Custom Line Chart')),
body: Column(
children: [
Expanded(
child: Center(
child: LineChart(
lines: [points, points2],
lineColors: const [Colors.blue, Colors.red],
yStep: 20,
lineWidth: 2.0,
yAxisTitle: 'Value',
minX: points.isNotEmpty ? points.last.dx - 70 : 0,
maxX: points.isNotEmpty ? points.last.dx + 70 : _nextX,
horizontalLineStyle: "dashed",
minY: 0,
maxY: 100,
horizontalLineColor: Colors.black26,
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _startAddingPoints,
child: const Text('Start Adding Lines'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: _stopAddingPoints,
child: const Text('Stop Adding Lines'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: _clearPoints,
child: const Text('Clear Data'),
),
],
),
],
),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment