graphic
graphic copied to clipboard
Tooltips not working for multiple LineMarks
Hello, thanks again for creating this library. I have a use case where I need to show multiple line series on the same graph, only they have different scales. We also have tooltips that should appear when hovering over the lines. I use two LineMark
s and two Variable
s so that I can have a different scale for each, but then I can't get the tooltips to appear over both lines. Here's a minimal example of my issue:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:graphic/graphic.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: MyHomePage());
}
}
class GraphData {
DateTime timestamp;
num value;
String id;
GraphData({
required this.timestamp,
required this.value,
required this.id,
});
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late final List<GraphData> series1;
late final List<GraphData> series2;
late final num series1Min;
late final num series1Max;
late final num series2Min;
late final num series2Max;
final Color color1 = const Color(0xff007c77);
final Color color2 = const Color(0xff212738);
final Color highlight = const Color(0xffff3cc7);
@override
void initState() {
super.initState();
const count = 20;
series1 = List.generate(count, (i) {
return GraphData(
timestamp: DateTime.now().subtract(Duration(days: count - i)),
value: sin(i) * 1000,
id: '1');
}, growable: false);
series1Min = series1.map((data) => data.value).reduce(min);
series1Max = series1.map((data) => data.value).reduce(max);
series2 = List.generate(count, (i) {
return GraphData(
timestamp: DateTime.now().subtract(Duration(days: count - i)),
value: cos(i) * 10,
id: '2');
}, growable: false);
series2Min = series2.map((data) => data.value).reduce(min);
series2Max = series2.map((data) => data.value).reduce(max);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Chart<GraphData>(
data: series1 + series2,
marks: [
LineMark(
shape: ShapeEncode(value: BasicLineShape()),
position:
Varset('timestamp') * Varset('series1Value') / Varset('id'),
color: ColorEncode(encoder: (data) => color1),
),
LineMark(
shape: ShapeEncode(value: BasicLineShape()),
position:
Varset('timestamp') * Varset('series2Value') / Varset('id'),
color: ColorEncode(encoder: (data) => color2),
)
],
variables: {
'timestamp': Variable(accessor: (data) => data.timestamp),
'series1Value': Variable(
accessor: (data) {
if (data.id == '1') {
return data.value;
}
return double.nan;
},
scale: LinearScale(
min: series1Min,
max: series1Max,
),
),
'series2Value': Variable(
accessor: (data) {
if (data.id == '2') {
return data.value;
}
return double.nan;
},
scale: LinearScale(
min: series2Min,
max: series2Max,
),
),
'id': Variable(accessor: (data) => data.id),
},
selections: {
'hover': PointSelection(
on: {GestureType.hover},
),
},
tooltip: TooltipGuide(
renderer: (Size size, Offset anchor,
Map<int, Map<String, dynamic>> selectedTuples) {
return [
CircleElement(
center: anchor,
radius: 5,
style: PaintStyle(fillColor: highlight),
),
];
},
selections: {'hover'},
),
axes: [
AxisGuide(
labelMapper: (_, __, ___) {
return LabelStyle(
textStyle: const TextStyle(
color: Colors.black,
),
offset: const Offset(-4, 0),
maxWidth: 32,
);
},
),
// series 1 axis
AxisGuide(
variable: 'series1Value',
labelMapper: (_, __, ___) => LabelStyle(
textStyle: TextStyle(color: color1),
offset: const Offset(0, -8),
),
),
// series 2 axis
AxisGuide(
variable: 'series2Value',
labelMapper: (_, __, ___) => LabelStyle(
textStyle: TextStyle(color: color2),
offset: const Offset(0, 8),
),
flip: true,
position: 0.99,
),
],
),
),
);
}
}
An alternative approach I tried was manually scaling the series2
values based on the min and max from both series, but the issue there is that the AxisGuide
then has the wrong values. I think there's likely a good way to accomplish this, but I haven't found it.
From what I can tell the deciding factor here is the order of the LineMark
s. Switching the order will show the tooltip on the other line. By adding a breakpoint in selection.dart
I can see that the correct index is being found for each LineMark
, but there's no additional check to see if the current LineMark
's nearestIndex
is actually closer than the previously found closest index. I'm not sure what the best way to do this is in the context of the dataflow. We calculate the nearestDistance
in point.dart
but it's not returned.
确实有这个问题,而且tooltip也不能自定义富文本,有什么解决办法吗