Created
October 11, 2024 15:55
-
-
Save mqhamdam/3084d919b5eff96393856eba9927903b to your computer and use it in GitHub Desktop.
Custom Graph Line Chart Example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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