imaNNeo / fl_chart

FL Chart is a highly customizable Flutter chart library that supports Line Chart, Bar Chart, Pie Chart, Scatter Chart, and Radar Chart.
https://flchart.dev
MIT License
6.85k stars 1.78k forks source link

How to make scrollable x axis in flchart so that it looks nice not so compact in nature? #71

Open nimesh1997 opened 5 years ago

nimesh1997 commented 5 years ago

Bar chart is very compact in nature if there are many values on the x-axis. How to make it less compact if there are so many values on x-axis. I want it to horizontal scrollable? Screenshot_20190928-152201

imaNNeo commented 5 years ago

Hi, I'm so happy to see your result :) Unfortunately, currently we are not going to implement the scrollable feature, I'm so busy these days and also pull requests are welcome, we will implement it in the future. Cheers! Thanks.

imaNNeo commented 5 years ago

I left it open, and people can thumb it up, then I will do it with high priority.

ZantsuRocks commented 5 years ago

@nimesh1997 You can put it in a Container bigger than the screen and put that Container in a SingleChildScrollView with scrollDirection: Axis.horizontal

imaNNeo commented 5 years ago

@ZantsuRocks Greate solution, Thank you :)

davicg commented 5 years ago

@ZantsuRocks solution is really useful, but this is still a good feature to be implemented in the future. In my case, along with the scrolling behavior I need a callback to mark each bar as "touched" when the chart is scrolled.

Stitch-Taotao commented 3 years ago

@nimesh1997 You can put it in a Container bigger than the screen and put that Container in a SingleChildScrollView with scrollDirection: Axis.horizontal

This will get perfomance problem ,especially in web .

Shashwat-Joshi commented 3 years ago

@nimesh1997 You can put it in a Container bigger than the screen and put that Container in a SingleChildScrollView with scrollDirection: Axis.horizontal

This is not working if I have a Sliver App Bar and the graph is placed in SliverChildListDelegate. I tried to change the width of container but it still is constant.

Abhilash-Chandran commented 3 years ago

If anyone wants to achieve panning and mousewheel zooming, following code might help. I have tested the following in flutter for windows and browser. In windows it works well and satisfies my needs. In browser however the panning is not good because of DragupdateDetails.primaryDelta comes in as 0 quite often and hence the panning is jittery.

Note this logic still has flaws like minX and maxX not being clamped to stop zooming etc. However I feel this is good start.

@imaNNeoFighT I am not sure if this is a performant way, but seems to achieve some results. Atleast in windows I didn't feel any jitter. 😄

Idea is as follows.

  1. Use a Listener widget to listen to mouse scroll events.
    • Increment and decrement minx and maxx by a fixed percentage of maxX, depending on the scroll direction.
      1. Use a GestureDetector widget to detect horizontal drag event.
    • decrement both minX and maxX by a percentage if panning to the left. that is if primary delta is negative.
    • Increment both minX and maxX by a percentage if panning to the right. that is if primary delta is positive.
  2. Finally clip the plot to be within the bounds using the clipData: FlClipData.all() of the LineChartData. without this the plot is rendered outside the widget.

cnLXXH8TLX

Following example achieves panning and zooming only in x-axis. However the logic can be extended to yaxis as well.

import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

class PlotData {
  List<double> result;
  double maxY;
  double minY;
  PlotData({
    required this.result,
    required this.maxY,
    required this.minY,
  });
}

class LinePlot extends StatefulWidget {
  final PlotData plotData;
  const LinePlot({
    required this.plotData,
    Key? key,
  }) : super(key: key);

  @override
  _LinePlotState createState() => _LinePlotState();
}

class _LinePlotState extends State<LinePlot> {
  late double minX;
  late double maxX;
  @override
  void initState() {
    super.initState();
    minX = 0;
    maxX = widget.plotData.result.length.toDouble();
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerSignal: (signal) {
        if (signal is PointerScrollEvent) {
          setState(() {
            if (signal.scrollDelta.dy.isNegative) {
              minX += maxX * 0.05;
              maxX -= maxX * 0.05;
            } else {
              minX -= maxX * 0.05;
              maxX += maxX * 0.05;
            }
          });
        }
      },
      child: GestureDetector(
        onDoubleTap: () {
          setState(() {
            minX = 0;
            maxX = widget.plotData.result.length.toDouble();
          });
        },
        onHorizontalDragUpdate: (dragUpdDet) {
          setState(() {
            print(dragUpdDet.primaryDelta);
            double primDelta = dragUpdDet.primaryDelta ?? 0.0;
            if (primDelta != 0) {
              if (primDelta.isNegative) {
                minX += maxX * 0.005;
                maxX += maxX * 0.005;
              } else {
                minX -= maxX * 0.005;
                maxX -= maxX * 0.005;
              }
            }
          });
        },
        child: LineChart(
          LineChartData(
            minX: minX,
            maxX: maxX,
            maxY: widget.plotData.maxY + widget.plotData.maxY * 0.1,
            titlesData: FlTitlesData(
              bottomTitles: SideTitles(
                showTitles: true,
                interval: widget.plotData.result.length / 10,
              ),
              leftTitles: SideTitles(
                showTitles: true,
                margin: 5,
              ),
              topTitles: SideTitles(
                showTitles: false,
                margin: 5,
              ),
            ),
            gridData: FlGridData(
              drawHorizontalLine: false,
            ),
            clipData: FlClipData.all(),
            lineBarsData: [
              LineChartBarData(
                barWidth: 1,
                dotData: FlDotData(
                  show: false,
                ),
                spots: widget.plotData.result
                    .asMap()
                    .entries
                    .map((entry) => FlSpot(entry.key.toDouble(), entry.value))
                    .toList(),
              )
            ],
          ),
        ),
      ),
    );
  }
}
jlubeck commented 3 years ago

This seems to have a lot of thumbs up now, are there any plans to support it in the near future? Thanks!

imaNNeo commented 3 years ago

Hi @jlubeck. You are right, it has a lot of thumbs up. I don't think we support it in the near future. Because I have a full-time job and I don't have any profit from this project (that's why I can't work a lot on this project) I work just like before, I will implement these features in my free time. BTW I can't promise any due time.

Also pull requests are welcome.

jlubeck commented 3 years ago

Totally understandable @imaNNeoFighT I wasn't trying to sound pushy or anything. I even sent a PR in the past and worked with you, just that this issue is a little over my head haha.

Looking forward to whenever there's time to do it!

imaNNeo commented 3 years ago

Thanks, @jlubeck. Yes, I remember you. I'm happy that you are an active member of our library.

Sorry if my answer bothered you.

jlubeck commented 3 years ago

Not a bother at all my friend!

