Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
nimesh1997 opened this issue Sep 28, 2019 · 79 comments
Labels
enhancement New feature or request Fundamental

Comments

@nimesh1997
Copy link

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

@nimesh1997 nimesh1997 changed the title How to make scrollable x axis in flchart so that it looks nice not compact? How to make scrollable x axis in flchart so that it looks nice not so compact in nature? Sep 28, 2019
@nimesh1997 nimesh1997 reopened this Sep 28, 2019
@imaNNeo
Copy link
Owner

imaNNeo commented Sep 28, 2019

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 imaNNeo closed this as completed Sep 28, 2019
@imaNNeo imaNNeo reopened this Sep 28, 2019
@imaNNeo
Copy link
Owner

imaNNeo commented Sep 28, 2019

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

@imaNNeo imaNNeo added the enhancement New feature or request label Sep 28, 2019
@ZantsuRocks
Copy link

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

@imaNNeo
Copy link
Owner

imaNNeo commented Sep 30, 2019

@ZantsuRocks Greate solution, Thank you :)

@davicg
Copy link

davicg commented Oct 24, 2019

@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
Copy link

@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
Copy link

@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
Copy link

Abhilash-Chandran commented Mar 28, 2021

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(),
              )
            ],
          ),
        ),
      ),
    );
  }
}

@jlubeck
Copy link
Contributor

jlubeck commented Jun 18, 2021

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

@imaNNeo
Copy link
Owner

imaNNeo commented Jul 2, 2021

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.

@wuao
Copy link

wuao commented Jul 31, 2024

hey. gays! It's already 2024, why don't fix this issue

@Tobbyte
Copy link

Tobbyte commented Jul 31, 2024

@wuao kind of tall order for a one-man open source project, hm?.. see my post just before (!) yours for a solid workaround

@wuao

This comment was marked as spam.

@AlexanderMonneret
Copy link

One of the biggest flaws of Flutter is the lack of official support (monetary) to such great plugins
I'm going to start a Flutter Fund to support libraries like this
Until then to speed up things and to make the whole community benefit from the solution
I'm donating 100$ to who ever fix this issue
Pinch zoom in & out,
Horizonal scroll
Mouse scroll for desktop
No hacks, compete integration

Let me know when the PR is ready
Cheers!

@wuao

This comment was marked as spam.

@NeariX67
Copy link

I'm donating 100$ to who ever fix this issue

Willing to add another 100€/$ into the pool.

@imaNNeo Interested in doing this for the sake of code quality?

@Peetee06
Copy link
Contributor

@imaNNeo I'll contribute zoom and horizontal scrolling if you don't mind. Haven't used fl_chart before so it'll take some time to understand and then find a solution.

@wuao

This comment was marked as spam.

@imaNNeo
Copy link
Owner

imaNNeo commented Nov 26, 2024

One of the biggest flaws of Flutter is the lack of official support (monetary) to such great plugins I'm going to start a Flutter Fund to support libraries like this Until then to speed up things and to make the whole community benefit from the solution I'm donating 100$ to who ever fix this issue Pinch zoom in & out, Horizonal scroll Mouse scroll for desktop No hacks, compete integration

Let me know when the PR is ready Cheers!
Willing to add another 100€/$ into the pool.

@imaNNeo Interested in doing this for the sake of code quality?

I'm delighted to see these kinds of effective messages. It is something that helps the open-source community thrive.
As you know I have my personal life, full-time job, and side projects, ... and I'm not committed to working full-time here (as it doesn't have any income compared to my full-time job). So these kind of supports help and I appreciate it.

@imaNNeo I'll contribute zoom and horizontal scrolling if you don't mind. Haven't used fl_chart before so it'll take some time to understand and then find a solution.

@Peetee06 I would appreciate it if you do this. My estimation about this issue is around 24-30 hours. So if you think you can do this from zero to 100, you can start.
And this is my guide for you to start:

  • Read the contributing file to understand what's happening and how to contribute
  • There are two approaches in my head for this issue:
    1. Expand the chart's width (using a scale rate or something like that), then wrap the chart widget with a SingleChildScrollView or something like that to show a part of the chart. So this way, we can scroll horizontally and scale the chart. It is not the most efficient solution, but it works. I've made some changes in this branch, you can take a look.
    2. Limiting the draw functionality using the minX, maxX, minY, and maxY, so this way seems more complicated as we already have these properties (user can customize them). We need to define something internal that works like these numbers that allows us to limit the drawing part. Then we need to listen to the scroll gestures (such as pan and pinch) to change those values, for this one, we can listen to the gestures here in our renderers. So this approach makes more sense to me, but it is more complicated.
    3. Or any other solutions that you might have, you can keep me updated on your PR, so these days I will keep my focus on your PR until it's finished.
  • Implement a new sample in your LineChart samples that uses the scroll features (as a showcase)
  • Write the documentation in our line_chart.md
  • Write some unit-tests

