Skip to content

Chart tooltip pop-up bug on multi-series #1819

@DominicOrga

Description

@DominicOrga

Bug description

When a tooltip is shown on series B, then decide to hide that series, attempting to show the tooltip of series A gets overlapped by a "ghost" tooltip from series B.

Steps to reproduce

  1. Create a chart with 2 series (Let's call them Series A and Series B)
  2. Open a tooltip from Series B.
  3. Hide Series B.
  4. Open a tooltip from Series A.

Code sample

Code sample
SfCartesianChart(
      onActualRangeChanged: (args) =>
          onActualRangeChanged(context: context, args: args),
      zoomPanBehavior: zoomPanBehavior(),
      primaryXAxis: primaryXAxis(chartRange: selectedChartRange),
      tooltipBehavior:
          tooltipBehavior(context: context, chartRange: selectedChartRange),
      legend: legend(chartRangeDataSeriesList: chartRangeDataSeriesList),
      series: chartRangeDataSeriesList.mapIndexed(
        (index, dataSeries) {
          return ColumnSeries<TestDataPoint, DateTime>(
            legendItemText: dataSeries.legend,
            dataSource: dataSeries.emptyAndFilledDataPoints(selectedChartRange),
            color: seriesColors[index % seriesColors.length],
            borderRadius: BorderRadius.circular(8),
            animationDuration: 0,
            xValueMapper: (dataPoint, _) => dataPoint.dateTime,
            yValueMapper: (dataPoint, _) {
              switch (dataPoint) {
                case TestEmptyDataPoint():
                  return null;
                case TestFilledDataPoint():
                  return dataPoint.yValue();
              }
            },
          );
        },
      ).toList(),
    );

/// The possible colors assigned to the series in the Syncfusion chart.
List<Color> get seriesColors => [
    Colors.tealAccent.shade200,
    Colors.amberAccent.shade200,
    Colors.deepPurpleAccent.shade200,
    Colors.pinkAccent.shade200,
  ];

/// The primary X-axis assigned to the Syncfusion chart.
ChartAxis primaryXAxis({required TestChartRange chartRange}) {
switch (chartRange) {
  case TestChartRange.week:
    return DateTimeAxis(
      autoScrollingDelta: 7,
      autoScrollingDeltaType: DateTimeIntervalType.days,
      dateFormat: DateFormat('E'),
      interval: 1,
    );
  case TestChartRange.month:
    return DateTimeAxis(
      autoScrollingDelta: 30,
      autoScrollingDeltaType: DateTimeIntervalType.days,
      dateFormat: DateFormat('d'),
      interval: 7,
    );
  case TestChartRange.sixMonth:
    return DateTimeAxis(
      autoScrollingDelta: 6,
      autoScrollingDeltaType: DateTimeIntervalType.months,
      dateFormat: DateFormat('MMM'),
      interval: 1,
    );
  case TestChartRange.year:
    return DateTimeAxis(
      autoScrollingDelta: 12,
      autoScrollingDeltaType: DateTimeIntervalType.months,
      dateFormat: DateFormat('MMM'),
      interval: 3,
    );
}
}

Legend legend({required List<TestDataSeries> chartRangeDataSeriesList}) {
return Legend(
  isVisible: chartRangeDataSeriesList.length > 1,
  position: LegendPosition.bottom,
);
}

/// The zoom and pan behavior assigned to the Syncfusion chart.
ZoomPanBehavior zoomPanBehavior() {
return ZoomPanBehavior(
  // Enables panning on the x-axis.
  enablePanning: true,
  // Shows only the necessary Y-labels based on the max Y-value upon
  // scrolling.
  zoomMode: ZoomMode.x,
);
}

/// The tooltip customization for the Syncfusion chart.
TooltipBehavior tooltipBehavior({
required BuildContext context,
required TestChartRange chartRange,
}) {
return TooltipBehavior(
  enable: true,
  color: context.colorScheme.surface5,
  builder: (data, point, series, pointIndex, seriesIndex) {
    switch (data as TestDataPoint) {
      case TestEmptyDataPoint():
        return const SizedBox();
      case TestFilledDataPoint():
        return tooltip(
          context: context,
          dataPoint: data as TestFilledDataPoint,
          chartRange: chartRange,
          seriesName: series.legendItemText ?? '',
          startDate: data.dateTime,
          isDataPointMedian: chartRange.isSixMonth || chartRange.isYear,
        );
    }
  },
);
}

/// The tooltip displayed when the user taps on a data point in the
/// Syncfusion chart.
///
/// The [dataPoint] is the data point whose value is displayed in the tooltip.
/// The [startDate] and [endDate] are the date range of the data point, which
/// is useful when the [dataPoint] is a median value as specified by the
/// [isDataPointMedian].
Widget tooltip({
required BuildContext context,
required TestChartRange chartRange,
required String seriesName,
required bool isDataPointMedian,
required TestFilledDataPoint? dataPoint,
required DateTime? startDate,
DateTime? endDate,
}) {
final formattedSeriesName =
    isDataPointMedian ? 'Mdn. $seriesName' : seriesName;

final String formattedDateRange;

// The date format of the tooltip depends on the chart range.
//
// For week and months, the date format is 'MMM d, yyyy since data is
// displayed per day.
//
// For six months and year, the date format is 'MMM yyyy' since data
// is displayed per month.
switch (chartRange) {
  case TestChartRange.week:
  case TestChartRange.month:
    if (startDate == null) {
      formattedDateRange = '';
    } else if (endDate == null || startDate.eqvYearMonthDay(endDate)) {
      // Same year, same month, same day
      // - Start Date: Oct 15, 2021
      // - End Date: Oct 15, 2021
      // - Format: Oct 15, 2021
      formattedDateRange = DateFormat.yMMMd().format(startDate);
    } else if (startDate.eqvYear(endDate) && startDate.eqvMonth(endDate)) {
      // Same year, same month, different day
      // Start Date: Oct 15, 2021
      // End Date: Oct 21, 2021
      // Format: Oct 15 - 21, 2021
      formattedDateRange = '${DateFormat.MMMd().format(startDate)} - '
          '${DateFormat('d, y').format(endDate)}';
    } else if (startDate.eqvYear(endDate)) {
      // Same year, different month
      // Start Date: Oct 15, 2021
      // End Date: Nov 21, 2021
      // Format: Oct 15 - Nov 21, 2021
      formattedDateRange = '${DateFormat.MMMd().format(startDate)} - '
          '${DateFormat.yMMMd().format(endDate)}';
    } else {
      // Different year
      // Start Date: Dec 15, 2021
      // End Date Date: Jan 15, 2022
      // Format: Dec 15, 2021 - Jan 21, 2022
      formattedDateRange = '${DateFormat.yMMMd().format(startDate)} - '
          '${DateFormat.yMMMd().format(endDate)}';
    }

  case TestChartRange.sixMonth:
  case TestChartRange.year:
    if (startDate == null) {
      formattedDateRange = '';
    } else if (endDate == null ||
        startDate.eqvYear(endDate) && startDate.eqvMonth(endDate)) {
      // Same year, same month
      // Start Date: Oct 2021
      // End Date: Oct 2021
      // Format: Oct 2021
      formattedDateRange = DateFormat.yMMM().format(startDate);
    } else if (startDate.eqvYear(endDate)) {
      // Same year, different month
      // Start Date: Nov 2021
      // End Date: Feb 2021
      // Format: Nov - Feb 2022
      formattedDateRange = '${DateFormat.MMM().format(startDate)} - '
          '${DateFormat.yMMM().format(endDate)}';
    } else {
      // Different year
      // Start Date: Dec 2021
      // End Date: Jan 2022
      // Format: Dec 2021 - Jan 2022
      formattedDateRange = '${DateFormat.yMMM().format(startDate)} - '
          '${DateFormat.yMMM().format(endDate)}';
    }
}

return Padding(
  padding: const EdgeInsets.all(Gap.paddingSmall),
  child: Column(
    mainAxisSize: MainAxisSize.min,
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(
        formattedSeriesName,
        style: context.textTheme.labelLarge?.copyWith(
          color: context.colorScheme.onSurface,
        ),
      ),
      const SizedBox(height: Gap.paddingTiny),
      if (dataPoint != null)
        dataPoint.tooltipValue(context)
      else
        const Text('–'),
      if (dataPoint?.isOptimal != null)
        Padding(
          padding: const EdgeInsets.only(top: Gap.paddingSmall),
          child: OptimalityChip(isOptimal: dataPoint!.isOptimal!),
        ),
      const SizedBox(height: Gap.paddingSmall),
      Text(
        formattedDateRange,
        style: context.textTheme.labelMedium
            ?.copyWith(color: context.colorScheme.onSurface),
      ),
    ],
  ),
);
}