jrguedes commented 3 years ago

I've found a function that I think can help to make it works.

https://www.reddit.com/r/flutterhelp/comments/nidlf5/how_to_make_graph_fl_chart_scrollable/

Widget makeScrollable(Widget w, {double width = 2000.0, double height = 2000.0}) {
  return Scrollbar( 
    child: SingleChildScrollView( 
      scrollDirection: Axis.horizontal, 
      child: SizedBox( 
        width: width, 
        height: height, 
        child: ListView( 
          children: [ 
            SizedBox( 
              width: width, 
              height: height, 
              child: w, 
             ) 
           ], 
        )
      )
    )
  ); 
}
DhavalRKansara commented 3 years ago

I left it open, and people can thumb it up, then I will do it with high priority.

So @imaNNeoFighT Do you have any plans to provide the support related to this issue, Where user can zoom in/out the graph as well use can horizontally(x-axis) scroll the data too?

imaNNeo commented 3 years ago

Hi @DhavalRKansara. I really like to fix this issue (Because I know it helps you and my objective is to help). But you know it takes a lot of time and I'm not able to spend my time to solve this issue right now (because I'm a full-time developer like many of you). But definitely, I will work on it If I find a little free time.

Even though I developing this package opensource and free, donation is a good thing for motivation. I set a goal to work 2 weeks full time. Check it out on buyMeACoffee

rrifafauzikomara commented 3 years ago

If anyone wants to achieve panning and mousewheel zooming, following code might help. I have tested the following in flutter for windows and browser. In windows it works well and satisfies my needs. In browser however the panning is not good because of DragupdateDetails.primaryDelta comes in as 0 quite often and hence the panning is jittery.

Note this logic still has flaws like minX and maxX not being clamped to stop zooming etc. However I feel this is good start.

@imaNNeoFighT I am not sure if this is a performant way, but seems to achieve some results. Atleast in windows I didn't feel any jitter. 😄

Idea is as follows.

  1. Use a Listener widget to listen to mouse scroll events.

    • Increment and decrement minx and maxx by a fixed percentage of maxX, depending on the scroll direction.
  2. Use a GestureDetector widget to detect horizontal drag event.
  • decrement both minX and maxX by a percentage if panning to the left. that is if primary delta is negative.
  • Increment both minX and maxX by a percentage if panning to the right. that is if primary delta is positive.
  1. Finally clip the plot to be within the bounds using the clipData: FlClipData.all() of the LineChartData. without this the plot is rendered outside the widget.

cnLXXH8TLX

Following example achieves panning and zooming only in x-axis. However the logic can be extended to yaxis as well.

import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

class PlotData {
  List<double> result;
  double maxY;
  double minY;
  PlotData({
    required this.result,
    required this.maxY,
    required this.minY,
  });
}

class LinePlot extends StatefulWidget {
  final PlotData plotData;
  const LinePlot({
    required this.plotData,
    Key? key,
  }) : super(key: key);

  @override
  _LinePlotState createState() => _LinePlotState();
}

class _LinePlotState extends State<LinePlot> {
  late double minX;
  late double maxX;
  @override
  void initState() {
    super.initState();
    minX = 0;
    maxX = widget.plotData.result.length.toDouble();
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerSignal: (signal) {
        if (signal is PointerScrollEvent) {
          setState(() {
            if (signal.scrollDelta.dy.isNegative) {
              minX += maxX * 0.05;
              maxX -= maxX * 0.05;
            } else {
              minX -= maxX * 0.05;
              maxX += maxX * 0.05;
            }
          });
        }
      },
      child: GestureDetector(
        onDoubleTap: () {
          setState(() {
            minX = 0;
            maxX = widget.plotData.result.length.toDouble();
          });
        },
        onHorizontalDragUpdate: (dragUpdDet) {
          setState(() {
            print(dragUpdDet.primaryDelta);
            double primDelta = dragUpdDet.primaryDelta ?? 0.0;
            if (primDelta != 0) {
              if (primDelta.isNegative) {
                minX += maxX * 0.005;
                maxX += maxX * 0.005;
              } else {
                minX -= maxX * 0.005;
                maxX -= maxX * 0.005;
              }
            }
          });
        },
        child: LineChart(
          LineChartData(
            minX: minX,
            maxX: maxX,
            maxY: widget.plotData.maxY + widget.plotData.maxY * 0.1,
            titlesData: FlTitlesData(
              bottomTitles: SideTitles(
                showTitles: true,
                interval: widget.plotData.result.length / 10,
              ),
              leftTitles: SideTitles(
                showTitles: true,
                margin: 5,
              ),
              topTitles: SideTitles(
                showTitles: false,
                margin: 5,
              ),
            ),
            gridData: FlGridData(
              drawHorizontalLine: false,
            ),
            clipData: FlClipData.all(),
            lineBarsData: [
              LineChartBarData(
                barWidth: 1,
                dotData: FlDotData(
                  show: false,
                ),
                spots: widget.plotData.result
                    .asMap()
                    .entries
                    .map((entry) => FlSpot(entry.key.toDouble(), entry.value))
                    .toList(),
              )
            ],
          ),
        ),
      ),
    );
  }
}

How if use BarChartData? Because don't have minX and maxX and only have minY and maxY. I already try with that but it's not horizontal scroll but vertical scroll.

DhavalRKansara commented 3 years ago

Hi @DhavalRKansara. I really like to fix this issue (Because I know it helps you and my objective is to help). But you know it takes a lot of time and I'm not able to spend my time to solve this issue right now (because I'm a full-time developer like many of you). But definitely, I will work on it If I find a little free time.

Even though I developing this package opensource and free, donation is a good thing for motivation. I set a goal to work 2 weeks full time. Check it out on buyMeACoffee

Sure @imaNNeoFighT, No issues I taught the issues is open since 2 years and you have close a lots of issues under this... So apart from lots of other people are waiting for this release since so long...

2shrestha22 commented 2 years ago

We badly want this feature.

khamidjon-khamidov commented 2 years ago

This worked for me

SingleChildScrollView(
  scrollDirection: Axis.horizontal,
  child: Container(
    width: 1000,
    padding: EdgeInsets.all(8),
    child: flchart.BarChart(
      ...
    ),
  )
)
ketancts commented 2 years ago

@imaNNeoFighT any update on the issue. We are not able to scroll horizontal axis

Check this

ketancts commented 2 years ago

This worked for me

SingleChildScrollView(
  scrollDirection: Axis.horizontal,
  child: Container(
    width: 1000,
    padding: EdgeInsets.all(8),
    child: flchart.BarChart(
      ...
    ),
  )
)

@SomeoneAndNoone Can you please, Share a full source code ? How to solve above issue?

