leeoniya / uPlot

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

Tooltips or an API to support their implementation #63

Closed silverwind closed 4 years ago

silverwind commented 4 years ago

I like to have tooltips instead of the legend to show the currently hovered value. Would you consider implementing tooltips yourself or alternatively provide an API like getValuesAtCursor to allow a implementation with libraries like popper.js? Something like this:

https://codepen.io/FezVrasta/pen/bWezaY

leeoniya commented 4 years ago

i definitely want to flesh out an event api to make extensibility possible, but probably not much beyond that.

thinking bigger picture, what can be useful for an onmove cursor event besides values? also, providing values as an array can be sub-optimal even with the existing rAF-debouncing, since it'll need to allocate up yo 60 arrays/s.

i think providing the index within the data should be sufficient, and the user can easily read off the raw values directly from the data struct.

the left,top position of the cursor would be needed.

an exposed method of translating a position into a value along a desired scale. (and vice-versa).

{onmove: (leftPos, topPos, idx) => {}}
.idxAt(leftPos)
.valAt(pos, scaleKey)
.posOf(val, scaleKey)

idx in onmove is not technically necessary since it can be queried via .idxAt(), but if the cursor intersection points are already being shown along each series, then this call is already done internally, so passing it through probably makes sense for perf. i guess this brings up the question of why not also pass the coords of the points through? probably cause translating a data value to a scale pos (redundantly) and returning a plain number is cheaper than either allocating an object/array for all values or searching a potentially multi-million-datapoint array for the nearest index redundantly.

thoughts?

leeoniya commented 4 years ago

ok, this turned out pretty well:

https://leeoniya.github.io/uPlot/demos/tooltips.html

silverwind commented 4 years ago

The reason I linked the popper.js example is that it is able to deal with tooltip overflow. I see two ways a tooltip can overflow and which one to choose depends on chart size and tooltip size:

  1. If the chart is bigger than tooltip, confine tooltip to the chart area and reposition it once it get to the right/bottom edges (for right/bottom anchored tooltips).
  2. If the chart is about equal or smaller than the tooltip (think sparklines), confine the tooltip to the page. It is important to me that this works even when overflow: hidden is set on the chart or any of its parent elements.

I strongly recommend either popper.js or maybe the lightweight placement.js for the tooltip placement. They both allow to set a bounding element that could be set depending on chart and expected tooltip size and they put their rendered elements inside a "portal" DOM node on body to avoid any overflow or z-index issues.

So my suggestion would be a mousemove event with access to:

leeoniya commented 4 years ago

the demo is just a demo. i think all the pieces are there to do what you need with popper.js.

the main reason i dont want to simply pass through the mousemove events is because they're rAF-debounced for reducing unnecesaary cursor, legend and focus updates. i suppose i can cache the raw event object internally until the rAF fires and pass it along with the other arguments.

however, everything you mentioned is already attainable without raw event access.

I could see this (presumably) O(n) calculation getting expensive on big charts.

it's a binary search, so O(log n).

silverwind commented 4 years ago

Yes, looks like the necessities are all there, I will try to set up a demo using placement.js and report back.

silverwind commented 4 years ago

https://codesandbox.io/s/uplot-tooltip-placement-l4b9u

I'm happy with the result. To change it to be confied to the chart area, set bound = plot. I opted to hide .cursor-hz always and only show .cursor-vt when the mouse is over the chart. Maybe this is something you want to consider supporting more directly (e.g. a option to hide a cursor line and some methods to show/hide them). Also maybe cursorenter and cursorleave events might be useful.

(For some reason that Sandbox only works in Firefox for me)

leeoniya commented 4 years ago

Maybe this is something you want to consider supporting more directly

i'd like to keep the opts minimal and delegate to css styling or dom whenever possible.

Also maybe cursorenter and cursorleave events might be useful.

there's nothing internally that tracks this right now, so it'll be adding code & event listeners for the sake of adding them. could be useful in the future if someone has a burning need for it, but i'll leave this alone for now. these events would coincide with plain old mouseleave and mouseenter events which could just be attached and handled without uplot's help since there's not much useful data they can carry besides their already-accessible exit/entry coords.

heads up, i'll probably be changing cursor-vt/hz to cursor-x/y before v1.0 (for consistency).

silverwind commented 4 years ago

there's nothing internally that tracks this right now

One thing I do not like about the default cursor lines is that they get stuck on the chart after mouseleave. I think it would be more expected if they properly show/hide with the cursor entering/leaving. If you want to do that as well, I guess you would need those events internally.

(Also my demo has a bug that if the cursor is over the chart while the page is loading, the tooltip will not show, so the style would needed to be toggled on on cursormove too.)

leeoniya commented 4 years ago

yeah those cursor improvements sound reasonable. not sure if leave/enter would be better than some cursortoggle that would trigger as a result of enter/leave.

i do want to keep the cursor always visible if it's clicked/locked.

edit: another benefit of having the cursor off by default is that i can stop moving it to the center on initial render, and thus avoid a move event.

leeoniya commented 4 years ago

@silverwind

now that the final API is mostly baked, here's how i'd do it:

https://jsfiddle.net/bg1uLx8s/

since uPlot does not handle the final dom insertion, it's necessary for the plugin to attach a mutation observer so we can avoid calling the rather expensive getBoundingClientRect on every mousemove, since its results are bogus at time of initial init & setCursor.

this isn't the only way to get things done but it does the trick.

i'll make a note to document that these hooks fire before dom insertion.

leeoniya commented 4 years ago

ok, this inelegance was bugging me.

i added a ready callback to the constructor which will defer firing hooks until e.g. dom insertion. i figured this was more flexible than adding target and assuming appendChild or similar.

anyways, this cleaned things up significantly: https://github.com/leeoniya/uPlot/commit/7f3c51e42971779744afc1010ce3a24d5489cb09#diff-b12b7f66b7a99297dec560a423dd5ed6

leeoniya commented 4 years ago

the third constructor param can now accept an HTMLElement container to append into. this signature also defers firing hooks until after dom insertion.

https://github.com/leeoniya/uPlot/blob/master/demos/cursor-tooltip.html#L108