leeoniya / uPlot

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

Auto X / Y Zoom #219

Closed EmiPhil closed 4 years ago

EmiPhil commented 4 years ago

Reference drawing:

image

Say you are trying to zoom from the point Mo.

What I'd like to do:

  1. If the cursor is in the invisible green band of height x pixels, zoom in on the X axis.
  2. If the cursor is in the invisible purple band of width x pixels, zoom in on the Y axis.
  3. If the cursor is in the invisible burgundy intersection of height/width x pixels, don't zoom at all
  4. If the cursor is in any of the four areas outside of the bands, zoom in on both the X and Y axis.

Hopefully that explanation makes sense!

Is this something that should be done via a plugin or would you consider it being part of the lib?

Thanks.

EmiPhil commented 4 years ago

A while ago I threw together a quick proof of concept while evaluating AM charts, so this kind of shows what I mean interactively (but doesn't work super great).

https://codepen.io/Akaritzu/pen/RwPGREx

leeoniya commented 4 years ago

dygraphs has something like this too (try on the home page): http://dygraphs.com/

except it only flips between either x or y but never both. from the amcharts codepen, i think having all 3 variants causes too much UI flashing during the selection...and you dont really save that much time over just doing an x zoom followed by a y zoom.

i might be open to having the dygraphs style if it doesn't add too much code or option complexity. i don't think we need to go as far as adding thresholds but just use whichever is greater of dx or dy to toggle between x or y scale. that means another option, e.g. cursor.drag: {x: true, y: true, bidi: false}

bidi might be too writing/text-centric a term. omni: false could work instead.

that being said, if what you need is the 3-variant flavor, having the 2-variant in the core won't help you much - you'll probably need to make a plugin that implements all the selection logic.

EmiPhil commented 4 years ago

So doing it both ways is fairly straightforward if it works for you.

Basically instead of setting omni to a boolean I used a number:

omni = 0 means use the default behavior omni = 1 means use omni with no threshold (the dygraph way) omni > 1 means use omni with a threshold of omni (the way I'm looking for)

leeoniya commented 4 years ago

not bad, actually.

except that the PR breaks unidirectional zooming 🤣

EmiPhil commented 4 years ago

:facepalm:

Maybe the next thing to look at is #72

leeoniya commented 4 years ago

that's a one large can of worms, my friend. even #175 wouldn't have saved you in this case.

EmiPhil commented 4 years ago

Okay new push should solve it. I was only test uni on the non omni charts! Relatively simple fix this go around.

leeoniya commented 4 years ago

if we're gonna do the threshold thing, the API has to change.

x: true, y: true already implies omnidirectional dragging. if i additionally set omni: 0, i expect it to restrict that assumption somewhat, but it actually does nothing, and omni: 1 acts as the restriction, which then is further restricted with omni: 1+. that's an ultra-confusing API.

we'll need 2 explicit opts. omni should be true/false (with a default set to whatever x/y opts imply unless explicitly set, e.g. drag.x && drag.y). then the threshold should be an additional opt. i use cursor.focus.prox in a similar capacity to focus series based on minimum cursor proximity.

or something else. i havent thought this through too hard. suggestions welcome.

EmiPhil commented 4 years ago

I guess the hard part is differentiating between omni as in the x/y window (how it is today) and omni as in the dygraph example. I took "omni" to mean "dygraph" style (so that x: true, y: true does not imply omni). Edit: Though I think omni: true does imply x and y are true!

Maybe omni is the wrong terminology?

Regardless, setting two params works for me so I can get that part done.

EmiPhil commented 4 years ago

Okay, that's done.

omni is a boolean, and omniThreshold is a number that defaults to -1 (dygraph style). If omniThreshold is positive, it is used as the threshold.

Doing it this way was kind of interesting, because you could say that a threshold of 0 would be equivalent to the window style (how it is today). I'm starting to think that the dygraph style is the edge case

So if you want we could dump the omni: true option completely, set threshold default to 0, if drag.x && drag.y we use the omni code. Setting threshold to a positive integer gives the behavior I was originally after, setting it negative gives dygraph?

leeoniya commented 4 years ago

the original intent is that drag.x and drag.y unlock dragability on those axes and i'd like to maintain these semantics. switching from omni to uni could be the path forward. effectively when both x/y are true (omni) we bring back limited unidirectional behavior via an explicit uni upper threshold. so the variants become:

x only (unidirectional, default):

x: true y: false uni: null

y only (unidirectional):

x: false y: true uni: null

x & y (omnidirectional):

x: true y: true uni: null

dygraphs (unidirectional up to 1e9, effectively Infinity):

x: true y: true uni: 1e9

your needed case (unidirectional up to 35, otherwise omnidirectional):

x: true y: true uni: 35

the last two options now share the same uni testing logic. when x: true, y: true, all unidirectional behavior will use the larger of dx or dy to determine the direction.

this feels pretty good to me; i can explain it without too many mental gymnastics.

thoughts?

EmiPhil commented 4 years ago

Yep that makes sense to me

EmiPhil commented 4 years ago

If uni is set should we infer that x and y must be true?

leeoniya commented 4 years ago

yeah, should make the code cleaner.

leeoniya commented 4 years ago

also, let's include all the above variants in a single demo. in least to most complex order.

EmiPhil commented 4 years ago

Done.

The way I got it to work was by saying that if we are in the burgundy square then use the dygraph way. In my original use case I would have made that zoom omni, but when you have a small enough threshold I think it should work for my users.

Since a uni of infinity means that the whole chart is basically the burgundy square, all works as expected.

The variations also show that setting uni to 0 == omni which I think is logical. But the value is set to null instead of default 0 so that we can skip computations if the user didn't specify uni.

leeoniya commented 4 years ago

nice. seems to work well.

final tweaks:

EmiPhil commented 4 years ago

Done!

leeoniya commented 4 years ago

awesome!

https://leeoniya.github.io/uPlot/demos/zoom-variations.html

thanks for doing the good work and sticking through my nagging :)

i'm gonna do a couple follow-ups to add it to the demos index and add comments to the typings file about how uni works.

leeoniya commented 4 years ago

did some minor refactoring tweaks here:

https://github.com/leeoniya/uPlot/commit/188e521d03015eed82bf608c92f106c0ac1e8f63#diff-0e94ed30087b62b2ab95466722c48f84

i swapped dx/dy distance testing to give dragX precedence when dx == dy.

otherwise everything should still be logically equivalent 🤞.

leeoniya commented 4 years ago

i found a small bug (fixed by 0895041)

when the uni-directional zooming is done along x, the y failed to autoscale within the zoomed x range because the mouseup handler only inspected the drag.x/drag.y opts rather than the new dragX & dragY vars.

i hoisted the vars up into a cache, next to dragging.