I don't want to disappoint you, I just want to let you know how much effort you need to put in.
So I will write it down here that you get $200 in total

@Peetee06
Copy link
Contributor

@imaNNeo thank you so much for the summary! That will allow me to save some time to figure out possible solutions.

The time estimate looks fine with me, I'll work on it until I got a good solution working.

Thanks for prioritizing the PR. Will keep you updated.

@imaNNeo
Copy link
Owner

imaNNeo commented Nov 26, 2024

@imaNNeo thank you so much for the summary! That will allow me to save some time to figure out possible solutions.

The time estimate looks fine with me, I'll work on it until I got a good solution working.

Thanks for prioritizing the PR. Will keep you updated.

Sounds good!

@imaNNeo
Copy link
Owner

imaNNeo commented Dec 17, 2024

I just got excited by the zoom/scroll feature that @Peetee06 has implemented (It's almost done, but needs some final improvements, you can follow it up here: #1793)
So I wanted to share a demo with you guys

CleanShot.2024-12-17.at.23.33.22.mp4

@wuao

This comment was marked as spam.

@imaNNeo
Copy link
Owner

imaNNeo commented Dec 19, 2024

Scroll/Zoom Feature is Here! 🎉

I'm happy to announce that fl_chart now supports the scroll/zoom feature in 0.70.0!

Special thanks to @Peetee06, who made it happen (from 0 to 100)!
You can check it out in LineChartSample12.

Also, a huge thanks to the sponsors (@AlexanderMonneret and @NeariX67) for adding some motivation. Please contact @Peetee06 to transfer the donation. (I know it’s not much, but it’s a reward!)

Thanks for waiting 5 years—this was something I really wanted to implement, but it’s super complicated. @Peetee06 can tell you how challenging it was.

Enjoy the new feature, and let us know what you think! 🚀

I will close this issue. But you can always create a new issue to report your bugs or feature requests.

@imaNNeo imaNNeo closed this as completed Dec 19, 2024
@ozzy1873
Copy link

Do you really need flutter >=3.27.0 ?

@imaNNeo
Copy link
Owner

imaNNeo commented Dec 19, 2024

Do you really need flutter >=3.27.0 ?

We did it in #1805 (you can read more about it)
The point is that 3.27.0 has a breaking change and it also affected our pub scoring (which works with the latest version of the flutter and dart)

@desmeit
Copy link

desmeit commented Dec 19, 2024

perfect! Great work!
Is it possible to load lazy? If we load for example dynamically content it would be nice to load month by month or similar and not all content with one request.
Is there a possibility?

@GanZhiXiong
Copy link

对于这个问题,我脑子里有两种方法:
扩展图表的宽度(使用缩放率或类似的东西),然后用或类似的东西包裹图表小部件SingleChildScrollView来显示图表的一部分。这样,我们就可以水平滚动并缩放图表。这不是最有效的解决方案,但它有效。我https://github.com/imaNNeo/fl_chart/pull/1507做了一些更改,你可以看看。
minX使用、maxX和minY来限制功能maxY,方式似乎更复杂,因为我们已经拥有这些属性(用户可以自定义它们)。我们需要定义一些像这些数字一样的内部功能,以便我们限制部分。然后我们需要通过中继放大器(例如平移和捏合)来更改这些值,为此,我们可以在渲染器中监听这些中继器所以这种方法对我来说更有意义,但也更复杂。
或者您可能有任何其他解决方案,您可以让我了解您的公关的最新情况,因此这些天我会一直关注您的公关,直到它完成。

@imaNNeo
I'm glad to hear that scrolling and zooming are finally supported. We've all been waiting for this for a long time.
I'd like to know some implementation details:

  1. What method was used to implement it?
  2. SingleChildScrollView should be simpler to implement. Why not use SingleChildScrollView?
  3. The App in the App Store does not support LineChartSample12 yet. When will it be updated?

@Peetee06
Copy link
Contributor

Peetee06 commented Dec 20, 2024

@desmeit with a little bit of custom work you can surely make this happen.

This can work by:

  1. Supplying your own TransformationController to the chart via the FlTransformationConfig
  2. Pass the chartRendererKey to the chart
  3. Add a listener to that controller.

In the listener you do the following:

  1. Get the current size of the chart via the chartRendererKey
  2. Do the same transformation on the chart widget's size as we do here
    // Applies the inverse transformation to the chart to get the zoomed
    // bounding box.
    //
    // The transformation matrix is inverted because the bounding box needs to
    // grow beyond the chart's boundaries when the chart is scaled in order
    // for its content to be laid out on the larger area. This leads to the
    // scaling effect.
    void _updateChartVirtualRect() {
    final scale = _transformationController.value.getMaxScaleOnAxis();
    if (scale == 1.0) {
    setState(() {
    _chartVirtualRect = null;
    });
    return;
    }
    final inverseMatrix = Matrix4.inverted(_transformationController.value);
    final chartVirtualQuad = CustomInteractiveViewer.transformViewport(
    inverseMatrix,
    _chartBoundaryRect,
    );
    final chartVirtualRect = CustomInteractiveViewer.axisAlignedBoundingBox(
    chartVirtualQuad,
    );
    final adjustedRect = Rect.fromLTWH(
    _canScaleHorizontally ? chartVirtualRect.left : _chartBoundaryRect.left,
    _canScaleVertically ? chartVirtualRect.top : _chartBoundaryRect.top,
    _canScaleHorizontally ? chartVirtualRect.width : _chartBoundaryRect.width,
    _canScaleVertically ? chartVirtualRect.height : _chartBoundaryRect.height,
    );
    setState(() {
    _chartVirtualRect = adjustedRect;
    });
    }
  3. Instead of using "setState" you check the chartVirtualRect.left property

If chartVirtualRect.left is

  • negative and close to 0, that means you are reaching the left edge of the chart and can load previous data
  • close to -chartVirtualRect.width + chart.width that means you are reaching the right edge of the chart and can load following data

Please let me know if this works or you need any more guidance :)