/// Called when the actual range of the Syncfusion chart changes.
///
/// If the x-axis has been panned, the visible range of the chart will be
/// reported to the parent [TestChartRangeTabCubit].
void onActualRangeChanged({
required BuildContext context,
required ActualRangeChangedArgs args,
}) {
if (args.orientation == AxisOrientation.horizontal) {
  context.read<TestChartRangeTabCubit>().setChartVisibleRange(
        (args.visibleMin as num).ceil(),
        (args.visibleMax as num).floor(),
      );
}
}

Screenshots or Video

bug_demo.mp4

Stack Traces

No exception thrown.

On which target platforms have you observed this bug?

Android

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.19.5, on macOS 14.3.1 23D60 darwin-arm64, locale en-PH)
    • Flutter version 3.19.5 on channel stable at /Users/test/fvm/versions/3.19.5
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 300451adae (3 weeks ago), 2024-03-27 21:54:07 -0500
    • Engine revision e76c956498
    • Dart version 3.3.3
    • DevTools version 2.31.1

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at /Users/test/Library/Android/sdk
    • Platform android-34, build-tools 34.0.0
    • ANDROID_HOME = /Users/test/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b829.9-10027231)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.3)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15E204a
    • CocoaPods version 1.15.0

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2022.3)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b829.9-10027231)

[✓] VS Code (version 1.88.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.86.0

[✓] Connected device (4 available)
    • Pixel 8 (mobile)        • 3B031FDJH005H1            • android-arm64  • Android 14 (API 34)
    • iPhone 13 mini (mobile) • 00008110-00046C3411B8801E • ios            • iOS 17.4.1 21E236
    • macOS (desktop)         • macos                     • darwin-arm64   • macOS 14.3.1 23D60 darwin-arm64
    • Chrome (web)            • chrome                    • web-javascript • Google Chrome 123.0.6312.124

[✓] Network resources
    • All expected network resources are available.

• No issues found!```

</details>

Metadata

Metadata

Assignees

No one assigned

    Labels

    chartsCharts componentsolvedSolved the query using existing solutionswaiting for customer responseCannot make further progress until the customer responds.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions