flutter-examples icon indicating copy to clipboard operation
flutter-examples copied to clipboard

[SFCartesianChart] make possible to return null with builder in DataLabelSettings

Open starfoxcom opened this issue 2 years ago • 4 comments

Hello, I don't know if this is possible or not, but I was committed with a task to build a Cartesian chart to show only 2 labels in the chart, but for some reason, when the point where one of the labels to show is close enough to the top or bottom limits, sometimes it disappears. I noticed that this is happening also when there was not enough space between the 'showing' labels and the 'not showing/empty' labels. Here is the code of the SFCartesianChart with the logic to show the 2 labels only and some screenshots of what im getting right now and what is expected to achieve. The reason of this issue is to know if its possible to return null for those 'empty labels' and with that, freeing space for the labels that need it, but only on the builder, since here is where the custom labels can be achieved.

SizedBox(
  height: 220,
  child: SfCartesianChart(
      key: Key('SmartSfCartesianChart'),
      margin: EdgeInsets.zero,
      plotAreaBorderColor: Colors.transparent,
      primaryXAxis: CategoryAxis(
          isVisible: false,
          labelPlacement: LabelPlacement.onTicks,
          edgeLabelPlacement: EdgeLabelPlacement.hide,
          majorGridLines: const MajorGridLines(color: Colors.transparent),
          axisLine: const AxisLine(width: 0),
          minorGridLines: const MinorGridLines(color: Colors.transparent)),
      primaryYAxis: CategoryAxis(
          isVisible: false,
          visibleMinimum: minLabel! - (minLabel! * 0.065),
          visibleMaximum: maxLabel! * 1.065,
          maximumLabels: 1,
          labelPlacement: LabelPlacement.onTicks,
          majorGridLines: const MajorGridLines(color: Colors.transparent),
          axisLine: const AxisLine(width: 0),
          minorGridLines: const MinorGridLines(color: Colors.transparent)),
      // Enable tooltip on initState
      tooltipBehavior: _tooltipBehavior,
      series: <LineSeries<CoinPerformanceData, String>>[
        LineSeries<CoinPerformanceData, String>(
            color: Colors.blue,
            name: 'USD Value',
            dataSource: coinPerformanceData,
            xValueMapper: (CoinPerformanceData d, _) {
              return d.valueX;
            },
            yValueMapper: (CoinPerformanceData d, _) {
              return d.valueY;
            },
            // Enable data label
            dataLabelSettings: DataLabelSettings(
                labelAlignment: ChartDataLabelAlignment.auto,
                // ignore: implicit_dynamic_parameter
                builder: (data, point, series, pointIndex, seriesIndex) {
                  final coinPerformanceDataValue = data as CoinPerformanceData;
                  if (coinPerformanceDataValue.valueY == maxLabel &&
                      !maxApplied) {
                    maxApplied = true;
                    return Padding(
                      padding: const EdgeInsets.only(top: 0),
                      child:
                          Text(
                              smartNumberize(
                                  number: coinPerformanceDataValue.valueY,
                                  isMoney: true),
                              style: Theme.of(context)
                                  .textTheme
                                  .bodyText2!
                                  .copyWith(fontSize: 10)),
                    );
                  }

                  if (coinPerformanceDataValue.valueY == minLabel &&
                      !minApplied) {
                    minApplied = true;
                    return Padding(
                      padding: const EdgeInsets.only(bottom: 0),
                      child:
                          Text(
                              smartNumberize(
                                  number: coinPerformanceDataValue.valueY,
                                  isMoney: true),
                              style: Theme.of(context)
                                  .textTheme
                                  .bodyText2!
                                  .copyWith(fontSize: 10)),
                    );
                  }
                  return Text('',
                      style: Theme.of(context)
                          .textTheme
                          .bodyText2!
                          .copyWith(fontSize: 10));
                },
                isVisible: true))
      ]));

Here is a data set where the chart only shows 1 of the 2 labels

{
	"2022-04-24 19:00": 39704.0,
	"2022-04-24 20:00": 39501.0,
	"2022-04-24 21:00": 39625.0,
	"2022-04-24 22:00": 39548.0,
	"2022-04-24 23:00": 39451.0,
	"2022-04-25 00:00": 38868.0,
	"2022-04-25 01:00": 39102.0,
	"2022-04-25 02:00": 39086.0,
	"2022-04-25 03:00": 39246.0,
	"2022-04-25 04:00": 39143.0,
	"2022-04-25 05:00": 38968.0,
	"2022-04-25 06:00": 38643.0,
	"2022-04-25 07:00": 38458.86,
	"2022-04-25 08:00": 38416.0,
	"2022-04-25 09:00": 38578.0,
	"2022-04-25 10:00": 38437.0,
	"2022-04-25 11:00": 38828.0,
	"2022-04-25 12:00": 38807.0,
	"2022-04-25 13:00": 38735.0,
	"2022-04-25 14:00": 39061.0,
	"2022-04-25 15:00": 38977.0,
	"2022-04-25 16:00": 39470.0,
	"2022-04-25 17:00": 39394.0
}

Here is the class that im using on the chart

class CoinPerformanceData {
  CoinPerformanceData({
    this.valueX,
    this.valueY,
  });
  String? valueX;
  double? valueY;
}

And these are some variables that are being used in the SFCartesianChart that need to be stablished to work

// This is a helper function for number formatting used in our code
String smartNumberize(
    {double? number,
    bool isMoney = false,
    bool? isSimple = false,
    bool? dynamicDecimals = false,
    int? decimalDigits = 1}) {
  if (number == null) {
    return 'N/A';
  }
  if (isMoney) {
    return NumberFormat.simpleCurrency(
            decimalDigits: dynamicDecimals! ? (number < 0.01 ? 6 : 2) : 2)
        .format(number);
  } else if (isSimple!) {
    return NumberFormat.decimalPattern().format(number);
  } else {
    return NumberFormat.decimalPercentPattern(decimalDigits: decimalDigits)
        .format(number);
  }
}
// These are set before the build method
  bool minApplied = false;
  bool maxApplied = false;
  double? minLabel;
  double? maxLabel;
  List<CoinPerformanceData> coinPerformanceData = [];
// This is only for this issue, since the data set is fetched from backend, 
// use it with the previous data set provided for testing
Map<String, dynamic> dataSet = {};

// This is set on initState in my project
dataSet.forEach((key, value) {
                  final valueY = value as double;
                  minLabel = minLabel != null
                      ? minLabel! > valueY
                          ? valueY
                          : minLabel
                      : valueY;
                  maxLabel = maxLabel != null
                      ? maxLabel! < valueY
                          ? valueY
                          : maxLabel
                      : valueY;
                  coinPerformanceData.add(CoinPerformanceData(
                    valueX: key,
                    valueY: valueY,
                  ));
                });

I noticed that this problem can be solved by giving more height to the SizedBox that contains the chart, but is not possible due to UI/UX rules that we have for the project.

Image references: What im getting in some charts image

What im expecting to get in all charts image

starfoxcom avatar Apr 25 '22 19:04 starfoxcom

Hi @starfoxcom,

On checking your scenario with the provided code snippet and replication information, we would like to let you know that the last data label which is getting hiding is due to the data label text / widget being too long and it flows outside the plot area and also collides with the other empty string data labels before the last point internally and this is the current default behavior in our chart widget. However, for this scenario you can render the last points data label text/ widget using the annotation feature available in our chart widget and setting necessary padding in order to align as per your requirement. We have also modified the provided code snippet to achieve the same and also attached the sample below for your reference.

Sample: datalabelminmax.zip

For further information on the annotation feature, please check the user guide below. https://help.syncfusion.com/flutter/cartesian-charts/annotations

Regards, Sriram Kiran

SriramKiranSenthilkumar avatar Apr 26 '22 18:04 SriramKiranSenthilkumar

Hello, I tested the code snippet provided on my project, and its working really well, just with some issues found: I tried to use the onDataLabelRender to render only the labels that match the indexes of the two labels that I want, but the problem is that these are still hiding when the data set is kind of big.

References when the amount of data points are not a problem: image image image

References when the amount of data points are a problem: image image

With the previous mentioned, I decided to go with the annotation option, which worked really well too, since this one will always show no matter what, but the position is absolute, so if the widget is kind of off from the edges, it has to be replaced. I know that it can be achieved with padding, but the problem here is that it cannot be a fixed number, since the points in the chart can vary, so I want to know if there's a way to get the exact position in pixels of these annotations to modify its padding according to it, since with that its possible to replace the widget by knowing the boxConstraints of the chart, then getting the size of the widget in pixels and get the position of it and calculate if the widget itself is out of bounds .

References of the charts where the annotations are quite off the chart area or even completely out of it: image image image image

starfoxcom avatar Apr 28 '22 16:04 starfoxcom

Hi @starfoxcom,

As we stated earlier empty string data label region may collide with the label you have rendered using onDataLabelRender callback. So, we have considered this as a bug, and we will fix this and roll out in our next weekly patch release. That would be expected on 10th May 2022. We appreciate your patience until then.

Regards, Yuvaraj.

Yuvaraj-Gajaraj avatar May 04 '22 14:05 Yuvaraj-Gajaraj

Hi @starfoxcom,

Thanks for your patience. We are glad to let you know that the reported issue regarding data labels getting hidden when adjacent labels have empty strings has been fixed and rolled out in our SP release. To resolve the issue at your end kindly upgrade the chart package to the latest version mentioned below.

Version: https://pub.dev/packages/syncfusion_flutter_charts/versions/20.1.55

Regards, Yuvaraj.

Yuvaraj-Gajaraj avatar May 12 '22 16:05 Yuvaraj-Gajaraj