graphic icon indicating copy to clipboard operation
graphic copied to clipboard

Tooltips not working for multiple LineMarks

Open gregwoodio opened this issue 1 year ago • 2 comments

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 LineMarks and two Variables 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.

gregwoodio avatar Jun 02 '23 19:06 gregwoodio

From what I can tell the deciding factor here is the order of the LineMarks. 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.

gregwoodio avatar Jun 05 '23 19:06 gregwoodio

确实有这个问题,而且tooltip也不能自定义富文本,有什么解决办法吗

DevDu avatar Jan 31 '24 02:01 DevDu