flutterdartgraphchartsfl-chart

Flutter fl_chart LineChart erroring out with more than a 4 day data differential


More specifically, the error I'm running into is:

Execution of the command buffer was aborted due to an error during execution. Too much geometry to support memoryless render pass attachments. (0000000d:kIOGPUCommandBufferCallbackErrorOutOfMemoryForParameterBuffer)

Here's a minimal example I've been testing it out on:

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';

class Playground extends HookConsumerWidget {

  @override
  Widget build(BuildContext context, WidgetRef ref) {

    List<Color> gradientColors = [
      Theme.of(context).colorScheme.primary.withOpacity(.5),
      Theme.of(context).colorScheme.primary,
    ];


    List<FlSpot> test_spots = [
      // FlSpot(
      //   Timestamp.fromDate(DateTime.now().subtract(const Duration(days: 4))).millisecondsSinceEpoch.toDouble() / 1000,
      //   167.0
      // )
    ];

    double minx = Timestamp.fromDate(DateTime.now().subtract(const Duration(days: 6))).millisecondsSinceEpoch.toDouble() / 1000;
    double maxx = Timestamp.fromDate(DateTime.now()).millisecondsSinceEpoch.toDouble() / 1000;

    double miny = 150;
    double maxy = 200;

    return Padding(
      padding: const EdgeInsets.fromLTRB(0, 100, 0, 0),
      child: Stack(
      children: <Widget>[
        AspectRatio(
          aspectRatio: 1.70,
          child: Padding(
            padding: const EdgeInsets.only(
              right: 18,
              left: 12,
              top: 24,
              bottom: 12,
            ),
            child: LineChart(
              LineChartData(
                gridData: FlGridData(
                  show: true,
                  drawVerticalLine: true,
                  horizontalInterval: 1,
                  verticalInterval: 1,
                  getDrawingHorizontalLine: (value) {
                    return const FlLine(
                      color: Colors.white,
                      strokeWidth: 1,
                    );
                  },
                  getDrawingVerticalLine: (value) {
                    return const FlLine(
                      color: Colors.white,
                      strokeWidth: 1,
                    );
                  },
                ),
                titlesData: const FlTitlesData(
                  show: true,
                  rightTitles: AxisTitles(
                    sideTitles: SideTitles(showTitles: false),
                  ),
                  topTitles: AxisTitles(
                    sideTitles: SideTitles(showTitles: false),
                  ),
                  bottomTitles: AxisTitles(
                    sideTitles: SideTitles(
                      showTitles: true,
                      reservedSize: 30,
                      getTitlesWidget: bottomTitleWidgets,
                    ),
                  ),
                  leftTitles: AxisTitles(
                    sideTitles: SideTitles(
                      showTitles: true,
                      getTitlesWidget: leftTitleWidgets,
                      reservedSize: 42,
                    ),
                  ),
                ),
                borderData: FlBorderData(
                  show: true,
                  border: Border.all(color: Theme.of(context).colorScheme.tertiary),
                ),
                minX: minx,
                maxX: maxx,
                minY: miny,
                maxY: maxy,
                lineBarsData: [
                  LineChartBarData(
                    spots: test_spots,
                    isCurved: false,
                    gradient: LinearGradient(
                      colors: gradientColors,
                    ),
                    barWidth: 3,
                    isStrokeCapRound: true,
                    dotData: const FlDotData(
                      show: false,
                    ),
                    belowBarData: BarAreaData(
                      show: true,
                      gradient: LinearGradient(
                        colors: gradientColors
                            .map((color) => color.withOpacity(0.3))
                            .toList(),
                      ),
                    ),
                  ),
                ],
              )
            ),
          ),
        ),
      ],
      )
    );
  }
}

Widget bottomTitleWidgets(double value, TitleMeta meta) {
  if(value == meta.min || value == meta.max){
    return const Text("");
  }
  Widget text = Text(
    DateFormat('Md').format(DateTime.fromMillisecondsSinceEpoch((value * 1000).round())), 
    style: TextStyle(
      color: Colors.grey[600],
      fontSize: 10,
    )
  );
  return SideTitleWidget(
    axisSide: meta.axisSide,
    child: text,
  );
}

Widget leftTitleWidgets(double value, TitleMeta meta) {
  return Text(
    value.toInt().toString(), 
    style: TextStyle(
      color: Colors.grey[600],
      fontSize: 15,
    ),
    textAlign: TextAlign.center
  );
}

If I set minx to Timestamp.fromDate(DateTime.now().subtract(const Duration(days: 4))).millisecondsSinceEpoch.toDouble() / 1000; instead (4 days instead of 6), the graph renders just fine. It doesn't always trigger, but hot reload once or twice and it should. I'm also testing on IOS (physical device) if that's relevant.

Any advice on how to resolve this? None of the doc examples feature granular DateTime data.

My best guess is that the LineChart may be rendering too many points on the graph due to the large nature of the DateTime doubles and doesn't do any averaging to reduce the geometric load- e.g. 3 days is 1720744251.983 to 1721003451.983, which lets say generates 300 geometry points (obv way more but humor me), and that then 6 days generates 600 instead of averaging down to 300, and that is the cutoff for a render failure. I'd be surprised if this wasn't accounted for in fl chart, but I'm not sure what else it could be.

Debating switching to another graphing package- thoughts?


Solution

  • re https://github.com/imaNNeo/fl_chart/issues/54

    The solution here is to scale your date values manually before passing into the graph like so:

    double dateToDouble(Timestamp d) => d.millisecondsSinceEpoch.toDouble();
    
    double scaleDate(Timestamp d, List<double> minmaxDates) => (dateToDouble(d) - minmaxDates[0]) / (minmaxDates[1] - minmaxDates[0]); // returns a value btw 0 and 1
    

    and unscale them (e.g. for axis labels) with something like:

    double unscaleDate(double d, List<double> minmaxDates) => d * (minmaxDates[1] - minmaxDates[0]) + minmaxDates[0];