Skip to content

Rendering Issue with Syncfusion Flutter Charts on Android #2344

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

Open
techieasif opened this issue Apr 30, 2025 · 0 comments
Open

Rendering Issue with Syncfusion Flutter Charts on Android #2344

techieasif opened this issue Apr 30, 2025 · 0 comments

Comments

@techieasif
Copy link

techieasif commented Apr 30, 2025

Bug description

Dear Syncfusion Team,

We sincerely appreciate the robust functionality provided by your libraries. Our development team is extensively utilizing the syncfusion_flutter_charts package in our application.

We have encountered a rendering issue specifically on the Android platform, while the same implementation functions seamlessly on iOS. For your reference, we have attached screenshots illustrating the issue. Notably, there are no error messages or logs to analyze, as the charts simply fail to render on Android.

We kindly request your assistance in diagnosing and resolving this issue. Please let us know if additional details or specific configurations are required to facilitate your investigation.

Steps to reproduce

App: https://play.google.com/store/apps/details?id=com.ppl.zeroxppl&hl=en_IN

Code sample

Code sample
class LineCandleChart extends StatefulWidget {
  final List<List<num>> chartData;
  final List<LineChartPointData> buyPointsToPlot;
  final List<LineChartPointData> sellPointsToPlot;
  final Color? graphColor;
  final bool isYAxisVisible;
  final bool isXAxisVisible;
  final bool isDateTimeXAxis;
  final void Function(double?)? onTrackballChange;
  final TrendingChartFilterType trendingChartFilterType;
  final bool isCandleStick;
  final bool isShowingValueInTooltip;
  final void Function(ChartTouchInteractionArgs)? onChartTouchInteractionDown;
  final void Function(ChartTouchInteractionArgs)? onChartTouchInteractionUp;
  final bool isTimeInSeconds;
  final bool showLiveBubble;
  final TokenPriceChartUtils? tokenPriceChartUtils;
  final bool showMajorGridLinesXAxis;
  final bool showMajorGridLinesYAxis;
  final bool showMajorTickLines;
  final bool showAxisLine;
  final bool showLabel;
  final bool showDateTimeXAxisInTime;
  final bool isFromFeed;
  final TrendingBuySellPoints? trendingBuySellPoints;
  final Map<String, PeopleModel>? identities;
  final Function(PeopleModel)? onPointTap;

  const LineCandleChart({
    super.key,
    required this.chartData,
    this.graphColor,
    this.buyPointsToPlot = const [],
    this.sellPointsToPlot = const [],
    this.isYAxisVisible = false,
    this.isXAxisVisible = false,
    this.onTrackballChange,
    required this.isCandleStick,
    required this.trendingChartFilterType,
    required this.isShowingValueInTooltip,
    this.onChartTouchInteractionDown,
    this.onChartTouchInteractionUp,
    this.isDateTimeXAxis = false,
    this.showLiveBubble = false,
    this.isTimeInSeconds = false,
    this.tokenPriceChartUtils,
    this.showMajorGridLinesXAxis = true,
    this.showMajorGridLinesYAxis = false,
    this.showAxisLine = true,
    this.showMajorTickLines = true,
    this.showLabel = false,
    this.showDateTimeXAxisInTime = false,
    this.isFromFeed = false,
    this.trendingBuySellPoints,
    this.identities,
    this.onPointTap,
  });

  @override
  State<LineCandleChart> createState() => _LineCandleChartState();
}

class _LineCandleChartState extends State<LineCandleChart> {
  bool isTrackballChanging = false;

