Skip to content

Commit

Permalink
Merge pull request pyqtgraph#2842 from outofculture/si-spin-improve
Browse files Browse the repository at this point in the history
Give siPrefix behavior to values of zero
  • Loading branch information
campagnola authored Jan 17, 2024
2 parents 63be2a3 + 4d2fb1f commit 261a325
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 25 deletions.
2 changes: 2 additions & 0 deletions pyqtgraph/examples/SpinBox.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.5, minStep=0.01)),
("Float with SI-prefixed units,<br>dec step=1.0, minStep=0.001",
pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=1.0, minStep=0.001)),
("Float with SI-prefixed units,<br>scaleAtZero=1e-6, step=1e-9",
pg.SpinBox(value=0, suffix='V', siPrefix=True, scaleAtZero=1e-6, step=1e-9)),
("Float with SI prefix but no suffix",
pg.SpinBox(value=1e9, siPrefix=True)),
("Float with custom formatting",
Expand Down
2 changes: 1 addition & 1 deletion pyqtgraph/util/garbage_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def __init__(self, interval=1.0, debug=False):

self.threshold = gc.get_threshold()
gc.disable()
self.timer.start(interval * 1000)
self.timer.start(int(interval * 1000))

def check(self):
#return self.debug_cycles() # uncomment to just debug cycles
Expand Down
58 changes: 36 additions & 22 deletions pyqtgraph/widgets/SpinBox.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import decimal
import re
import warnings
from math import isinf, isnan

from .. import functions as fn
Expand Down Expand Up @@ -81,7 +82,8 @@ def __init__(self, parent=None, value=0.0, **kwargs):
'prefix': '', ## string to be prepended to spin box value
'suffix': '',
'siPrefix': False, ## Set to True to display numbers with SI prefix (ie, 100pA instead of 1e-10A)

'scaleAtZero': None,

'delay': 0.3, ## delay sending wheel update signals for 300ms

'delayUntilEditFinished': True, ## do not send signals until text editing has finished
Expand Down Expand Up @@ -133,6 +135,9 @@ def setOpts(self, **opts):
orders of magnitude, such as a Reynolds number, an SI
prefix is allowed with no suffix. Default is False.
prefix (str) String to be prepended to the spin box value. Default is an empty string.
scaleAtZero (float) If siPrefix is also True, this option then sets the default SI prefix
that a value of 0 will have applied (and thus the default scale of the first
number the user types in after the SpinBox has been zeroed out).
step (float) The size of a single step. This is used when clicking the up/
down arrows, when rolling the mouse wheel, or when pressing
keyboard arrows while the widget has keyboard focus. Note that
Expand Down Expand Up @@ -370,7 +375,7 @@ def setValue(self, value=None, update=True, delaySignal=False):
changed = not fn.eq(value, prev) # use fn.eq to handle nan

if update and (changed or not bounded):
self.updateText(prev=prev)
self.updateText()

if changed:
self.sigValueChanging.emit(self, float(self.val)) ## change will be emitted in 300ms if there are no subsequent changes.
Expand Down Expand Up @@ -401,34 +406,35 @@ def stepEnabled(self):
return self.StepEnabledFlag.StepUpEnabled | self.StepEnabledFlag.StepDownEnabled

def stepBy(self, n):
if isinf(self.val) or isnan(self.val):
return
## note all steps (arrow buttons, wheel, up/down keys..) emit delayed signals only.
self.setValue(self._stepByValue(n), delaySignal=True)

n = decimal.Decimal(int(n)) ## n must be integral number of steps.
s = [decimal.Decimal(-1), decimal.Decimal(1)][n >= 0] ## determine sign of step
def _stepByValue(self, steps):
if isinf(self.val) or isnan(self.val):
return self.val
steps = int(steps)
sign = [decimal.Decimal(-1), decimal.Decimal(1)][steps >= 0]
val = self.val

for i in range(int(abs(n))):
for i in range(int(abs(steps))):
if self.opts['dec']:
if val == 0:
step = self.opts['minStep']
exp = None
else:
vs = [decimal.Decimal(-1), decimal.Decimal(1)][val >= 0]
#exp = decimal.Decimal(int(abs(val*(decimal.Decimal('1.01')**(s*vs))).log10()))
fudge = decimal.Decimal('1.01')**(s*vs) ## fudge factor. at some places, the step size depends on the step sign.
## fudge factor. at some places, the step size depends on the step sign.
fudge = decimal.Decimal('1.01') ** (sign * vs)
exp = abs(val * fudge).log10().quantize(1, decimal.ROUND_FLOOR)
step = self.opts['step'] * decimal.Decimal(10)**exp
step = self.opts['step'] * decimal.Decimal(10) ** exp
if 'minStep' in self.opts:
step = max(step, self.opts['minStep'])
val += s * step
#print "Exp:", exp, "step", step, "val", val
val += sign * step
else:
val += s*self.opts['step']
val += sign * self.opts['step']

if 'minStep' in self.opts and abs(val) < self.opts['minStep']:
val = decimal.Decimal(0)
self.setValue(val, delaySignal=True) ## note all steps (arrow buttons, wheel, up/down keys..) emit delayed signals only.
return val

def valueInRange(self, value):
if not isnan(value):
Expand All @@ -442,11 +448,11 @@ def valueInRange(self, value):
return False
return True

def updateText(self, prev=None):
def updateText(self, **kwargs):
# temporarily disable validation
self.skipValidate = True

txt = self.formatText(prev=prev)
txt = self.formatText(**kwargs)

# actually set the text
self.lineEdit().setText(txt)
Expand All @@ -455,7 +461,13 @@ def updateText(self, prev=None):
# re-enable the validation
self.skipValidate = False

def formatText(self, prev=None):
def formatText(self, **kwargs):
if 'prev' in kwargs:
warnings.warn(
"updateText and formatText no longer take prev argument. This will error after January 2025.",
DeprecationWarning,
stacklevel=2
) # TODO remove all kwargs handling here and updateText after January 2025
# get the number of decimal places to print
decimals = self.opts['decimals']
suffix = self.opts['suffix']
Expand All @@ -466,9 +478,11 @@ def formatText(self, prev=None):
if self.opts['siPrefix'] is True:
# SI prefix was requested, so scale the value accordingly

if self.val == 0 and prev is not None:
# special case: if it's zero use the previous prefix
(s, p) = fn.siScale(prev)
if self.val == 0:
if self.opts['scaleAtZero'] is not None:
(s, p) = fn.siScale(self.opts['scaleAtZero'])
else:
(s, p) = fn.siScale(self._stepByValue(1))
else:
(s, p) = fn.siScale(val)
parts = {'value': val, 'suffix': suffix, 'decimals': decimals, 'siPrefix': p, 'scaledValue': s*val, 'prefix':prefix}
Expand Down
8 changes: 8 additions & 0 deletions tests/test_stability.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@
"""
import gc
import sys
import time
import weakref
from random import randint, seed

import pyqtgraph as pg
from pyqtgraph.Qt import QtTest
from pyqtgraph.util.garbage_collector import GarbageCollector

app = pg.mkQApp()


def test_garbage_collector():
GarbageCollector(interval=0.1)
time.sleep(1)


seed(12345)

widgetTypes = [
Expand Down
21 changes: 19 additions & 2 deletions tests/widgets/test_spinbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ def test_SpinBox_defaults():
(-2500.3427, '$-2500.34', dict(int=False, format='${value:0.02f}')),
(1000, '1 k', dict(siPrefix=True, suffix="")),
(1.45e-9, 'i = 1.45e-09 A', dict(int=False, decimals=6, suffix='A', siPrefix=False, prefix='i =')),
(0, '0 mV', dict(suffix='V', siPrefix=True, scaleAtZero=1e-3)),
(0, '0 mV', dict(suffix='V', siPrefix=True, minStep=5e-6, scaleAtZero=1e-3)),
(0, '0 mV', dict(suffix='V', siPrefix=True, step=1e-3)),
(0, '0 mV', dict(suffix='V', dec=True, siPrefix=True, minStep=15e-3)),
])
def test_SpinBox_formatting(value, expected_text, opts):
sb = pg.SpinBox(**opts)
Expand All @@ -33,14 +37,27 @@ def test_SpinBox_formatting(value, expected_text, opts):
assert sb.value() == value
assert sb.text() == expected_text


def test_evalFunc():
sb = pg.SpinBox(evalFunc=lambda s: 100)

sb.lineEdit().setText('3')
sb.editingFinishedEvent()
assert sb.value() == 100

sb.lineEdit().setText('0')
sb.editingFinishedEvent()
assert sb.value() == 100


@pytest.mark.parametrize("suffix", ["", "V"])
def test_SpinBox_gui_set_value(suffix):
sb = pg.SpinBox(suffix=suffix)

sb.lineEdit().setText('0.1' + suffix)
sb.lineEdit().setText(f'0.1{suffix}')
sb.editingFinishedEvent()
assert sb.value() == 0.1

sb.lineEdit().setText('0.1 m' + suffix)
sb.lineEdit().setText(f'0.1 m{suffix}')
sb.editingFinishedEvent()
assert sb.value() == 0.1e-3

0 comments on commit 261a325

Please sign in to comment.