syncfusion / flutter-widgets

Syncfusion Flutter widgets libraries include high quality UI widgets and file-format packages to help you create rich, high-quality applications for iOS, Android, and web from a single code base.
1.6k stars 788 forks source link

[syncfusion_flutter_charts-SfCartesianChart]Conflic beetween scroll horizontal and get index of series #2124

Open quynhnb2021 opened 1 month ago

quynhnb2021 commented 1 month ago

Bug description

When using SfCartesianChart in Flutter with horizontal scrolling enabled via ZoomPanBehavior and autoScrollingDelta, the chart experiences issues when trying to retrieve the index of the data points using the onTrackballPositionChanging callback. Specifically:

When panning to the rightmost data points, the chart glitches by snapping back to the initial position, preventing further horizontal scrolling. Trackball and tooltip do not appear even though TrackballBehavior is enabled. Additionally, I am trying to change the color of axis labels (to red) when the trackball is over a specific data point or when scrolling. However, the index from the onTrackballPositionChanging callback does not update properly, preventing the color change from happening.

Steps to reproduce

1: Create a SfCartesianChart with the following configuration: Enable horizontal panning with ZoomPanBehavior(enablePanning: true). Set auto-scrolling properties with autoScrollingDelta: 5 and autoScrollingMode: AutoScrollingMode.start. Add a TrackballBehavior with activationMode: ActivationMode.singleTap and enable: true. Implement the onTrackballPositionChanging callback to capture the index of the data point: onTrackballPositionChanging: (trackballArgs) { setState(() { indexOfPoint = trackballArgs.chartPointInfo.dataPointIndex ?? 0; }); }, Use axisLabelFormatter to change the axis label color when the index matches the data point: axisLabelFormatter: (AxisLabelRenderDetails details) { return ChartAxisLabel( details.text, TextStyle( color: indexOfPoint == details.value ? Colors.red : Colors.black, ), ); },

2: Scroll horizontally to the rightmost data points.

3: Observe the following issues: The chart snaps back to the initial position, preventing continuous scrolling to the right. The trackball and tooltip are not displayed when tapping on the chart, even though the TrackballBehavior is enabled. The axis label color does not update based on the indexOfPoint due to improper index handling. This issue seems to be a conflict between ZoomPanBehavior and TrackballBehavior when horizontal scrolling is enabled with autoScrollingMode.

Code sample

Code sample ```dart [import 'dart:math'; import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { late List chartData; late TrackballBehavior _trackballBehavior; int indexOfPoint = 0; @override void initState() { chartData = List.generate(100, (int index) => ChartData(index.toString(), Random().nextInt(90))) .toList(); _trackballBehavior = TrackballBehavior( activationMode: ActivationMode.singleTap, enable: true, ); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Padding( padding: const EdgeInsets.all(20.0), child: SfCartesianChart( onAxisLabelTapped: (axisLabelTapArgs) { setState(() { indexOfPoint = axisLabelTapArgs.value as int; }); }, trackballBehavior: _trackballBehavior, onTrackballPositionChanging: (trackballArgs) { setState(() { indexOfPoint = trackballArgs.chartPointInfo.dataPointIndex ?? 0; }); }, zoomPanBehavior: ZoomPanBehavior( enablePanning: true, ), primaryXAxis: CategoryAxis( autoScrollingDelta: 5, autoScrollingMode: AutoScrollingMode.start, axisLabelFormatter: (AxisLabelRenderDetails details) { return ChartAxisLabel( details.text, TextStyle( color: indexOfPoint == details.value ? Colors.red : Colors.black, )); }), series: >[ SplineSeries( dataSource: chartData, xValueMapper: (ChartData data, _) => data.x, yValueMapper: (ChartData data, _) => data.y, ) ], ), ), ); } } class ChartData { final String x; final num y; ChartData(this.x, this.y); } ] ```

Screenshots or Video

Screenshots / Video demonstration [Upload media here]

https://github.com/user-attachments/assets/299f8bce-0c69-4f2a-85e1-a171b152a1de

Stack Traces

Stack Traces ```dart [Add the Stack Traces here] [demochart.zip](https://github.com/user-attachments/files/17345537/demochart.zip) ```

On which target platforms have you observed this bug?

Android, iOS

Flutter Doctor output

Doctor output ```console [Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.24.3, on macOS 14.5 23F79 darwin-x64, locale en-VN) [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [✓] Xcode - develop for iOS and macOS (Xcode 15.0) [✓] Chrome - develop for the web [✓] Android Studio (version 2023.1) [✓] IntelliJ IDEA Ultimate Edition (version 2023.3.4) [✓] VS Code (version 1.93.1) [✓] Connected device (4 available) [✓] Network resources • No issues found!] ```
quynhnb2021 commented 1 month ago

@VijayakumarMariappan Hello, Did you check this bug?

Baranibharathip commented 1 month ago

Hi @quynhnb2021,

