leeoniya / uPlot

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

auto scaling does not work with all 0 datapoints #916

Closed DreiDe closed 3 months ago

DreiDe commented 3 months ago

When all datapoints of the graph are 0 I would expect the scales.y.auto option to center the data between e.g. -50 and 50. However the graph is scaled from 0 to 100. I tried to adjust this default range from 0 to 40 however at least when using the range property the auto scaling is disables enirely.

leeoniya commented 3 months ago

However the graph is scaled from 0 to 100.

yes, there is a bias towards zero at the edge and positive values; it's not simply sticking the middle of the range in the middle of the screen under all conditions, since that would mean that you would get the same graph for all-1 data points and all-2 datapoints.

if you provide a callback for range, you should be able DIY the ranging as you want. there are a few static util functions you can use to help with this, if you dont want to handle all the logic but just refine uPlot's default behavior a bit:

https://github.com/leeoniya/uPlot/blob/4b9b92788627724c13667228fab411ac9f638f4a/dist/uPlot.d.ts#L131-L139

DreiDe commented 3 months ago

@leeoniya So you are saying that the 0 to 100 isn't hardcoded anywhere but instead a result of an internal calculation?

Shouldn't it respect the set range or min/max values anyway? I mean it is not intuitive for me that I set a range from 0 to 40 but with auto scaling enabled I get a graph with values from 0 to 100 by default.

It seems like the auto ranging function in general does not work when all datapoints are equal in the y-coordinate. When I insert e.g. only 1s, the scale on the y-Axis shows values at 0.001, not at 1 as expected. I think in such cases the values should be centered in the graph and padded by e.g. 20%.

leeoniya commented 3 months ago

So you are saying that the 0 to 100 isn't hardcoded anywhere but instead a result of an internal calculation?

it is a hard-coded fallback, specifically for flat-zero case. other "flat" values like 2 will still put zero on one side, and put 2 in the middle, so you get 0-4.

https://github.com/leeoniya/uPlot/blob/4b9b92788627724c13667228fab411ac9f638f4a/src/utils.js#L254-L255

i think this is the correct behavior. people don't want their scale jumping from centered-zero to bottom-zero as they update data from flat to a single datapoint, which is a common scenario.

there's no such concept as a "default range" in uPlot. there is only the range that gets returned by the scale.range callback. if you supply a static range, like [0,40], that's just a shorthand for () => [0,40]. the default range callback is the same as the rangeNum() static util function, with some predefined padding params, and extra: true, which is the special treatment of "edgifying" zero:

https://github.com/leeoniya/uPlot/blob/4b9b92788627724c13667228fab411ac9f638f4a/src/utils.js#L162-L171

It seems like the auto ranging function in general does not work when all datapoints are equal in the y-coordinate. When I insert e.g. only 1s, the scale on the y-Axis shows values at 0.001, not at 1 as expected.

i cannot reproduce this: https://jsfiddle.net/p5b4funo/

can you create a jsfiddle that does?

leeoniya commented 3 months ago

there are settings for softMin and softMax though, which i guess you can think of as "default" range:

https://leeoniya.github.io/uPlot/demos/soft-minmax.html

by default zero is both softMin and softMax -- it sticks to top for all-neg datasets, and to bottom for all-zero-or-pos datasets.

DreiDe commented 3 months ago

i think this is the correct behavior.

Yes, seems sensible. I believe ECharts also handles it that way. My first thought of implementation was just naively centering the values.

can you create a jsfiddle that does?

This was an issue with my implementation. I mocked the sensor and forgot, that the range is indeed from 0-40000 while only being plotted in a range of 0-40.

there are settings for softMin and softMax though, which i guess you can think of as "default" range:

The softMin/softMax parameters do unfortunately not solve the problem. They nicely set a "default" range, but once new values arrive this default range is kept until the values cross over one of the limits. For me it would be nice if the "default" range was "removed" instead once there are non 0 values.

My use case is a sensor that outputs sensor values between 0 and 40, never above or below, 5 times per second. When using the sensor the values will usually have a span of 0.5. So setting the soft limits from 0-40 is not an option, because the precision is too low. And setting them both at e.g. 4 would be a good option if i knew they had an average of 4, but I can't guarantee that.

I believe the easiest solution then is to detect when the flat-zero case is over and then automatically remove the range from the plot config (or an entirely custom range function as you suggested)?

leeoniya commented 3 months ago

I believe the easiest solution then is to detect when the flat-zero case is over and then automatically remove the range from the plot config (or an entirely custom range function as you suggested)?

if you're happy with the ranging behavior except in flat zero case, you can just wrap the default ranging util fn in your own fn that returns something different when dataMin and dataMax are zero.

DreiDe commented 3 months ago

Yes this is exactly the way I choose to implement it. Turned out to be quite easy. However it took a while to understand how things are working internally. Thanks for pointing me in the right direction.