@Peetee06
Copy link
Contributor

@GanZhiXiong Happy to hear that 👍

I can say something about point 1 and 2. @imaNNeo will take care of 3 and can answer that best.

1. Implementation details

We are proxying a custom InteractiveViewer that does not transform its child. We pass a TransformationController in and use the Matrix that is transformed through user interaction to scale and translate the chart widget's size. We then use that transformed size to calculate the layout of the data. Lastly, we paint that data onto the canvas and clip anything outside of it.

You can start in AxisChartScaffoldWidget and go deeper from there. The interesting parts are the chart renderers like bar_chart_renderer.dart and the painters like bar_chart_painter.dart.

2. Why not SingleChildScrollView?

While SingelChildScrollView would have been easier to implement, it would also be less powerful. It would only be possible to scale/pan vertically OR horizontally but not both at the same time.

@nateshmbhat
Copy link

Awesome work guys ! @Peetee06 @imaNNeo
thanks so much for this ! ❤️

@eljorgit
Copy link

eljorgit commented Jan 5, 2025

Hi @Peetee06 @imaNNeo!
Thanks for this new feature, I have just implemented on my app.
But I have one question, maybe you can answer me.
Right now, the chart I have in the app shows up like this: (Link to video)

And I wanted to know if there is any way to automatically scroll the chart to the end of the line, or to the last bar if it were a bar chart....

Thanks in advance!

@wuao
Copy link

wuao commented Jan 5, 2025 via email

@Peetee06
Copy link
Contributor

Peetee06 commented Jan 5, 2025

Hi @eljorgit,

Great to hear you are using the zoom/pan feature!

To set the zoom to a specific position, you can do the following:

  1. Pass a GlobalKey to your chart's chartRendererKey
  2. Use that key to get the chart's size and offset like here:
    Rect get _chartBoundaryRect {
    assert(_chartKey.currentContext != null);
    final childRenderBox =
    _chartKey.currentContext!.findRenderObject()! as RenderBox;
    return Offset.zero & childRenderBox.size;
    }
  3. Create a method that calculates the transformed chart size and offset similar to this one but instead of setting _chartVirtualRect you return the resulting Rect (you might need to copy the CustomInteractiveViewer methods to your code manually, if you can't access them directly):
    void _updateChartVirtualRect() {
    final scale = _transformationController.value.getMaxScaleOnAxis();
    if (scale == 1.0) {
    setState(() {
    _chartVirtualRect = null;
    });
    return;
    }
    final inverseMatrix = Matrix4.inverted(_transformationController.value);
    final chartVirtualQuad = CustomInteractiveViewer.transformViewport(
    inverseMatrix,
    _chartBoundaryRect,
    );
    final chartVirtualRect = CustomInteractiveViewer.axisAlignedBoundingBox(
    chartVirtualQuad,
    );
    final adjustedRect = Rect.fromLTWH(
    _canScaleHorizontally ? chartVirtualRect.left : _chartBoundaryRect.left,
    _canScaleVertically ? chartVirtualRect.top : _chartBoundaryRect.top,
    _canScaleHorizontally ? chartVirtualRect.width : _chartBoundaryRect.width,
    _canScaleVertically ? chartVirtualRect.height : _chartBoundaryRect.height,
    );
    setState(() {
    _chartVirtualRect = adjustedRect;
    });
    }
  4. Use WidgetsBinding.instance.addPostFrameCallback to transform your own TransformationController's matrix value post frame with .translate(-((transformedChart.width - chart.width) / 2)) where transformedChart is the result of your method from step 3 and chart is the result from step 2.

Your matrix needs to be scaled before calculating the transformedChart. Otherwise the resulting Rect will be the same as your chart's Rect and the translation will be 0.

This should result in your chart being scrolled to the end.

Please let me know if you need any more help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request Fundamental
Projects
None yet
Development

No branches or pull requests