Is there a tool to draw lines or markups directly on the graph while viewing them on the Flutter Web?
It will be really handy to have those just like TradingView does.
Of course, then these will be saved into some sort of file that I can save in db.
Thanks!
Hi,
We have validated your query. Currently, we do not have toolbox support like TrendView. However, you can achieve this by using the annotations property in the SfCartesianChart. By using annotations property, you can draw a line with CustomPainter. In the SfCartesianChart, you can obtain the tapped details using the onChartTouchInteractionDown and onChartTouchInteractionMove callbacks. In these callbacks, you can store the details in a variable and use it in the annotations property, using x to draw the line from the starting point and y to end the line. You can clear the line by using the TextButton. We have provided a code snippet, User Guide documentation, and a sample for your reference. Please let us know if you need any further details.
You can also draw any other shapes by using this annotations property. Please refer to the below User Guide link.
https://help.syncfusion.com/flutter/cartesian-charts/annotations#chart-with-watermark
Code Snippet:
|
import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_charts/charts.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget { const MyApp({Key? key});
@override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyChart(), ); } }
class MyChart extends StatefulWidget { const MyChart({Key? key});
@override State<MyChart> createState() => _MyChartState(); }
double _yChartMoveDetails = 0; double _xChartMoveDetails = 0; double _xChartDownDetails = 0; double _yChartDownDetails = 0;
class _MyChartState extends State<MyChart> { late List<ChartSampleData> _firstData;
@override void initState() { _firstData = <ChartSampleData>[ ChartSampleData(10, 50), ChartSampleData(20, 60), ChartSampleData(30, 20), ChartSampleData(40, 10), ChartSampleData(50, 30), ChartSampleData(60, 40), ChartSampleData(70, 80), ChartSampleData(80, 90), ChartSampleData(90, 70), ChartSampleData(100, 40), ]; super.initState(); }
@override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( body: Center( child: Column( children: [ SfCartesianChart( annotations: <CartesianChartAnnotation>[ CartesianChartAnnotation( region: AnnotationRegion.plotArea, widget: CustomPaint(painter: LinePainter()), coordinateUnit: CoordinateUnit.point, x: Offset(_xChartDownDetails, _yChartDownDetails).dx, y: Offset(_xChartMoveDetails, _yChartMoveDetails).dy, ), ], onChartTouchInteractionMove: (tapArgs) { setState(() {}); _xChartMoveDetails = tapArgs.position.dx; _yChartMoveDetails = tapArgs.position.dy; }, onChartTouchInteractionDown: (tapArgs) { setState(() {}); _xChartDownDetails = tapArgs.position.dx; _yChartDownDetails = tapArgs.position.dy; }, primaryXAxis: NumericAxis(), primaryYAxis: NumericAxis(), series: <ChartSeries<ChartSampleData, num>>[ ColumnSeries<ChartSampleData, num>( dataSource: _firstData, xValueMapper: (ChartSampleData sales, _) => sales.x, yValueMapper: (ChartSampleData sales, _) => sales.y, ), ], ), const SizedBox(height: 20), TextButton( onPressed: () { setState(() { _yChartMoveDetails = 0; _xChartMoveDetails = 0; _xChartDownDetails = 0; _yChartDownDetails = 0; }); }, child: const Text('Clear Line'), ), ], ), ), ), ); } }
class LinePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { Paint paint = Paint() ..color = Colors.red ..strokeWidth = 3.0; if (_yChartMoveDetails != 0 && _xChartMoveDetails != 0 && _xChartDownDetails != 0 && _yChartDownDetails != 0) { canvas.drawLine( Offset(_xChartDownDetails, _yChartDownDetails), Offset(_xChartMoveDetails, _yChartMoveDetails), paint, ); } }
@override bool shouldRepaint(covariant CustomPainter oldDelegate) { return false; } }
class ChartSampleData { final num x; final num y;
ChartSampleData(this.x, this.y); }
|
Regards,
Lokesh P.
Could you check the code snippet you provided ones, it is not working as it is supposed.
I am looking for similar functionality in my candle series charts ,as asked in this thread. But the code you provided is not drawing lines correctly. They are getting drawn randomly.
I am trying to run that in desktop chrome web browser.
Hi Shashwat,
In the previous chart version, the annotation was rendered based on the given pixel value, even though the coordinate unit was set to points. This was incorrect behavior the annotation should consider the x and y value as a center point of annotation and render. In new chart version, we have fixed this behavior issue, now the annotation render by considering the x and y values as its center point and render based on that. So, we recommend you to achieve your requirement with the help of CustomZoomPanBehavior in the SfCartesianChart. By using the CustomZoomPanBehavior, you can draw the line by storing the positions of interaction with the _startPosition and _movingPosition in the handleEvent and _updateMovingPosition methods. You can then use the stored positions to draw the line in the onPaint method of CustomZoomPanBehavior. To clear the line, you can call the reset method in the ZoomPanBehavior from the onPressed function of the TextButton. We have shared the modified code snippet, a screen recording, user guide documentation link, and modified sample for your reference below. Please let us know if you need any further assistance.
UG Link for ZoomPanBehavior: https://help.syncfusion.com/flutter/cartesian-charts/methods#events-in-zoompanbehavior
Code Snippet:
|
Column( children: [ SfCartesianChart( primaryXAxis: const NumericAxis(), primaryYAxis: const NumericAxis(), zoomPanBehavior: _zoomPanBehavior, ), const SizedBox(height: 20), TextButton( onPressed: () { _zoomPanBehavior.reset(); }, child: const Text('Clear Line'), ), ], ),
class CustomZoomPanBehavior extends ZoomPanBehavior { Offset? _startPosition; Offset? _movingPosition;
@override bool get enablePanning => true;
@override void handleEvent(PointerEvent event, BoxHitTestEntry entry) { if (event is PointerDownEvent) { _startPosition = parentBox!.globalToLocal(event.position); } else if (event is PointerMoveEvent) { _updateMovingPosition(event.position); } else if (event is PointerUpEvent) { _clearPositions(); } }
void _updateMovingPosition(Offset globalPosition) { if (parentBox == null || _startPosition == null) return;
Offset moveOffset = parentBox!.globalToLocal(globalPosition); _movingPosition = _startPosition! - (_startPosition! - moveOffset); parentBox?.markNeedsPaint(); }
void _clearPositions() { _startPosition = null; _movingPosition = null; }
@override void reset() { parentBox?.markNeedsPaint(); }
@override void onPaint(PaintingContext context, Offset offset, SfChartThemeData chartThemeData, ThemeData themeData) { if (_startPosition != null && _movingPosition != null) { context.canvas.drawLine( _startPosition!, _movingPosition!, Paint() ..color = Colors.red ..strokeWidth = 3, ); } } }
|
Regards,
Lokesh P.
In this method, I am unable to draw lines based on the chart point. Instead, it draws the line referencing the screen coordinates. As a result, when I move the chart around, the line does not move with it but instead stays in the same spot.
Could you tell me how to achieve this behavior in the new method?
Hi Shashwat,
We have analyzed your query and to achieve your requirement we suggest following the below suggestions:
To draw the line, we handle touch events: when the user touches down (onChartTouchInteractionDown), we start recording the drag position, and when the touch is released (onChartTouchInteractionUp), we complete the line segment. These points are added to the draggingLineData list, which is used as the data source for the LineSeries. We also manage panning by using the ZoomPanBehavior with an enablePanning flag. The panning functionality can be toggled on or off using a button in the app bar. When panning is disabled, we record the line points; when enabled, the user can pan the chart without affecting the line drawing. We have shared a code snippet, and a sample for your reference. You can modify the sample based on your needs.
Code snippet:
late final List<ChartData> chartData = <ChartData>[ ChartData(10, 50), ChartData(20, 60), ChartData(30, 20), ChartData(40, 10), ChartData(50, 30), ChartData(60, 40), ChartData(70, 80), ChartData(80, 90), ChartData(90, 70), ChartData(100, 40), ]; final List<ChartData> draggingLineData = <ChartData>[]; int? pointIndex; late Offset position; ChartSeriesController? _chartSeriesController; late ZoomPanBehavior _zoomPanBehavior;
bool _isPanningEnabled = false;
@override void initState() { super.initState(); _zoomPanBehavior = ZoomPanBehavior( enableMouseWheelZooming: true, zoomMode: ZoomMode.x, enablePanning: _isPanningEnabled, ); }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Chart with Toggle Panning'), actions: [ TextButton( onPressed: () { setState(() { _isPanningEnabled = !_isPanningEnabled; _zoomPanBehavior = ZoomPanBehavior( enableMouseWheelZooming: true, zoomMode: ZoomMode.x, enablePanning: _isPanningEnabled, ); }); }, child: Text( _isPanningEnabled ? 'Disable Panning' : 'Enable Panning', style: const TextStyle( color: Colors.black, backgroundColor: Colors.blue), ), ), ], ), body: Column( children: <Widget>[ Expanded( child: SfCartesianChart( primaryXAxis: const NumericAxis( minimum: 0, maximum: 110, ), primaryYAxis: const NumericAxis(), zoomPanBehavior: _zoomPanBehavior, onChartTouchInteractionDown: (tapArgs) { if (!_isPanningEnabled) { setState(() { draggingLineData .clear(); // Clear the last segment before adding a new one CartesianChartPoint<dynamic> dragStartPoint = _chartSeriesController!.pixelToPoint(tapArgs.position); draggingLineData.add( ChartData(dragStartPoint.x, dragStartPoint.y as double?), ); }); } }, onChartTouchInteractionUp: (tapArgs) { if (!_isPanningEnabled) { CartesianChartPoint<dynamic> dragEndPoint = _chartSeriesController!.pixelToPoint(tapArgs.position); setState(() { draggingLineData.add( ChartData(dragEndPoint.x, dragEndPoint.y as double?), ); }); } }, series: <CartesianSeries<ChartData, num>>[ ColumnSeries<ChartData, num>( animationDuration: 0, dataSource: chartData, xValueMapper: (ChartData data, _) => data.x, yValueMapper: (ChartData data, _) => data.y, ), LineSeries<ChartData, num>( animationDuration: 0, dataSource: draggingLineData, onRendererCreated: (ChartSeriesController controller) { _chartSeriesController = controller; }, xValueMapper: (ChartData data, _) => data.x, yValueMapper: (ChartData data, _) => data.y, ), ], ), ), ], ), ); } }
class ChartData { ChartData(this.x, this.y); final num x; final double? y; } |
Please let us know if you need any further assistance.
Regards,
Preethika Selvam.
Attachment: fr185439_884e4f1e.zip