pyqtgraph / pyqtgraph

Fast data visualization and GUI tools for scientific / engineering applications
https://www.pyqtgraph.org
Other
3.85k stars 1.1k forks source link

Request: limit upper/bottom units when auto SI-scaling #3126

Open nicoddemus opened 3 weeks ago

nicoddemus commented 3 weeks ago

Hi folks,

First of all thanks a lot for all the work put into pyqtgraph, it is definitely one of the best graph libraries for Python/Qt out there.

Short description

Currently it is possible to auto-scale axes using SI notation, which is very neat and useful.

It would be nice to be able to define bottom and upper units when auto-scaling SI units.

Currently it goes all the way down to yocto and up to yotta:

https://github.com/pyqtgraph/pyqtgraph/blob/442f08a476644e7cac1babbb3076a767a265800f/pyqtgraph/functions.py#L62-L63

After that, it switches to scientific notation.

Depending on the application domain, it is interesting to specify a maximum bottom/upper unit that could be scaled to (for example micro/Mega), and after that switch to scientific notation.

Additional context

I don't have a suggestion on how to accomplish this, but I would say ideally it would be nice to be able to configure this per-plot (or per-axis), as opposed to a global setting.

ullmannJan commented 3 weeks ago

Additionally it would be nice to change the behavior depending on whether it is in log-scale. For log-scale axes si-auto-scaling does not really make sense in my opinion.

outofculture commented 2 weeks ago

Hmm. I can think of a few ways to get this done.

  1. specify the range(s) in which si prefixing should happen. e.g. siPrefixEnableRanges=((1e-12, 0), (1e9, 1e15))
    • This accomplishes the main request, but does nothing to address the logMode add-on
  2. pass in a function that lets you arbitrarily control the scale/prefix using the AxisItem directly. e.g. siScalingFunction=lambda axis: (1.0, '') if axis.logMode else fn.siScale(max(abs(axis.range[0] * axis.scale)), abs(axis.range[1] * axis.scale)))
    • This offers the most power, but will be confusing to use. (for instance, how would we communicate the need to multiply by the axis.scale value?)
  3. pass in a function that takes a few clear arguments, unscaled_range_low, unscaled_range_high (and maybe also log_mode) e.g. siScalingFunction=lambda *rng: (1.0, '') if (val := max(abs(rng[0]), abs(rng[1]))) > 1e15 else fn.siScale(val)
    • This is still a bit confusing, and will be brittle to requests that more information be provided to the scaling function.

Anyone have any other ideas or a preference between these? I think my slight preference is # 2, with a warning to read the existing code before writing anything custom.

nicoddemus commented 2 weeks ago

I personally prefer 1 because it is simpler, 2 does offer more power but as mentioned it is more complex and also I'm not sure which real use case it covers?

But at the end of the day I would be happy with either.

ullmannJan commented 2 weeks ago

I would also choose option 1. It is easy to document and also easy to use.

Furthermore, it would be great to implement a second keyword argument in the enableAutoSIPrefix() function to enable or disable it for log scaling. This could of course also be done with just another function enableAutoSIPrefixInLog()

outofculture commented 2 weeks ago

I have a PR that doesn't address the logMode add-on, but I wanted to get some feedback on my implementation before continuing. How's it look?