roly151 commented 2 years ago

@imaNNeoFighT In another issue you mentioned:

Actually, in our latest release, we introduced a ChartScaffold class to separate the chart and titles. Now it allows us to have the chart itself scrollable (with fixed titles).

Is this available now? Are there any examples we can see to implement this, as it's exactly what I'm looking for? (fixed titles)

imaNNeo commented 2 years ago

@imaNNeoFighT In another issue you mentioned:

Actually, in our latest release, we introduced a ChartScaffold class to separate the chart and titles. Now it allows us to have the chart itself scrollable (with fixed titles).

Is this available now? Are there any examples we can see to implement this, as it's exactly what I'm looking for? (fixed titles)

Yes, it is available now. But there is no public API to use in your app. You should change the internal library codes to achieve what you want. (We have an AxisChartScaffoldWidget which is private in our lib)

BTW my next big step is going to implement this feature internally.

roly151 commented 2 years ago

Sorry, this is a little beyond my skills. Is it possible for someone to share example code of how I might use this to get fixed left titles for a line chart I have in my app? The graph scrolls on the x-axis.

vlad-ed-git commented 2 years ago

To achieve horizontal scrolling for the LineChart, I am wrapping it in a container with a large width (in code below it's 2 x the number of data points ) and then wrapping that container in a SingleChildScrollView and then wrapping that in a constrained container with a fixed max width )....

Container( constraints: BoxConstraints( maxWidth : 400), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Container( width: widget.mainPoints.length * 2, child: LineChart( ....

AviSharmaaa commented 2 years ago

Hey @imaNNeoFighT any update on the issue?

KaranCodes95 commented 2 years ago

No hate or offence but for an app claiming - "highly customizable Flutter chart library", with no horizontal scrolling for extra data, is not cool. Respect for all the effort though.

GivDavidL commented 1 year ago

This worked for me

SingleChildScrollView(
  scrollDirection: Axis.horizontal,
  child: Container(
    width: 1000,
    padding: EdgeInsets.all(8),
    child: flchart.BarChart(
      ...
    ),
  )
)

Can you do this without losing the axis on the side? I scroll right and the left axis disappears

roly151 commented 1 year ago

This worked for me

SingleChildScrollView(
  scrollDirection: Axis.horizontal,
  child: Container(
    width: 1000,
    padding: EdgeInsets.all(8),
    child: flchart.BarChart(
      ...
    ),
  )
)

Can you do this without losing the axis on the side? I scroll right and the left axis disappears

Hi @GivDavidL - Yes, this is possible, as the chart axis and chart itself can now be separated. I created a row, displayed the axis, then the chart, and then another axis. I will paste the code for the axis I created here:

Container(
          color: Colors.transparent,
          width: 14.0,
          height: widget.chartHeight,
          child: DecorationsRenderer(
            [
              HorizontalAxisDecoration(
                showTopValue: true,
                axisStep: 3,
                showValues: true,
                legendFontStyle: Theme.of(context).textTheme.caption,
                lineColor: Theme.of(context)
                    .colorScheme
                    .primaryContainer
                    .withOpacity(0.2),
              ),
            ],
            // Must pass same state as your chart, this is used to calculate spacings and padding of decoration, to make sure it matches the chart.
            _chartState,
          ),
        ),
desmeit commented 1 year ago

need this feature too.

Pebsie commented 1 year ago

This worked for me

SingleChildScrollView(
  scrollDirection: Axis.horizontal,
  child: Container(
    width: 1000,
    padding: EdgeInsets.all(8),
    child: flchart.BarChart(
      ...
    ),
  )
)

Can you do this without losing the axis on the side? I scroll right and the left axis disappears

Hi @GivDavidL - Yes, this is possible, as the chart axis and chart itself can now be separated. I created a row, displayed the axis, then the chart, and then another axis. I will paste the code for the axis I created here:

Container(
          color: Colors.transparent,
          width: 14.0,
          height: widget.chartHeight,
          child: DecorationsRenderer(
            [
              HorizontalAxisDecoration(
                showTopValue: true,
                axisStep: 3,
                showValues: true,
                legendFontStyle: Theme.of(context).textTheme.caption,
                lineColor: Theme.of(context)
                    .colorScheme
                    .primaryContainer
                    .withOpacity(0.2),
              ),
            ],
            // Must pass same state as your chart, this is used to calculate spacings and padding of decoration, to make sure it matches the chart.
            _chartState,
          ),
        ),

Unless I'm mistaken, you're responding to an issue on the wrong plugin. This is fl_chart not charts_painter. fl_chart doesn't have DecorationsRenderer, ChartState or HorizontalAxisDecoration.

roly151 commented 1 year ago

Unless I'm mistaken, you're responding to an issue on the wrong plugin. This is fl_chart not charts_painter. fl_chart doesn't have DecorationsRenderer, ChartState or HorizontalAxisDecoration.

My apologies, you are correct. I haven't looked into this in a while and forgot I had changed so I could have axis that didnt disappear.

TeoVogel commented 1 year ago

I improved over @Abhilash-Chandran solution. I made a wrapper widget for charts that use the minX and maxX parameters. It works great on mobile, supports double tap, pinch zoom and drag. Works only on the X axis (because that's what I need) but it should be easy to extend.

import 'dart:math';

import 'package:flutter/material.dart';

class ZoomableChart extends StatefulWidget {
  ZoomableChart({
    super.key,
    required this.maxX,
    required this.builder,
  });

  double maxX;
  Widget Function(double, double) builder;

  @override
  State<ZoomableChart> createState() => _ZoomableChartState();
}

class _ZoomableChartState extends State<ZoomableChart> {
  late double minX;
  late double maxX;

  late double lastMaxXValue;
  late double lastMinXValue;

  @override
  void initState() {
    super.initState();
    minX = 0;
    maxX = widget.maxX;
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onDoubleTap: () {
        setState(() {
          minX = 0;
          maxX = widget.maxX;
        });
      },
      onHorizontalDragStart: (details) {
        lastMinXValue = minX;
        lastMaxXValue = maxX;
      },
      onHorizontalDragUpdate: (details) {
        var horizontalDistance = details.primaryDelta ?? 0;
        if (horizontalDistance == 0) return;
        print(horizontalDistance);
        var lastMinMaxDistance = max(lastMaxXValue - lastMinXValue, 0.0);

        setState(() {
          minX -= lastMinMaxDistance * 0.005 * horizontalDistance;
          maxX -= lastMinMaxDistance * 0.005 * horizontalDistance;

          if (minX < 0) {
            minX = 0;
            maxX = lastMinMaxDistance;
          }
          if (maxX > widget.maxX) {
            maxX = widget.maxX;
            minX = maxX - lastMinMaxDistance;
          }
          print("$minX, $maxX");
        });
      },
      onScaleStart: (details) {
        lastMinXValue = minX;
        lastMaxXValue = maxX;
      },
      onScaleUpdate: (details) {
        var horizontalScale = details.horizontalScale;
        if (horizontalScale == 0) return;
        print(horizontalScale);
        var lastMinMaxDistance = max(lastMaxXValue - lastMinXValue, 0);
        var newMinMaxDistance = max(lastMinMaxDistance / horizontalScale, 10);
        var distanceDifference = newMinMaxDistance - lastMinMaxDistance;
        print("$lastMinMaxDistance, $newMinMaxDistance, $distanceDifference");
        setState(() {
          final newMinX = max(
            lastMinXValue - distanceDifference,
            0.0,
          );
          final newMaxX = min(
            lastMaxXValue + distanceDifference,
            widget.maxX,
          );

          if (newMaxX - newMinX > 2) {
            minX = newMinX;
            maxX = newMaxX;
          }
          print("$minX, $maxX");
        });
      },
      child: widget.builder(minX, maxX),
    );
  }
}

Copy and paste that code in a file like zoomable_chart.dart. Then wrap your chart with ZoomableChart, provide a maxX and a function that returns your widget.

Example

return AspectRatio(
      aspectRatio: 16 / 9,
      child: ZoomableChart(
        maxX: /* maxX OF YOUR DATA HERE */,
        builder: (minX, maxX) {
          return LineChart(
            LineChartData(
              clipData: FlClipData.all(),
              minX: minX,
              maxX: maxX,
              lineTouchData: LineTouchData(enabled: false),
              lineBarsData: [
                LineChartBarData(
                  spots: /* DATA HERE */,
                  isCurved: false,
                  barWidth: 2,
                  color: line2Color,
                  dotData: FlDotData(
                    show: false,
                  ),
                ),
              ],
              minY: 1,
              borderData: FlBorderData(
                show: false,
              ),
            ),
          );
        },
      ),
    );

Working demo

https://user-images.githubusercontent.com/13321167/216432341-303f1f58-62e4-4932-91cb-44ec3a6e49eb.mov

imaNNeo commented 1 year ago

I improved over @Abhilash-Chandran solution. I made a wrapper widget for charts that use the minX and maxX parameters. It works great on mobile, supports double tap, pinch zoom and drag. Works only on the X axis (because that's what I need) but it should be easy to extend.

... Screen.Recording.2023-02-02.at.16.36.18.mov

It is a performant solution. Because by putting it inside a ScrollView, it draws the whole chart but you see a portion of it. But in your solution, it just renders what you see.

aguilanbon commented 1 year ago

I improved over @Abhilash-Chandran solution. I made a wrapper widget for charts that use the minX and maxX parameters. It works great on mobile, supports double tap, pinch zoom and drag. Works only on the X axis (because that's what I need) but it should be easy to extend.

import 'dart:math';

import 'package:flutter/material.dart';

class ZoomableChart extends StatefulWidget {
  ZoomableChart({
    super.key,
    required this.maxX,
    required this.builder,
  });

  double maxX;
  Widget Function(double, double) builder;

  @override
  State<ZoomableChart> createState() => _ZoomableChartState();
}

class _ZoomableChartState extends State<ZoomableChart> {
  late double minX;
  late double maxX;

  late double lastMaxXValue;
  late double lastMinXValue;

  @override
  void initState() {
    super.initState();
    minX = 0;
    maxX = widget.maxX;
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onDoubleTap: () {
        setState(() {
          minX = 0;
          maxX = widget.maxX;
        });
      },
      onHorizontalDragStart: (details) {
        lastMinXValue = minX;
        lastMaxXValue = maxX;
      },
      onHorizontalDragUpdate: (details) {
        var horizontalDistance = details.primaryDelta ?? 0;
        if (horizontalDistance == 0) return;
        print(horizontalDistance);
        var lastMinMaxDistance = max(lastMaxXValue - lastMinXValue, 0.0);

        setState(() {
          minX -= lastMinMaxDistance * 0.005 * horizontalDistance;
          maxX -= lastMinMaxDistance * 0.005 * horizontalDistance;

          if (minX < 0) {
            minX = 0;
            maxX = lastMinMaxDistance;
          }
          if (maxX > widget.maxX) {
            maxX = widget.maxX;
            minX = maxX - lastMinMaxDistance;
          }
          print("$minX, $maxX");
        });
      },
      onScaleStart: (details) {
        lastMinXValue = minX;
        lastMaxXValue = maxX;
      },
      onScaleUpdate: (details) {
        var horizontalScale = details.horizontalScale;
        if (horizontalScale == 0) return;
        print(horizontalScale);
        var lastMinMaxDistance = max(lastMaxXValue - lastMinXValue, 0);
        var newMinMaxDistance = max(lastMinMaxDistance / horizontalScale, 10);
        var distanceDifference = newMinMaxDistance - lastMinMaxDistance;
        print("$lastMinMaxDistance, $newMinMaxDistance, $distanceDifference");
        setState(() {
          final newMinX = max(
            lastMinXValue - distanceDifference,
            0.0,
          );
          final newMaxX = min(
            lastMaxXValue + distanceDifference,
            widget.maxX,
          );

          if (newMaxX - newMinX > 2) {
            minX = newMinX;
            maxX = newMaxX;
          }
          print("$minX, $maxX");
        });
      },
      child: widget.builder(minX, maxX),
    );
  }
}

Copy and paste that code in a file like zoomable_chart.dart. Then wrap your chart with ZoomableChart, provide a maxX and a function that returns your widget.

Example

return AspectRatio(
      aspectRatio: 16 / 9,
      child: ZoomableChart(
        maxX: /* maxX OF YOUR DATA HERE */,
        builder: (minX, maxX) {
          return LineChart(
            LineChartData(
              clipData: FlClipData.all(),
              minX: minX,
              maxX: maxX,
              lineTouchData: LineTouchData(enabled: false),
              lineBarsData: [
                LineChartBarData(
                  spots: /* DATA HERE */,
                  isCurved: false,
                  barWidth: 2,
                  color: line2Color,
                  dotData: FlDotData(
                    show: false,
                  ),
                ),
              ],
              minY: 1,
              borderData: FlBorderData(
                show: false,
              ),
            ),
          );
        },
      ),
    );

Working demo

Screen.Recording.2023-02-02.at.16.36.18.mov

does this only work on mobile? do you have any samples for web?

TeoVogel commented 1 year ago

@aguilanbon I've only tested it on mobile, but it should work on web too. Note that I removed the Listener widget class that's present on the original solution from @Abhilash-Chandran . This widget listens to PointerScrollEvent, so my solution does not respond to the scroll wheel. You can extend my solution, adding back that Listener and performing the same work done on onHorizontalDragUpdate for scrolling through the data on the X axis

stukdev commented 1 year ago

@TeoVogel can you post a full example? I'm trying use your solution, but i've some trouble. Thanks

GustavoDuregger commented 1 year ago

I achieved a satisfactory result with little effort using a dynamic x-axis, this way I can change the range of the x-axis only with a gesture detection in the graph component.

 @observable
  int xMinValue = 0;

  @action
  void updateXMinValue(int newValue) {
    xMinValue = newValue;
  }

That way you can generate the bars in a specific range

for (int i = controller.xMinValue; i <= indexController; i++) {
      rawBarGroups.add(makeGroupData(
        x: i,
        bar1: widget.chartDataSet[i].bar1));
    }

Finally, you can change this range from a swipe movement in the graph component, one way to do this is using the GestureDetector.

GestureDetector(
      onHorizontalDragEnd: (DragEndDetails details) {
        if (details.primaryVelocity! > 0 && controller.xMinValue > 0) {
          controller.updateXMinValue(controller.xMinValue - 1);
        } else if (details.primaryVelocity! < 0 && widget.chartDataSet.first.index! > controller.xMinValue + 7) {
          controller.updateXMinValue(controller.xMinValue + 1);
        }
      },
      child: BarChart(
        BarChartData( ...

In a graph with a large number of spots, this solution can become a usability problem as navigation tends to be a little slower by using the swipe gesture. In these cases I recommend using a solution closer to a zoom in the graph: https://github.com/imaNNeo/fl_chart/issues/71#issuecomment-1414267612

zoo7314 commented 1 year ago

@GustavoDuregger

I achieved a satisfactory result with little effort using a dynamic x-axis, this way I can change the range of the x-axis only with a gesture detection in the graph component.

 @observable
  int xMinValue = 0;

  @action
  void updateXMinValue(int newValue) {
    xMinValue = newValue;
  }

That way you can generate the bars in a specific range

for (int i = controller.xMinValue; i <= indexController; i++) {
      rawBarGroups.add(makeGroupData(
        x: i,
        bar1: widget.chartDataSet[i].bar1));
    }

Finally, you can change this range from a swipe movement in the graph component, one way to do this is using the GestureDetector.

GestureDetector(
      onHorizontalDragEnd: (DragEndDetails details) {
        if (details.primaryVelocity! > 0 && controller.xMinValue > 0) {
          controller.updateXMinValue(controller.xMinValue - 1);
        } else if (details.primaryVelocity! < 0 && widget.chartDataSet.first.index! > controller.xMinValue + 7) {
          controller.updateXMinValue(controller.xMinValue + 1);
        }
      },
      child: BarChart(
        BarChartData( ...

In a graph with a large number of spots, this solution can become a usability problem as navigation tends to be a little slower by using the swipe gesture. In these cases I recommend using a solution closer to a zoom in the graph: #71 (comment)

but no minX and maxX in BarChartData

rydwan10 commented 1 year ago

Hello everyone, any update on this issue?

imaNNeo commented 1 year ago

Hi, unfortunately, I couldn't find enough free time to do that yet. Please stay tuned, you can also be my sponsor to motivate me, and then I can put more time into this project.

aditya113141 commented 1 year ago

I Wrapped my Barchart inside a SizedBox and made the sizedbox horizontally scorllable using SingleChildScrollView. The code looks like this :- Screenshot (69)

The width of sizedbox is horizontalLength, and its value is set dynamically as (datasize+2) * (blankSpaceWidth + barWidth).

blankSpaceWidth is the groupsSpace between two consecutive groups of bars and barWidth is the width of each bar. DataSize is basically the number of datapoints or number of bar groups. I have taken datasize+2 here, so as to provide adequate empty space before first bar and after last bar. The final output looks like this :-

https://github.com/imaNNeo/fl_chart/assets/56213335/3f443b8a-168f-42b3-991b-ebe9d2dc434e

aditya113141 commented 1 year ago

I Wrapped my Barchart inside a SizedBox and made the sizedbox horizontally scorllable using SingleChildScrollView. The code looks like this :- Screenshot (69)

The width of sizedbox is horizontalLength, and its value is set dynamically as (datasize+2) * (blankSpaceWidth + barWidth).

blankSpaceWidth is the groupsSpace between two consecutive groups of bars and barWidth is the width of each bar. DataSize is basically the number of datapoints or number of bar groups. I have taken datasize+2 here, so as to provide adequate empty space before first bar and after last bar. The final output looks like this :-

Flutter.Demo.-.Profile.1.-.Microsoft.Edge.2023-09-17.13-11-24.mp4

Achieved similar results with LineChart.

Here, I have set the horizontalLength as (datasize+2) * 50

https://github.com/imaNNeo/fl_chart/assets/56213335/14a5c153-7350-4d3b-8cd9-d0154952e2d7

JuYiYang commented 11 months ago

The owner of this bag has no time and needs money

A glance at the comments section reveals two main options

plan 1 . Give yourself a large SizedBox and wrap it externally with SingleChildScrollView plan 2 . Change the source code of the package yourself You are welcome to add plan

jpgtzg commented 11 months ago

Yeah those are pretty much the solutions. Maybe we can create a pull request with these solutions

itaishalom commented 11 months ago

I improved over @Abhilash-Chandran solution. I made a wrapper widget for charts that use the minX and maxX parameters. It works great on mobile, supports double tap, pinch zoom and drag. Works only on the X axis (because that's what I need) but it should be easy to extend.

import 'dart:math';

import 'package:flutter/material.dart';

class ZoomableChart extends StatefulWidget {
  ZoomableChart({
    super.key,
    required this.maxX,
    required this.builder,
  });

  double maxX;
  Widget Function(double, double) builder;

  @override
  State<ZoomableChart> createState() => _ZoomableChartState();
}

class _ZoomableChartState extends State<ZoomableChart> {
  late double minX;
  late double maxX;

  late double lastMaxXValue;
  late double lastMinXValue;

  @override
  void initState() {
    super.initState();
    minX = 0;
    maxX = widget.maxX;
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onDoubleTap: () {
        setState(() {
          minX = 0;
          maxX = widget.maxX;
        });
      },
      onHorizontalDragStart: (details) {
        lastMinXValue = minX;
        lastMaxXValue = maxX;
      },
      onHorizontalDragUpdate: (details) {
        var horizontalDistance = details.primaryDelta ?? 0;
        if (horizontalDistance == 0) return;
        print(horizontalDistance);
        var lastMinMaxDistance = max(lastMaxXValue - lastMinXValue, 0.0);

        setState(() {
          minX -= lastMinMaxDistance * 0.005 * horizontalDistance;
          maxX -= lastMinMaxDistance * 0.005 * horizontalDistance;

          if (minX < 0) {
            minX = 0;
            maxX = lastMinMaxDistance;
          }
          if (maxX > widget.maxX) {
            maxX = widget.maxX;
            minX = maxX - lastMinMaxDistance;
          }
          print("$minX, $maxX");
        });
      },
      onScaleStart: (details) {
        lastMinXValue = minX;
        lastMaxXValue = maxX;
      },
      onScaleUpdate: (details) {
        var horizontalScale = details.horizontalScale;
        if (horizontalScale == 0) return;
        print(horizontalScale);
        var lastMinMaxDistance = max(lastMaxXValue - lastMinXValue, 0);
        var newMinMaxDistance = max(lastMinMaxDistance / horizontalScale, 10);
        var distanceDifference = newMinMaxDistance - lastMinMaxDistance;
        print("$lastMinMaxDistance, $newMinMaxDistance, $distanceDifference");
        setState(() {
          final newMinX = max(
            lastMinXValue - distanceDifference,
            0.0,
          );
          final newMaxX = min(
            lastMaxXValue + distanceDifference,
            widget.maxX,
          );

          if (newMaxX - newMinX > 2) {
            minX = newMinX;
            maxX = newMaxX;
          }
          print("$minX, $maxX");
        });
      },
      child: widget.builder(minX, maxX),
    );
  }
}

Copy and paste that code in a file like zoomable_chart.dart. Then wrap your chart with ZoomableChart, provide a maxX and a function that returns your widget.

Example

return AspectRatio(
      aspectRatio: 16 / 9,
      child: ZoomableChart(
        maxX: /* maxX OF YOUR DATA HERE */,
        builder: (minX, maxX) {
          return LineChart(
            LineChartData(
              clipData: FlClipData.all(),
              minX: minX,
              maxX: maxX,
              lineTouchData: LineTouchData(enabled: false),
              lineBarsData: [
                LineChartBarData(
                  spots: /* DATA HERE */,
                  isCurved: false,
                  barWidth: 2,
                  color: line2Color,
                  dotData: FlDotData(
                    show: false,
                  ),
                ),
              ],
              minY: 1,
              borderData: FlBorderData(
                show: false,
              ),
            ),
          );
        },
      ),
    );

Working demo

Screen.Recording.2023-02-02.at.16.36.18.mov

How did you achieve that the x axis label changes depended on the zoom level? Also you loose the touch event to see the values

austibwu commented 8 months ago

I improved over @Abhilash-Chandran solution. I made a wrapper widget for charts that use the minX and maxX parameters. It works great on mobile, supports double tap, pinch zoom and drag. Works only on the X axis (because that's what I need) but it should be easy to extend.

Have also modified @TeoVogel 's solution a bit so that pinch/trackpad zoom will track from the location you pinch, rather than from center of graph. I was too lazy to remove my double tap feature, which zooms to an x axis width of ± 50 from the point on the graph that was clicked. maybe that will be useful to someone too : ) confirmed works on iOS and macOS.

note the 65 offset in my code, which is specific to the margin and padding I applied to my chart. The commented out onTapDown function at the bottom can help you find your offset.

class ZoomableChart extends StatefulWidget {
  ZoomableChart({
    super.key,
    required this.maxX,
    required this.builder,
    this.touchpadScroll = false,
  });

  double maxX;
  bool touchpadScroll;
  Widget Function(double, double) builder;

  @override
  State<ZoomableChart> createState() => _ZoomableChartState();
}

class _ZoomableChartState extends State<ZoomableChart> {
  late double minX;
  late double maxX;

  late double lastMaxXValue;
  late double lastMinXValue;

  double focalPoint = -1;

  bool isZoomed = false;

  late RenderBox renderBox;
  late double chartW;
  late Offset position;
  late double currPosition;

  @override
  void initState() {
    super.initState();
    minX = 0;
    maxX = widget.maxX;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      renderBox = context.findRenderObject() as RenderBox;
      chartW = renderBox.size.width;
      position = renderBox.localToGlobal(Offset.zero);
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onDoubleTapDown: (details) {
        setState(() {
          if (isZoomed) {
            minX = 0;
            maxX = widget.maxX;
          } else {
            renderBox = context.findRenderObject() as RenderBox;
            chartW = renderBox.size.width - 65; // <-- you will need to figure out the offset from the edge of this widget to actual graph.
            position = renderBox.localToGlobal(Offset.zero);
            currPosition = details.localPosition.dx - 65; // <-----
            double currPositionX = (currPosition / chartW)*(maxX - minX) + minX;
            minX = currPositionX - 50;
            maxX = currPositionX + 50;
          }

          isZoomed = !isZoomed;
        });
      },
      trackpadScrollToScaleFactor: kDefaultTrackpadScrollToScaleFactor,
      onHorizontalDragStart: (details) {
        lastMinXValue = minX;
        lastMaxXValue = maxX;
      },
      onHorizontalDragUpdate: (details) {
        var horizontalDistance = details.primaryDelta ?? 0;
        if (horizontalDistance == 0) return;
        // print('distance : $horizontalDistance');
        var lastMinMaxDistance = max(lastMaxXValue - lastMinXValue, 0.0);
        // print(lastMinMaxDistance);
        setState(() {
          minX -= lastMinMaxDistance * 0.003 * horizontalDistance;
          maxX -= lastMinMaxDistance * 0.003 * horizontalDistance;

          if (minX <= 0) {
            minX = 0;
            maxX = lastMinMaxDistance;
          }
          if (maxX > widget.maxX) {
            maxX = widget.maxX;
            minX = maxX - lastMinMaxDistance;
          }
          // print("hordrag update x: $minX, $maxX");
        });
      },
      onScaleStart: (details) {
        lastMinXValue = minX;
        lastMaxXValue = maxX;

        renderBox = context.findRenderObject() as RenderBox;
        chartW = renderBox.size.width - 65;          // <-- you will need to figure out the offset from the edge of this widget to actual graph.
        position = renderBox.localToGlobal(Offset.zero);
        currPosition = details.localFocalPoint.dx - 65;  // <-----
      },
      onScaleUpdate: (details) {
        double leftUpdateFactor = currPosition / chartW;
        double rightUpdateFactor = 1 - leftUpdateFactor;
        var horizontalScale = details.horizontalScale;
        if (horizontalScale == 0) return;
        // print(horizontalScale);
        var lastMinMaxDistance = max(lastMaxXValue - lastMinXValue, 0);
        var newMinMaxDistance = max(lastMinMaxDistance / horizontalScale, 10);
        var distanceDifference = newMinMaxDistance - lastMinMaxDistance;
        // print("sss $lastMinMaxDistance, $newMinMaxDistance, $distanceDifference");
        setState(() {
          focalPoint = lastMinXValue + leftUpdateFactor * lastMinMaxDistance;
          final newMinX = max(
            lastMinXValue - distanceDifference * leftUpdateFactor,
            0.0,
          );
          final newMaxX = min(
            lastMaxXValue + distanceDifference * rightUpdateFactor,
            widget.maxX,
          );

          if (newMaxX - newMinX > 2) {
            minX = newMinX;
            maxX = newMaxX;
          }
          // print("window X: $minX, $maxX");
        });
      },
      onScaleEnd: (details) {
        // print('scale ended');
        setState(() {});
      },
      // onTapDown: (details) {
      // print(details);
      // print(chartW);
      // print('local clicked position: ${details.localPosition.dx}');
      // },
      child: widget.builder(minX, maxX),
    );
  }
}
juffis commented 8 months ago

I improved over @Abhilash-Chandran solution. I made a wrapper widget for charts that use the minX and maxX parameters. It works great on mobile, supports double tap, pinch zoom and drag. Works only on the X axis (because that's what I need) but it should be easy to extend.

Have also modified @TeoVogel 's solution a bit so that pinch/trackpad zoom will track from the location you pinch, rather than from center of graph. I was too lazy to remove my double tap feature, which zooms to an x axis width of ± 50 from the point on the graph that was clicked. maybe that will be useful to someone too : ) confirmed works on iOS and macOS.

note the 65 offset in my code, which is specific to the margin and padding I applied to my chart. The commented out onTapDown function at the bottom can help you find your offset.

class ZoomableChart extends StatefulWidget {
  ZoomableChart({
    super.key,
    required this.maxX,
    required this.builder,
    this.touchpadScroll = false,
  });

  double maxX;
  bool touchpadScroll;
  Widget Function(double, double) builder;

  @override
  State<ZoomableChart> createState() => _ZoomableChartState();
}

class _ZoomableChartState extends State<ZoomableChart> {
  late double minX;
  late double maxX;

  late double lastMaxXValue;
  late double lastMinXValue;

  double focalPoint = -1;

  bool isZoomed = false;

  late RenderBox renderBox;
  late double chartW;
  late Offset position;
  late double currPosition;

  @override
  void initState() {
    super.initState();
    minX = 0;
    maxX = widget.maxX;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      renderBox = context.findRenderObject() as RenderBox;
      chartW = renderBox.size.width;
      position = renderBox.localToGlobal(Offset.zero);
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onDoubleTapDown: (details) {
        setState(() {
          if (isZoomed) {
            minX = 0;
            maxX = widget.maxX;
          } else {
            renderBox = context.findRenderObject() as RenderBox;
            chartW = renderBox.size.width - 65; // <-- you will need to figure out the offset from the edge of this widget to actual graph.
            position = renderBox.localToGlobal(Offset.zero);
            currPosition = details.localPosition.dx - 65; // <-----
            double currPositionX = (currPosition / chartW)*(maxX - minX) + minX;
            minX = currPositionX - 50;
            maxX = currPositionX + 50;
          }

          isZoomed = !isZoomed;
        });
      },
      trackpadScrollToScaleFactor: kDefaultTrackpadScrollToScaleFactor,
      onHorizontalDragStart: (details) {
        lastMinXValue = minX;
        lastMaxXValue = maxX;
      },
      onHorizontalDragUpdate: (details) {
        var horizontalDistance = details.primaryDelta ?? 0;
        if (horizontalDistance == 0) return;
        // print('distance : $horizontalDistance');
        var lastMinMaxDistance = max(lastMaxXValue - lastMinXValue, 0.0);
        // print(lastMinMaxDistance);
        setState(() {
          minX -= lastMinMaxDistance * 0.003 * horizontalDistance;
          maxX -= lastMinMaxDistance * 0.003 * horizontalDistance;

          if (minX <= 0) {
            minX = 0;
            maxX = lastMinMaxDistance;
          }
          if (maxX > widget.maxX) {
            maxX = widget.maxX;
            minX = maxX - lastMinMaxDistance;
          }
          // print("hordrag update x: $minX, $maxX");
        });
      },
      onScaleStart: (details) {
        lastMinXValue = minX;
        lastMaxXValue = maxX;

        renderBox = context.findRenderObject() as RenderBox;
        chartW = renderBox.size.width - 65;          // <-- you will need to figure out the offset from the edge of this widget to actual graph.
        position = renderBox.localToGlobal(Offset.zero);
        currPosition = details.localFocalPoint.dx - 65;  // <-----
      },
      onScaleUpdate: (details) {
        double leftUpdateFactor = currPosition / chartW;
        double rightUpdateFactor = 1 - leftUpdateFactor;
        var horizontalScale = details.horizontalScale;
        if (horizontalScale == 0) return;
        // print(horizontalScale);
        var lastMinMaxDistance = max(lastMaxXValue - lastMinXValue, 0);
        var newMinMaxDistance = max(lastMinMaxDistance / horizontalScale, 10);
        var distanceDifference = newMinMaxDistance - lastMinMaxDistance;
        // print("sss $lastMinMaxDistance, $newMinMaxDistance, $distanceDifference");
        setState(() {
          focalPoint = lastMinXValue + leftUpdateFactor * lastMinMaxDistance;
          final newMinX = max(
            lastMinXValue - distanceDifference * leftUpdateFactor,
            0.0,
          );
          final newMaxX = min(
            lastMaxXValue + distanceDifference * rightUpdateFactor,
            widget.maxX,
          );

          if (newMaxX - newMinX > 2) {
            minX = newMinX;
            maxX = newMaxX;
          }
          // print("window X: $minX, $maxX");
        });
      },
      onScaleEnd: (details) {
        // print('scale ended');
        setState(() {});
      },
      // onTapDown: (details) {
      // print(details);
      // print(chartW);
      // print('local clicked position: ${details.localPosition.dx}');
      // },
      child: widget.builder(minX, maxX),
    );
  }
}

I cannot get this to work. Is there a minimum amount of spots/datapoints needed for zoom to work? I wrapped my function which returns LineChartData with ZoomableChart. I'm unsure what kind of value I should use with maxX.

EDIT: I got it to work, but I don't understand how to configure the max zoom level. I got 11 points and the "closest" I can zoom to still has all of them in view. I can zoom out a lot.

Still trying to figure out how to display the datapoints' data when the lineTouchData is disabled. If I enable it the zoomable chart doesn't work but at least I can view the labels.

mikaelzero commented 7 months ago

If anyone wants to achieve panning and mousewheel zooming, following code might help. I have tested the following in flutter for windows and browser. In windows it works well and satisfies my needs. In browser however the panning is not good because of DragupdateDetails.primaryDelta comes in as 0 quite often and hence the panning is jittery.

Note this logic still has flaws like minX and maxX not being clamped to stop zooming etc. However I feel this is good start.

@imaNNeoFighT I am not sure if this is a performant way, but seems to achieve some results. Atleast in windows I didn't feel any jitter. 😄

Idea is as follows.

  1. Use a Listener widget to listen to mouse scroll events.

    • Increment and decrement minx and maxx by a fixed percentage of maxX, depending on the scroll direction.
  2. Use a GestureDetector widget to detect horizontal drag event.
  • decrement both minX and maxX by a percentage if panning to the left. that is if primary delta is negative.
  • Increment both minX and maxX by a percentage if panning to the right. that is if primary delta is positive.
  1. Finally clip the plot to be within the bounds using the clipData: FlClipData.all() of the LineChartData. without this the plot is rendered outside the widget.

cnLXXH8TLX cnLXXH8TLX

Following example achieves panning and zooming only in x-axis. However the logic can be extended to yaxis as well.

import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

class PlotData {
  List<double> result;
  double maxY;
  double minY;
  PlotData({
    required this.result,
    required this.maxY,
    required this.minY,
  });
}

class LinePlot extends StatefulWidget {
  final PlotData plotData;
  const LinePlot({
    required this.plotData,
    Key? key,
  }) : super(key: key);

  @override
  _LinePlotState createState() => _LinePlotState();
}

class _LinePlotState extends State<LinePlot> {
  late double minX;
  late double maxX;
  @override
  void initState() {
    super.initState();
    minX = 0;
    maxX = widget.plotData.result.length.toDouble();
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerSignal: (signal) {
        if (signal is PointerScrollEvent) {
          setState(() {
            if (signal.scrollDelta.dy.isNegative) {
              minX += maxX * 0.05;
              maxX -= maxX * 0.05;
            } else {
              minX -= maxX * 0.05;
              maxX += maxX * 0.05;
            }
          });
        }
      },
      child: GestureDetector(
        onDoubleTap: () {
          setState(() {
            minX = 0;
            maxX = widget.plotData.result.length.toDouble();
          });
        },
        onHorizontalDragUpdate: (dragUpdDet) {
          setState(() {
            print(dragUpdDet.primaryDelta);
            double primDelta = dragUpdDet.primaryDelta ?? 0.0;
            if (primDelta != 0) {
              if (primDelta.isNegative) {
                minX += maxX * 0.005;
                maxX += maxX * 0.005;
              } else {
                minX -= maxX * 0.005;
                maxX -= maxX * 0.005;
              }
            }
          });
        },
        child: LineChart(
          LineChartData(
            minX: minX,
            maxX: maxX,
            maxY: widget.plotData.maxY + widget.plotData.maxY * 0.1,
            titlesData: FlTitlesData(
              bottomTitles: SideTitles(
                showTitles: true,
                interval: widget.plotData.result.length / 10,
              ),
              leftTitles: SideTitles(
                showTitles: true,
                margin: 5,
              ),
              topTitles: SideTitles(
                showTitles: false,
                margin: 5,
              ),
            ),
            gridData: FlGridData(
              drawHorizontalLine: false,
            ),
            clipData: FlClipData.all(),
            lineBarsData: [
              LineChartBarData(
                barWidth: 1,
                dotData: FlDotData(
                  show: false,
                ),
                spots: widget.plotData.result
                    .asMap()
                    .entries
                    .map((entry) => FlSpot(entry.key.toDouble(), entry.value))
                    .toList(),
              )
            ],
          ),
        ),
      ),
    );
  }
}

i create XAxisScrollableChart base on this, just support x axis scroll

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

typedef XAxisRangeWidgetBuilder = Widget Function(double minX, double maxX);

class XAxisScrollableChart extends StatefulWidget {
  final double minX;
  final double maxX;
  final int visibleXSize;
  final XAxisRangeWidgetBuilder itemBuilder;
  final double xScrollOffset;
  const XAxisScrollableChart({
    required this.minX,
    required this.maxX,
    required this.itemBuilder,
    this.visibleXSize = 5,
    this.xScrollOffset = 0.01,
    Key? key,
  }) : super(key: key);

  @override
  State<XAxisScrollableChart> createState() => _LinePlotState();
}

class _LinePlotState extends State<XAxisScrollableChart> {
  late double minX;
  late double maxX;
  late double xRange;
  late double currentMinX;
  late double currentMaxX;
  late int visibleRange;
  @override
  void initState() {
    super.initState();
    _handleX();
  }

  @override
  void didUpdateWidget(covariant XAxisScrollableChart oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.minX != oldWidget.minX || widget.maxX != oldWidget.maxX) {
      setState(() {
        _handleX();
      });
    }
  }

  void _handleX() {
    visibleRange = widget.visibleXSize - 1;
    minX = widget.minX;
    maxX = widget.maxX;
    xRange = maxX -= minX;
    currentMaxX = maxX;
    currentMinX = maxX - visibleRange;
    if (currentMinX < minX) {
      currentMinX = minX;
      currentMaxX = minX + visibleRange;
    }
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onHorizontalDragUpdate: (dragUpdDet) {
        double primaryDelta = dragUpdDet.primaryDelta ?? 0.0;
        double tempMinX = currentMinX;
        double tempMaxX = currentMaxX;
        if (primaryDelta != 0) {
          if (primaryDelta.isNegative) {
            tempMinX += xRange * widget.xScrollOffset;
            tempMaxX += xRange * widget.xScrollOffset;
          } else {
            tempMinX -= xRange * widget.xScrollOffset;
            tempMaxX -= xRange * widget.xScrollOffset;
          }
        }
        if (tempMinX < minX) {
          setState(() {
            currentMinX = minX;
          });
          return;
        }
        if (tempMaxX > maxX) {
          setState(() {
            currentMaxX = maxX;
          });
          return;
        }
        setState(() {
          currentMinX = tempMinX;
          currentMaxX = tempMaxX;
        });
      },
      child: widget.itemBuilder(currentMinX, currentMaxX),
    );
  }
}

usage

XAxisScrollableChart(
                minX: 0,
                maxX: spotsData.isEmpty ? 0 : (spotsData.length.toDouble() - 1),
                itemBuilder: (double minX, double maxX) {
return YourLineChart;
                },
            ),