We have validated the issue and would like to inform you that the reported problem 'Charts get resetting to old or previous position' occurs because of calling setState in the onAxisLabelTapped and onTrackballPositionChanging callbacks, causes the chart to rebuild every time, resetting when the trackball is moved or the chart is panned. We recommend you to avoid using setState inside the chart callbacks while doing interactions.

We have achieved your requirement by implementing a CustomTrackballBehavior, where we override the onPaint method of the TrackballBehavior. This onPaint method allows us to draw custom axis labels with specific styling, such as custom colors and font sizes. By using the _CustomTrackballBehavior class, you can control the appearance and positioning of the axis labels and tooltips as per your needs. Please refer to the following code snippet.

Code snippet:

class _CustomTrackballBehavior extends TrackballBehavior {
  @override
  bool get enable => true;

  @override
  ActivationMode get activationMode => ActivationMode.singleTap;

  @override
  void onPaint(PaintingContext context, Offset offset,
      SfChartThemeData chartThemeData, ThemeData themeData) {
    super.onPaint(context, offset, chartThemeData, themeData);
    if (chartPointInfo.isEmpty || parentBox == null) {
      return;
    }

    final Rect plotAreaBounds = parentBox!.paintBounds;
    final Offset position =
        Offset(chartPointInfo[0].xPosition!, chartPointInfo[0].yPosition!);
    _drawCustomAxisLabel(context.canvas, position, plotAreaBounds);
  }

  void _drawCustomAxisLabel(
      Canvas canvas, Offset position, Rect plotAreaBounds) {
    const TextStyle textStyle =
        TextStyle(color: Colors.red, fontSize: 13, fontWeight: FontWeight.bold);
    final Paint rectPaint = Paint()
      ..color = Colors.white
      ..style = PaintingStyle.fill;

    final Offset tooltipPos = Offset(position.dx, plotAreaBounds.bottom);
    final String label = chartPointInfo[0].header ?? '0';
    final Size labelSize = measureText(label, textStyle);
    final Rect rect = _calculateRect(labelSize, tooltipPos);
    final Offset alignedPos = tooltipPos.translate(-rect.width / 2, 5);

    final RRect tooltipRRect = RRect.fromRectAndRadius(
      Rect.fromLTWH(alignedPos.dx, alignedPos.dy, rect.width, rect.height),
      const Radius.circular(5),
    );

    canvas.drawRRect(tooltipRRect, rectPaint);
    _drawText(canvas, label, _textPosition(tooltipRRect, labelSize), textStyle);
  }

  Rect _calculateRect(Size labelSize, Offset tooltipPos) {
    const double padding = 5;
    return Rect.fromLTWH(
      tooltipPos.dx,
      tooltipPos.dy,
      labelSize.width + padding,
      labelSize.height + padding,
    );
  }

  Offset _textPosition(RRect tooltipRRect, Size labelSize) {
    return Offset(
        (tooltipRRect.left + tooltipRRect.width / 2) - labelSize.width / 2,
        (tooltipRRect.top + tooltipRRect.height / 2) - labelSize.height / 2);
  }

  void _drawText(Canvas canvas, String text, Offset point, TextStyle style) {
    final TextPainter textPainter = TextPainter(
      text: TextSpan(text: text, style: style),
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );

    textPainter
      ..layout()
      ..paint(canvas, point);
  }
}

Demo:

https://github.com/user-attachments/assets/171b31fd-ae14-4537-8e7e-f16431a0eac5

For more details, refer the following Knowledge Base link: KB : https://support.syncfusion.com/kb/article/16112/how-to-customize-the-trackball-in-flutter-cartesianchart

Regards, Baranibharathi P.

quynhnb2021 commented 1 month ago

Hi @Baranibharathip ,

Thanks, it works! By the way, could you help me check how to use CustomTrackballBehavior to showByIndex in your example? I want to bold in red and display the tooltip and trackball on the first element of the chart without needing to tap on it.

Baranibharathip commented 3 weeks ago

Hi @quynhnb2021,

We have validated your query and would like to inform you that a custom trackball behavior, similar to the default trackball behavior, can be used. To achieve the requirement of displaying the trackball tooltip on the first element of the chart without the need to tap on it, you can use the showByIndex method of the TrackballBehavior to display the trackball at a specific data point. Please refer to the following code snippet.

Code snippet:

late TrackballBehavior trackballBehavior;

void initState() {
    // Initialize the custom trackball behavior
    trackballBehavior = _CustomTrackballBehavior();
    // Show the trackball at the first data point (index 0) once the chart is built
    WidgetsBinding.instance.addPostFrameCallback((_) {
      trackballBehavior.showByIndex(0);
    });
    super.initState();
  }

Demo:

https://github.com/user-attachments/assets/eb7a5a90-7c51-4c81-bf3f-f62770c08c60

For more details, refer to the following User Guide link: UG: https://help.syncfusion.com/flutter/cartesian-charts/trackball-crosshair#trackball

Also, we have attached sample for your reference.

Sample: GH_2124.zip

Regards, Baranibharathi P.