Hello!
I have a chart that gets updated every other second or so from a web socket, so this is essentially a real time chart.I have created TrackballBehavior. I'm utilizing the TrackballBehavior builder method to customize the tooltip UI.
Here's the problem:
When i interact with the chart, the tooltip appears, but whenever i update the state (update the data source), the tooltip disappears. Anyway to get around this?
Here is my code:
@override
Widget build(BuildContext context) {
final xAxisMinimum = current.lastOrNull?.timestamp.subtract(state.currentInterval.duration);
final maxConsumption = current.map((e) => e.consumption).fold<double>(0, max).roundUpToNearest(1);
const minConsumption = 0.0;
final chart = SfCartesianChart(
plotAreaBorderWidth: 0,
margin: const EdgeInsets.only(right: 10.0, left: 8.0),
trackballBehavior: TrackballBehavior(
activationMode: ActivationMode.singleTap,
tooltipDisplayMode: TrackballDisplayMode.groupAllPoints,
tooltipAlignment: ChartAlignment.near,
shouldAlwaysShow: true,
enable: true,
builder: (context, details) {
final stat = _getHanDeviceDataFromLabel(details.groupingModeInfo?.points.firstOrNull?.dataLabelMapper);
if (stat != null) {
return ChartToolTip(
key: const Key("date_time_tool_tip_key"),
title: state.currentInterval.tooltipDateFormat(stat.timestamp),
rows: [
ChartToolTipRowConfig(
title: stat.isMocked ? Strings.commonNoData : stat.consumption.kwFormatted(),
dotColor: colors.secondaryColor,
),
],
);
}
return const SizedBox();
},
),
zoomPanBehavior: ZoomPanBehavior(
zoomMode: ZoomMode.x,
enablePanning: true,
enablePinching: true,
enableDoubleTapZooming: true,
),
primaryXAxis: DateTimeAxisExtension.defaultDateTimeAxis().copyWith(
axisLine: AxisLine(color: colors.black180),
majorTickLines: MajorTickLines(color: colors.black180),
dateFormat: state.currentInterval.chartDateFormat,
intervalType: state.currentInterval.chartInterval,
minimum: xAxisMinimum,
visibleMinimum: xAxisMinimum,
maximum: current.lastOrNull?.timestamp,
visibleMaximum: current.lastOrNull?.timestamp,
desiredIntervals: 4,
labelStyle: current.isEmpty ? const TextStyle(color: Colors.transparent) : null,
),
primaryYAxis: NumericAxisExtension.defaultNumericAxis().copyWith(
axisLine: const AxisLine(width: 0),
majorTickLines: const MajorTickLines(size: 0),
maximum: maxConsumption,
minimum: minConsumption,
opposedPosition: true,
desiredIntervals: 4,
labelStyle: current.isEmpty ? const TextStyle(color: Colors.transparent) : null,
),
series: <ChartSeries<BaseHanDeviceData, DateTime>>[
AreaSeries<BaseHanDeviceData, DateTime>(
dataSource: current,
xValueMapper: (BaseHanDeviceData data, _) => data.timestamp,
yValueMapper: (BaseHanDeviceData data, _) => data.consumption,
animationDuration: 0,
borderColor: colors.secondaryColor80,
onCreateShader: (details) => _gradientColors.createShader(details.rect),
dataLabelMapper: (_, index) => index.toString(),
),
],
);
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
"kW",
style: fonts.mini.copyWith(
color: colors.black100,
),
),
],
).horizontalPadding(spacing16),
const SizedBox(height: spacing24),
current.isEmpty ? Stack(
alignment: Alignment.center,
children: [
chart,
Text(Strings.commonNoDataForPeriod)
],
) : chart,
],
);
}
BaseHanDeviceData? _getHanDeviceDataFromLabel(String? indexString) {
final parsedIndex = int.tryParse(indexString ?? "");
if (parsedIndex != null) {
if (current.containsIndex(parsedIndex)) {
return current[parsedIndex];
}
}
return null;
}
Update:
I managed to prevent the tooltip/trackball from disappearing by updating the data source using
ChartSeriesController. But now i ran into another problem, the tooltip/trackball does not move with the x axis as the data source gets updated.
Hi Martin,
We have analyzed your query and prepared a sample with updateDataSource method and trackball behavior. Here, we are rendering the LineSeries in the real time update using the updateDataSource method with the help of ChartSeriesController and to make the trackball tooltip always visible at the last previous point, passed the specified data point index to the showByIndex method as shown in the code snippet below.
class _MyWidgetState extends State<MyWidget> { _MyWidgetState() { timer = Timer.periodic(const Duration(seconds: 1), _updateDataSource); }
Timer? timer; List<ChartData>? chartData; ChartSeriesController? _chartSeriesController; int count = 19;
late SfCartesianChart chart; late TrackballBehavior _trackballBehavior;
@override void initState() { chartData = <ChartData>[ ChartData(0, 42), ChartData(1, 47), ChartData(2, 33), ChartData(3, 49), ChartData(4, 54), ChartData(5, 41), ChartData(6, 58), ChartData(7, 51), ChartData(8, 98), ChartData(9, 41), ChartData(10, 53), ChartData(11, 72), ChartData(12, 86), ChartData(13, 52), ChartData(14, 94), ChartData(15, 92), ChartData(16, 86), ChartData(17, 72), ChartData(18, 94), ]; _trackballBehavior = TrackballBehavior( enable: true, shouldAlwaysShow: true, ); super.initState(); }
@override Widget build(BuildContext context) { chart = SfCartesianChart( primaryXAxis: NumericAxis(), primaryYAxis: NumericAxis(), trackballBehavior: _trackballBehavior, series: <LineSeries<ChartData, int>>[ LineSeries<ChartData, int>( onRendererCreated: (ChartSeriesController controller) { _chartSeriesController = controller; }, dataSource: chartData!, color: const Color.fromRGBO(192, 108, 132, 1), xValueMapper: (ChartData sales, _) => sales.country, yValueMapper: (ChartData sales, _) => sales.sales, animationDuration: 0, ), ], );
return MaterialApp( home: Scaffold( body: Center( child: chart, ), ), ); }
void _updateDataSource(Timer timer) { chartData!.add(ChartData(count, getRandomInt(10, 100))); if (chartData!.length == 20) { chartData!.removeAt(0); _chartSeriesController?.updateDataSource( addedDataIndexes: <int>[chartData!.length - 1], removedDataIndexes: <int>[0], ); _trackballBehavior.showByIndex(chartData!.length - 1); } else { _chartSeriesController?.updateDataSource( addedDataIndexes: <int>[chartData!.length - 1], ); _trackballBehavior.showByIndex(chartData!.length - 1); }
count = count + 1; }
int getRandomInt(int min, int max) { final random = math.Random(); return min + random.nextInt(max - min); }
@override void dispose() { timer?.cancel(); chartData!.clear(); _chartSeriesController = null; super.dispose(); } } |
You can modify the attached sample below according to your needs, and if you have further queries, please get back to us.
Regards,
Hari Hara Sudhan. K.
Thats close but not quite. I want the tooltip to be stuck on the point selected as the data source updates. In your example it always displays the value of the data in the last data source index.
Hi Martin,
Unfortunately, we are unable to understand your requirement clearly and we need some clarifications regarding the points mentioned below:
With that additional information, it will be very useful for us to provide the best possible solution from our end.
Regards,
Hari Hara Sudhan. K.
I need the trackball tooltip to move along with the data points as they get updated. See attachment for desired behavior. There you can see the tooltip move to the left with the data point as new data points are added.
Hi Martin,
We have prepared a sample to add and remove the segment of the Line Series in a programmatic update using the updateDataSource method. We have achieved the mentioned requirement using the TrackballBehavior.showByIndex method and onTrackballPositionChanging callback. Here, the current index of the trackball can be obtained using the onTrackballPositionChanging callback and when we update the Line Series, we have passed the current index of the trackball to the TrackballBehavior.showByIndex method as shown in the code snippet below.
class _MyAppState extends State<MyApp> { List<ChartData>? chartData; ChartSeriesController? _chartSeriesController; int count = 19; int? index; Offset? offset;
late TrackballBehavior _trackballBehavior;
@override void initState() { chartData = <ChartData>[ ChartData(0, 42), ChartData(1, 47), ChartData(2, 33), ChartData(3, 49), ChartData(4, 54), ChartData(5, 41), ChartData(6, 58), ChartData(7, 51), ChartData(8, 98), ChartData(9, 41), ChartData(10, 53), ChartData(11, 72), ChartData(12, 86), ChartData(13, 52), ChartData(14, 94), ChartData(15, 92), ChartData(16, 86), ChartData(17, 72), ChartData(18, 94), ]; _trackballBehavior = TrackballBehavior( enable: true, shouldAlwaysShow: true, activationMode: ActivationMode.singleTap, ); super.initState(); }
void _addDataSource() { chartData!.add( ChartData( count, getRandomInt(10, 100), ), );
_chartSeriesController?.updateDataSource( addedDataIndex: count, ); if (index != null) { _trackballBehavior.showByIndex(index!); } count = count + 1; }
void _removeDataSource() { chartData!.removeAt(count - 1); _chartSeriesController?.updateDataSource( removedDataIndex: chartData!.length, ); if (index != null) { _trackballBehavior.showByIndex(index!); } count = count - 1; }
int getRandomInt(int min, int max) { final random = math.Random(); return min + random.nextInt(max - min); }
@override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( body: Center( child: Stack( children: [ SfCartesianChart( onTrackballPositionChanging: (TrackballArgs trackballArgs) { index = trackballArgs.chartPointInfo.dataPointIndex; }, primaryXAxis: NumericAxis(), primaryYAxis: NumericAxis(), series: <LineSeries<ChartData, int>>[ LineSeries<ChartData, int>( onRendererCreated: (ChartSeriesController controller) { _chartSeriesController = controller; }, dataSource: chartData!, color: Colors.green, xValueMapper: (ChartData sales, _) => sales.country, yValueMapper: (ChartData sales, _) => sales.sales, animationDuration: 0, ), ], trackballBehavior: _trackballBehavior, ), Align( alignment: Alignment.bottomRight, child: Padding( padding: const EdgeInsets.all(10.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ FloatingActionButton.small( onPressed: _addDataSource, child: const Icon(Icons.add), ), const SizedBox(width: 10), FloatingActionButton.small( onPressed: _removeDataSource, child: const Icon(Icons.remove), ), ], ), ), ), ], ), ), ), ); }
@override void dispose() { chartData!.clear(); _chartSeriesController = null; super.dispose(); } } |
Also attached the sample below for your reference and you can modify the sample according to your needs. If you have further queries, please get back to us.
Regards,
Hari Hara Sudhan. K.
Hi Martin,
Also attached the recording below regarding the mentioned requirement for your reference.
Regards,
Hari Hara Sudhan. K.
Thanks! This resolves my problem
Hi Martin,
Most Welcome. Kindly get back to us if you have further queries. We are always happy to assist you.
Regards,
Hari Hara Sudhan. K.