  @override
  Widget build(BuildContext context) {
    LineChartUtils lineChartUtils = LineChartUtils();
    LineChartDataModel? lineChartDataModel =
        lineChartUtils.getGradientSeriesData(
      chartData: widget.chartData,
      context: context,
      chartColor: getColor(context),
      isTimeInSeconds: widget.isTimeInSeconds,
      buyPointsToPlot: widget.buyPointsToPlot,
      sellPointsToPlot: widget.sellPointsToPlot,
      isCandleStick: widget.isCandleStick,
      isDateTimeXAxis: widget.isDateTimeXAxis,
      showLiveBubble: widget.showLiveBubble,
      tokenPriceChartUtils: widget.tokenPriceChartUtils,
      isTrackballChanging: isTrackballChanging,
      trendingBuySellPoints: widget.trendingBuySellPoints,
      identities: widget.identities,
      onPointTap: widget.onPointTap,
    );
    if (widget.chartData.length < 2 || lineChartDataModel == null) {
      return const SizedBox.shrink();
    }
    int? lastHapticPointIndex = 0;

    return RepaintBoundary(
      child: SfCartesianChart(
        key: widget.key,
        title: const ChartTitle(text: ''),
        plotAreaBorderWidth: 0,
        primaryXAxis: widget.isDateTimeXAxis
            ? DateTimeAxis(
                isVisible: widget.isXAxisVisible,
                majorGridLines: const MajorGridLines(
                  width: 0,
                ),
                title: const AxisTitle(),
                labelFormat: '{value}',
                dateFormat: DateTimeUtils.formatGraphTimeDifference(
                  widget.chartData,
                ),
                minimum: DateTime.fromMillisecondsSinceEpoch(
                  lineChartDataModel.minX,
                ),
                maximum: DateTime.fromMillisecondsSinceEpoch(
                  lineChartDataModel.maxX,
                ),
                labelStyle: Theme.of(context).textTheme.displaySmall?.copyWith(
                      color: Theme.of(context).colorScheme.text3,
                      fontSize: 11,
                    ),
                axisLine: widget.showAxisLine
                    ? AxisLine(
                        width: 0.1,
                        color: Theme.of(context).colorScheme.text2,
                      )
                    : const AxisLine(
                        width: 0,
                      ),
                majorTickLines: MajorTickLines(
                  size: 0,
                  color: Theme.of(context).colorScheme.text2,
                ),
                desiredIntervals: 4,
                tickPosition: TickPosition.outside,
                axisBorderType: AxisBorderType.withoutTopAndBottom,
                rangePadding: ChartRangePadding.auto,
                labelsExtent: 70,
                labelPosition: ChartDataLabelPosition.outside,
                placeLabelsNearAxisLine: true,
                edgeLabelPlacement: EdgeLabelPlacement.none,
                intervalType: DateTimeIntervalType.auto,
              )
            : NumericAxis(
                isVisible: widget.isXAxisVisible,
                labelFormat: '{value}',
                desiredIntervals: DateTimeUtils.getLastTimeDuration(
                              widget.chartData,
                            ).inHours >
                            48 &&
                        DateTimeUtils.getLastTimeDuration(
                              widget.chartData,
                            ).inDays <
                            4
                    ? 3
                    : 4,
                axisLabelFormatter: (value) {
                  if (widget.isFromFeed) {
                    return ChartAxisLabel(
                      DateTimeUtils.formatGraphTimeDifference(
                        widget.chartData,
                      ).format(
                        DateTime.fromMillisecondsSinceEpoch(
                          value.value.toInt(),
                        ),
                      ),
                      value.textStyle,
                    );
                  }
                  return ChartAxisLabel(
                    NumberUtils.abbreviateNumberForGraph(value.value),
                    value.textStyle,
                  );
                },
                majorGridLines: widget.showMajorGridLinesXAxis
                    ? const MajorGridLines()
                    : const MajorGridLines(
                        width: 0,
                      ),
                axisLine: widget.showAxisLine
                    ? const AxisLine()
                    : const AxisLine(
                        width: 0,
                      ),
                majorTickLines: widget.showMajorTickLines
                    ? const MajorTickLines()
                    : const MajorTickLines(
                        size: 0,
                      ),
                title: const AxisTitle(),
                minimum: lineChartDataModel.minX.toDouble(),
                maximum: lineChartDataModel.maxX.toDouble(),
                tickPosition: TickPosition.inside,
                labelStyle: Theme.of(context).textTheme.displaySmall?.copyWith(
                      color: Theme.of(context).colorScheme.text3,
                      fontSize: 10,
                    ),
                plotOffsetEnd: 24,
                rangePadding: ChartRangePadding.auto,
                edgeLabelPlacement: EdgeLabelPlacement.shift,
              ),
        primaryYAxis: NumericAxis(
          isVisible: widget.isYAxisVisible,
          opposedPosition: true,
          maximumLabels: 2,
          majorGridLines: MajorGridLines(
            width: widget.showMajorGridLinesYAxis ? 2 : 0.7,
            color: Theme.of(context).colorScheme.borderLight,
          ),
          numberFormat: NumberFormat.compact(),
          labelStyle: Theme.of(context).textTheme.displaySmall!.copyWith(
                color: Theme.of(context).colorScheme.text3,
                fontSize: 11,
              ),
          axisLine: const AxisLine(width: 0),
          majorTickLines: const MajorTickLines(size: 0),
          axisLabelFormatter: (value) {
            return ChartAxisLabel(
              value.value == 0 ? "" : value.text,
              value.textStyle,
            );
          },
          title: const AxisTitle(),
          tickPosition: TickPosition.outside,
          rangePadding: ChartRangePadding.round,
          edgeLabelPlacement: EdgeLabelPlacement.shift,
        ),
        onTrackballPositionChanging: (TrackballArgs args) {
          updateTrackballState(true);
          if (lastHapticPointIndex != args.chartPointInfo.dataPointIndex) {
            HapticFeedback.selectionClick();
            lastHapticPointIndex = args.chartPointInfo.dataPointIndex;
          }

          if (widget.onTrackballChange != null) {
            if (widget.isCandleStick) {
              widget.onTrackballChange!(
                args.chartPointInfo.chartPoint?.close?.toDouble(),
              );
            } else {
              widget.onTrackballChange!(
                args.chartPointInfo.chartPoint?.y?.toDouble(),
              );
            }
          }
        },
        onChartTouchInteractionDown: (ChartTouchInteractionArgs args) {
          HapticFeedback.mediumImpact();
          if (widget.onChartTouchInteractionDown != null) {
            updateTrackballState(true);
            widget.onChartTouchInteractionDown!(args);
          }
        },
        onChartTouchInteractionUp: (ChartTouchInteractionArgs args) {
          updateTrackballState(false);
          if (widget.onTrackballChange != null) {
            widget.onTrackballChange!(null);
          }
          if (widget.onChartTouchInteractionUp != null) {
            widget.onChartTouchInteractionUp!(args);
          }
        },
        series: lineChartDataModel.data,
        trackballBehavior: TrackballBehavior(
          enable: true,
          lineWidth: 0.5,
          activationMode: ActivationMode.singleTap,
          tooltipAlignment: ChartAlignment.near,
          tooltipDisplayMode: TrackballDisplayMode.groupAllPoints,
          lineDashArray: const <double>[5, 5],
          tooltipSettings: InteractiveTooltip(
            format: 'point.x',
            color: Theme.of(context).colorScheme.baseColor,
            connectorLineColor: Theme.of(context).colorScheme.baseColor,
            textStyle: Theme.of(context).textTheme.displaySmall!.copyWith(
                  color: Theme.of(context).colorScheme.text1,
                ),
          ),
          builder: (BuildContext context, TrackballDetails details) {
            if (widget.isCandleStick) {
              return tooltipWidgetForCandle(context, details);
            }
            return tooltipWidgetForLineChart(context, details);
          },
          markerSettings: TrackballMarkerSettings(
            markerVisibility: TrackballVisibilityMode.visible,
            height: 12,
            width: 12,
            borderWidth: 12,
            borderColor: getColor(context).withOpacityValue(0.2),
            color: getColor(context),
            shape: DataMarkerType.circle,
          ),
        ),
        zoomPanBehavior: ZoomPanBehavior(
          enablePinching: true,
          zoomMode: ZoomMode.x,
          enableMouseWheelZooming: true,
          enableSelectionZooming: false,
          enableDoubleTapZooming: true,
        ),
      ),
    );
  }

  void updateTrackballState(bool isTrackballChange) {
    if (isTrackballChanging != isTrackballChange) {
      isTrackballChanging = isTrackballChange;
    }
  }

  bool get isPositive {
    final firstValue = widget.chartData.first;
    final index = firstValue.length > 4 ? 4 : 1;
    return widget.chartData.first[index] < widget.chartData.last[index];
  }

  Color getColor(BuildContext context) {
    if (widget.showLiveBubble && !widget.isFromFeed) {
      return Theme.of(context).colorScheme.brightness == Brightness.light
          ? Theme.of(context).colorScheme.primary
          : Colors.white;
    }
    if (widget.graphColor != null) {
      return widget.graphColor!;
    }
    return isPositive
        ? Theme.of(context).colorScheme.success
        : Theme.of(context).colorScheme.error;
  }

  Widget tooltipWidgetForLineChart(
    BuildContext context,
    TrackballDetails trackballDetails,
  ) {
    if (trackballDetails.groupingModeInfo == null ||
        (trackballDetails.groupingModeInfo?.visibleSeriesIndices.isEmpty ??
            true) ||
        (trackballDetails.groupingModeInfo?.visibleSeriesList.isEmpty ??
            true)) {
      return const SizedBox.shrink();
    }
    final index = trackballDetails.groupingModeInfo!.currentPointIndices.first;

    if (index < 0 ||
        index >=
            trackballDetails
                .groupingModeInfo!.visibleSeriesList.first.dataSource.length) {
      return const SizedBox.shrink();
    }
    LineChartPointData? point = trackballDetails
        .groupingModeInfo!.visibleSeriesList.first.dataSource[index];
    if (point == null) {
      return const SizedBox.shrink();
    }
    final formattedTime = DateTimeUtils.formatCustomDateForGraph(
      point.valueX.toInt(),
      widget.trendingChartFilterType,
    );
    final formattedValue = NumberUtils.abbreviateNumberForGraph(
      point.valueY ?? 0,
    );
    return Container(
      padding: const EdgeInsets.all(8),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.baseColor,
        borderRadius: BorderRadius.circular(4),
      ),
      child: Text(
        widget.isShowingValueInTooltip
            ? "\$$formattedValue • $formattedTime"
            : formattedTime,
        style: Theme.of(context).textTheme.displaySmall,
      ),
    );
  }

  Widget tooltipWidgetForCandle(
    BuildContext context,
    TrackballDetails trackballDetails,
  ) {
    if (trackballDetails.seriesIndex != 0) {
      return const SizedBox.shrink();
    }
    final high = trackballDetails.point?.high == null
        ? null
        : NumberUtils.abbreviateNumberForGraph(
            trackballDetails.point!.high!,
          );
    final low = trackballDetails.point?.low == null
        ? null
        : NumberUtils.abbreviateNumberForGraph(
            trackballDetails.point!.low!,
          );
    final open = trackballDetails.point?.open == null
        ? null
        : NumberUtils.abbreviateNumberForGraph(
            trackballDetails.point!.open!,
          );
    final close = trackballDetails.point?.close == null
        ? null
        : NumberUtils.abbreviateNumber(trackballDetails.point!.close!);
    final formattedTime = DateTimeUtils.formatCustomDateForGraph(
      trackballDetails.point?.x?.toInt(),
      widget.trendingChartFilterType,
    );
    getTextSpan(title, subtitle) {
      return TextSpan(
        children: [
          TextSpan(
            text: title,
            style: Theme.of(context)
                .textTheme
                .displaySmall!
                .copyWith(fontWeight: FontWeight.w600),
          ),
          TextSpan(
            text: subtitle,
            style: Theme.of(context).textTheme.displaySmall,
          ),
        ],
      );
    }

    return Container(
      padding: const EdgeInsets.all(8),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.baseColor,
        borderRadius: BorderRadius.circular(4),
      ),
      child: RichText(
        text: TextSpan(
          style: Theme.of(context).textTheme.displaySmall,
          children: [
            getTextSpan('Date: ', "$formattedTime\n"),
            getTextSpan('High: ', "$high\n"),
            getTextSpan('Low: ', "$low\n"),
            getTextSpan('Open: ', "$open\n"),
            getTextSpan('Close: ', "$close"),
          ],
        ),
      ),
    );
  }
}

Screenshots or Video

Screenshots / Video demonstration

Android (SOME CHARTS HAS RENDERING ISSUE)
Image

iOS (WORKING AS EXPECTED)
Image

Stack Traces

Stack Traces
NA

On which target platforms have you observed this bug?

Android

Flutter Doctor output

Doctor output
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.29.3, on macOS 15.3.2 24D81 darwin-arm64, locale en-IN)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.3)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.3)
[✓] IntelliJ IDEA Ultimate Edition (version 2025.1)
[✓] VS Code (version 1.99.3)
[✓] Connected device (4 available)
    ! Error: Browsing on the local area network for Praveen Agrawal’s iPhone. Ensure the device is
      unlocked and attached with a cable or associated with the same local area network as this
      Mac.
      The device must be opted into Developer Mode to connect wirelessly. (code -27)
[✓] Network resources

• No issues found!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant