Several minor bugfixes and features

- Added rate-limited mode to SignalProxy
  - Added basic text justification to LabelItem
  - ViewBox.addItem now has ignoreBounds option, which causes the item to be ignored when autoscaling
  - Added ValueLabel widget
  - Fixed some autoscaling bugs
  - InfiniteLine fix - no hilight if movable=False
This commit is contained in:
Luke Campagnola 2012-04-03 01:01:33 -04:00
parent bdef8dc4c7
commit 5a357ddb2a
9 changed files with 155 additions and 36 deletions

View File

@ -7,31 +7,35 @@ __all__ = ['SignalProxy']
class SignalProxy(QtCore.QObject): class SignalProxy(QtCore.QObject):
"""Object which collects rapid-fire signals and condenses them """Object which collects rapid-fire signals and condenses them
into a single signal. Used, for example, to prevent a SpinBox into a single signal or a rate-limited stream of signals.
from generating multiple signals when the mouse wheel is rolled Used, for example, to prevent a SpinBox from generating multiple
over it. signals when the mouse wheel is rolled over it.
Emits sigDelayed after input signals have stopped for a certain period of time. Emits sigDelayed after input signals have stopped for a certain period of time.
""" """
sigDelayed = QtCore.Signal(object) sigDelayed = QtCore.Signal(object)
def __init__(self, signal, delay=0.3, slot=None): def __init__(self, signal, delay=0.3, rateLimit=0, slot=None):
"""Initialization arguments: """Initialization arguments:
signal - a bound Signal or pyqtSignal instance signal - a bound Signal or pyqtSignal instance
delay - Time (in seconds) to wait for signals to stop before emitting (default 0.3s) delay - Time (in seconds) to wait for signals to stop before emitting (default 0.3s)
slot - Optional function to connect sigDelayed to. slot - Optional function to connect sigDelayed to.
rateLimit - (signals/sec) if greater than 0, this allows signals to stream out at a
steady rate while they are being received.
""" """
QtCore.QObject.__init__(self) QtCore.QObject.__init__(self)
signal.connect(self.signalReceived) signal.connect(self.signalReceived)
self.signal = signal self.signal = signal
self.delay = delay self.delay = delay
self.rateLimit = rateLimit
self.args = None self.args = None
self.timer = ThreadsafeTimer.ThreadsafeTimer() self.timer = ThreadsafeTimer.ThreadsafeTimer()
self.timer.timeout.connect(self.flush) self.timer.timeout.connect(self.flush)
self.block = False self.block = False
self.slot = slot self.slot = slot
self.lastFlushTime = None
if slot is not None: if slot is not None:
self.sigDelayed.connect(slot) self.sigDelayed.connect(slot)
@ -43,8 +47,20 @@ class SignalProxy(QtCore.QObject):
if self.block: if self.block:
return return
self.args = args self.args = args
self.timer.stop() if self.rateLimit == 0:
self.timer.start((self.delay*1000)+1) self.timer.stop()
self.timer.start((self.delay*1000)+1)
else:
now = time()
if self.lastFlushTime is None:
leakTime = 0
else:
lastFlush = self.lastFlushTime
leakTime = max(0, (lastFlush + (1.0 / self.rateLimit)) - now)
self.timer.stop()
self.timer.start((min(leakTime, self.delay)*1000)+1)
def flush(self): def flush(self):
"""If there is a signal queued up, send it now.""" """If there is a signal queued up, send it now."""
@ -54,6 +70,7 @@ class SignalProxy(QtCore.QObject):
self.sigDelayed.emit(self.args) self.sigDelayed.emit(self.args)
self.args = None self.args = None
self.timer.stop() self.timer.stop()
self.lastFlushTime = time()
return True return True
def disconnect(self): def disconnect(self):

View File

@ -5,8 +5,7 @@ import numpy as np
class Transform(QtGui.QTransform): class Transform(QtGui.QTransform):
"""Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate """Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate
This transform has no shear; angles are always preserved.
This transform always has 0 shear.
""" """
def __init__(self, init=None): def __init__(self, init=None):
QtGui.QTransform.__init__(self) QtGui.QTransform.__init__(self)
@ -24,6 +23,16 @@ class Transform(QtGui.QTransform):
elif isinstance(init, QtGui.QTransform): elif isinstance(init, QtGui.QTransform):
self.setFromQTransform(init) self.setFromQTransform(init)
def getScale(self):
return self._state['scale']
def getAngle(self):
return self._state['angle']
def getTranslation(self):
return self._state['pos']
def reset(self): def reset(self):
self._state = { self._state = {
'pos': Point(0,0), 'pos': Point(0,0),

View File

@ -72,7 +72,6 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al
Return the number x formatted in engineering notation with SI prefix. Return the number x formatted in engineering notation with SI prefix.
Example:: Example::
siFormat(0.0001, suffix='V') # returns "100 μV" siFormat(0.0001, suffix='V') # returns "100 μV"
""" """
@ -90,8 +89,11 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al
fmt = "%." + str(precision) + "g%s%s" fmt = "%." + str(precision) + "g%s%s"
return fmt % (x*p, pref, suffix) return fmt % (x*p, pref, suffix)
else: else:
plusminus = space + u"±" + space if allowUnicode:
fmt = "%." + str(precision) + u"g%s%s%s%s" plusminus = space + u"±" + space
else:
plusminus = " +/- "
fmt = "%." + str(precision) + "g%s%s%s%s"
return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal)) return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal))
def siEval(s): def siEval(s):

View File

@ -45,7 +45,7 @@ class GraphicsLayout(GraphicsWidget):
self.addItem(vb, row, col, rowspan, colspan) self.addItem(vb, row, col, rowspan, colspan)
return vb return vb
def addLabel(self, text, row=None, col=None, rowspan=1, colspan=1, **kargs): def addLabel(self, text=' ', row=None, col=None, rowspan=1, colspan=1, **kargs):
text = LabelItem(text, **kargs) text = LabelItem(text, **kargs)
self.addItem(text, row, col, rowspan, colspan) self.addItem(text, row, col, rowspan, colspan)
return text return text

View File

@ -221,7 +221,7 @@ class InfiniteLine(UIGraphicsItem):
self.sigPositionChangeFinished.emit(self) self.sigPositionChangeFinished.emit(self)
def hoverEvent(self, ev): def hoverEvent(self, ev):
if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): if (not ev.isExit()) and self.movable and ev.acceptDrags(QtCore.Qt.LeftButton):
self.currentPen = fn.mkPen(255, 0,0) self.currentPen = fn.mkPen(255, 0,0)
else: else:
self.currentPen = self.pen self.currentPen = self.pen

View File

@ -14,15 +14,14 @@ class LabelItem(GraphicsWidget):
""" """
def __init__(self, text, parent=None, angle=0, **args): def __init__(self, text=' ', parent=None, angle=0, **args):
GraphicsWidget.__init__(self, parent) GraphicsWidget.__init__(self, parent)
self.item = QtGui.QGraphicsTextItem(self) self.item = QtGui.QGraphicsTextItem(self)
self.opts = args self.opts = {
if 'color' not in args: 'color': 'CCC',
self.opts['color'] = 'CCC' 'justify': 'center'
else: }
if isinstance(args['color'], QtGui.QColor): self.opts.update(args)
self.opts['color'] = fn.colorStr(args['color'])[:6]
self.sizeHint = {} self.sizeHint = {}
self.setText(text) self.setText(text)
self.setAngle(angle) self.setAngle(angle)
@ -47,6 +46,8 @@ class LabelItem(GraphicsWidget):
optlist = [] optlist = []
if 'color' in opts: if 'color' in opts:
if isinstance(opts['color'], QtGui.QColor):
opts['color'] = fn.colorStr(opts['color'])[:6]
optlist.append('color: #' + opts['color']) optlist.append('color: #' + opts['color'])
if 'size' in opts: if 'size' in opts:
optlist.append('font-size: ' + opts['size']) optlist.append('font-size: ' + opts['size'])
@ -58,13 +59,25 @@ class LabelItem(GraphicsWidget):
#print full #print full
self.item.setHtml(full) self.item.setHtml(full)
self.updateMin() self.updateMin()
self.resizeEvent(None)
self.update()
def resizeEvent(self, ev): def resizeEvent(self, ev):
c1 = self.boundingRect().center() #c1 = self.boundingRect().center()
c2 = self.item.mapToParent(self.item.boundingRect().center()) # + self.item.pos() #c2 = self.item.mapToParent(self.item.boundingRect().center()) # + self.item.pos()
dif = c1 - c2 #dif = c1 - c2
self.item.moveBy(dif.x(), dif.y()) #self.item.moveBy(dif.x(), dif.y())
#print c1, c2, dif, self.item.pos() #print c1, c2, dif, self.item.pos()
if self.opts['justify'] == 'left':
self.item.setPos(0,0)
elif self.opts['justify'] == 'center':
bounds = self.item.mapRectToParent(self.item.boundingRect())
self.item.setPos(self.width()/2. - bounds.width()/2., 0)
elif self.opts['justify'] == 'right':
bounds = self.item.mapRectToParent(self.item.boundingRect())
self.item.setPos(self.width() - bounds.width(), 0)
#if self.width() > 0:
#self.item.setTextWidth(self.width())
def setAngle(self, angle): def setAngle(self, angle):
self.angle = angle self.angle = angle
@ -76,16 +89,23 @@ class LabelItem(GraphicsWidget):
bounds = self.item.mapRectToParent(self.item.boundingRect()) bounds = self.item.mapRectToParent(self.item.boundingRect())
self.setMinimumWidth(bounds.width()) self.setMinimumWidth(bounds.width())
self.setMinimumHeight(bounds.height()) self.setMinimumHeight(bounds.height())
#print self.text, bounds.width(), bounds.height()
#self.sizeHint = { self.sizeHint = {
#QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()), QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()),
#QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()), QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()),
#QtCore.Qt.MaximumSize: (bounds.width()*2, bounds.height()*2), QtCore.Qt.MaximumSize: (-1, -1), #bounds.width()*2, bounds.height()*2),
#QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this? QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this?
#} }
self.update()
#def sizeHint(self, hint, constraint): def sizeHint(self, hint, constraint):
#return self.sizeHint[hint] if hint not in self.sizeHint:
return QtCore.QSizeF(0, 0)
return QtCore.QSizeF(*self.sizeHint[hint])
#def paint(self, p, *args):
#p.setPen(fn.mkPen('r'))
#p.drawRect(self.rect())
#p.drawRect(self.item.boundingRect())

View File

@ -690,7 +690,10 @@ class PlotItem(GraphicsWidget):
def addItem(self, item, *args, **kargs): def addItem(self, item, *args, **kargs):
self.items.append(item) self.items.append(item)
self.vb.addItem(item, *args) vbargs = {}
if 'ignoreBounds' in kargs:
vbargs['ignoreBounds'] = kargs['ignoreBounds']
self.vb.addItem(item, *args, **vbargs)
if hasattr(item, 'implements') and item.implements('plotData'): if hasattr(item, 'implements') and item.implements('plotData'):
self.dataItems.append(item) self.dataItems.append(item)
#self.plotChanged() #self.plotChanged()

View File

@ -197,11 +197,12 @@ class ViewBox(GraphicsWidget):
def mouseEnabled(self): def mouseEnabled(self):
return self.state['mouseEnabled'][:] return self.state['mouseEnabled'][:]
def addItem(self, item): def addItem(self, item, ignoreBounds=False):
if item.zValue() < self.zValue(): if item.zValue() < self.zValue():
item.setZValue(self.zValue()+1) item.setZValue(self.zValue()+1)
item.setParentItem(self.childGroup) item.setParentItem(self.childGroup)
self.addedItems.append(item) if not ignoreBounds:
self.addedItems.append(item)
self.updateAutoRange() self.updateAutoRange()
#print "addItem:", item, item.boundingRect() #print "addItem:", item, item.boundingRect()

67
widgets/ValueLabel.py Normal file
View File

@ -0,0 +1,67 @@
from pyqtgraph.Qt import QtCore, QtGui
from pyqtgraph.ptime import time
import pyqtgraph as pg
__all__ = ['ValueLabel']
class ValueLabel(QtGui.QLabel):
"""
QLabel specifically for displaying numerical values.
Extends QLabel adding some extra functionality:
- displaying units with si prefix
- built-in exponential averaging
"""
def __init__(self, parent=None, suffix='', siPrefix=False, averageTime=0, formatStr=None):
"""
Arguments:
*suffix* (str or None) The suffix to place after the value
*siPrefix* (bool) Whether to add an SI prefix to the units and display a scaled value
*averageTime* (float) The length of time in seconds to average values. If this value
is 0, then no averaging is performed. As this value increases
the display value will appear to change more slowly and smoothly.
*formatStr* (str) Optionally, provide a format string to use when displaying text. The text will
be generated by calling formatStr.format(value=, avgValue=, suffix=)
(see Python documentation on str.format)
This option is not compatible with siPrefix
"""
QtGui.QLabel.__init__(self, parent)
self.values = []
self.averageTime = averageTime ## no averaging by default
self.suffix = suffix
self.siPrefix = siPrefix
if formatStr is None:
formatStr = '{avgValue:0.2g} {suffix}'
self.formatStr = formatStr
def setValue(self, value):
now = time()
self.values.append((now, value))
cutoff = now - self.averageTime
while len(self.values) > 0 and self.values[0][0] < cutoff:
self.values.pop(0)
self.update()
def setFormatStr(self, text):
self.formatStr = text
self.update()
def averageValue(self):
return reduce(lambda a,b: a+b, [v[1] for v in self.values]) / float(len(self.values))
def paintEvent(self, ev):
self.setText(self.generateText())
return QtGui.QLabel.paintEvent(self, ev)
def generateText(self):
if len(self.values) == 0:
return ''
avg = self.averageValue()
val = self.values[-1][1]
if self.siPrefix:
return pg.siFormat(avg, suffix=self.suffix)
else:
return self.formatStr.format(value=val, avgValue=avg, suffix=self.suffix)