leeoniya / uPlot

📈 A small, fast chart for time series, lines, areas, ohlc & bars
MIT License
8.83k stars 386 forks source link

Cursor Idx does not match highlighted point when using hover.skip #1020

Closed nicolaszuts closed 1 week ago

nicolaszuts commented 1 week ago

Hi!

I've run into an issue where idx and idxs properties of cursor do not match the highlighted point in the graph. I have a custom tooltip plugin that uses the idx property to access the data value but due to the current behaviour, i can't get the tooltip value to sync with the highlighted point. This can be reproduced in your basic tooltip demo by changing the cursorMemo getter fn to the following:

const cursorMemo = {
  set: (left, top) => {
    cursLeft = left;
    cursTop = top;
  },
  get: () => ({
    left: cursLeft,
    top: cursTop,
    focus: {
      prox: 30,
      bias: 1,
    },
    hover: { skip: [3, 4, 5, 60, 65] },
  }),
};

Screenshot 2024-11-14 at 11 50 26 AM

For some additional context, what let me to this issue was my attempt to prioritize the "snapping" of the cursor into a non-zero point. I tried setting bias:1 for both focus and hover, but that didn't seem to do anything. I found and used the skip function as a plan B although it's not the ideal UX. I would prefer a prioritization mechanism based on some proximity rather than skipping zero altogether. If you have any advice on how to proceed, it would be much appreciated.

With all that being said, these are my questions: 1) how can i access the idx of the point that's being highlighted? 2) is there a way to achieve what i want without using skip? 3) and just for my own future understanding, what are the functional differences between focus and hover?

Thanks for all the hard work with this library.

leeoniya commented 1 week ago

hey, let's start with the task rather than the implementation details.

which of these are you aiming to accomplish?

is there any other chart lib that behaves as you're looking for that you can link here as example?

nicolaszuts commented 1 week ago

Thanks for the quick reply.

TL/DR: I want to snap the cursor to non-zero data points along x but up to a certain proximity threshold. Once outside the threshold, i'm okay if it snaps to a zero value. CodePen that demonstrates how it's hard for users to focus on the non-zero data when surrounded by a lot of zeros.

Context:

I have multi-series line chart to visualize time series data. the data is sampled at a constant interval. I have a lot of zero values in my data. I often have situations where a single non-zero data point is surrounded by zeroes

My tooltip is similar in functionality to the grafana time series tooltip. Much like grafana, I:

Task:

Before jumping into the details, I just want to clarify that when I say "highlighted points", i mean the points that get filled when the cursor is near them. Apologies if that's the wrong vocabulary to be using. In the image below, i consider both points to be highlighted:

Screenshot 2024-11-14 at 7 33 49 PM

My desired behaviour is to make it easier for users to focus on non-zero data. This is especially useful when there are tons of zero values surrounding 1 non-zero data point. I want the points that get highlighted/filled to be non-zero y-values up to a certain proximity threshold. Outside of that threshold, i'm okay with focusing on zero values. I also want to make sure that my tooltip is showing values for the highlighted points. I essentially want my tooltip values to be in sync with the highlighted points and i want to be able to prioritize highlighting non-zero values.

What i've tried

Seeing as i'm using the cursor.idx to access the data points for my tooltip (e.g.plot.data[seriesIdx]?.[c.idx]), I tried setting the cursor focus bias to 1. This didn't seem to change the value of cursor.idx. I also tried setting cursor hover property to ignore zero values. This also did not update the cursor.idx but i did notice that it stopped highlighting points with zero values. This leads me to believe that the idx used to determine the highlighted points is different than cursor.idx. I guess this is on purpose but i'm not sure how to keep both values synced. This leads to some confusing UX because i highlight a set of points but my tooltip shows something different.

Hope that's more clear. Thank you again.

leeoniya commented 1 week ago

the option you're looking for is called cursor.dataIdx, this gives you full control over which datapoint should be hovered/highlighted for each series relative to the actual hovered index at the cursor position. you can do all your own logic there like scanning past any adjacent zeros or nulls to the nearest non-zero point and returning the index you want highlighted.

the cursor.hover options like prox, bias, and skip are just settings which, if supplied, will get compiled to a custom cursor.dataIdx with those pre-defined thresholds and scan-past values.

you can see a demo of how this works in https://leeoniya.github.io/uPlot/demos/nearest-non-null.html

leeoniya commented 1 week ago

you can see how in TimeSeries we use the cursor.hover options instead of writing a completely custom cursor.dataIdx function.

https://github.com/grafana/grafana/blob/e331b778c1473478398325df54b66a136fee29e2/public/app/core/components/TimeSeries/utils.ts#L552-L575

but in some other panels we have more custom hover behavior and use cursor.dataIdx instead:

https://github.com/grafana/grafana/blob/e331b778c1473478398325df54b66a136fee29e2/public/app/core/components/TimelineChart/timeline.ts#L415-L437

leeoniya commented 1 week ago

each index returned by cursor.dataIdx is what ends up getting populated into the cursor.idxs array, and you can pull those values out in the setCursor hook, or whatever.