Merge branch 'develop' into core

This commit is contained in:
Luke Campagnola 2014-04-15 15:11:19 -04:00
commit c7f4a8fd39
95 changed files with 4153 additions and 1594 deletions

View File

@ -92,15 +92,11 @@ class GraphicsScene(QtGui.QGraphicsScene):
self.clickEvents = [] self.clickEvents = []
self.dragButtons = [] self.dragButtons = []
self.prepItems = weakref.WeakKeyDictionary() ## set of items with prepareForPaintMethods
self.mouseGrabber = None self.mouseGrabber = None
self.dragItem = None self.dragItem = None
self.lastDrag = None self.lastDrag = None
self.hoverItems = weakref.WeakKeyDictionary() self.hoverItems = weakref.WeakKeyDictionary()
self.lastHoverEvent = None self.lastHoverEvent = None
#self.searchRect = QtGui.QGraphicsRectItem()
#self.searchRect.setPen(fn.mkPen(200,0,0))
#self.addItem(self.searchRect)
self.contextMenu = [QtGui.QAction("Export...", self)] self.contextMenu = [QtGui.QAction("Export...", self)]
self.contextMenu[0].triggered.connect(self.showExportDialog) self.contextMenu[0].triggered.connect(self.showExportDialog)
@ -437,10 +433,10 @@ class GraphicsScene(QtGui.QGraphicsScene):
for item in items: for item in items:
if hoverable and not hasattr(item, 'hoverEvent'): if hoverable and not hasattr(item, 'hoverEvent'):
continue continue
shape = item.shape() shape = item.shape() # Note: default shape() returns boundingRect()
if shape is None: if shape is None:
continue continue
if item.mapToScene(shape).contains(point): if shape.contains(item.mapFromScene(point)):
items2.append(item) items2.append(item)
## Sort by descending Z-order (don't trust scene.itms() to do this either) ## Sort by descending Z-order (don't trust scene.itms() to do this either)

View File

@ -131,8 +131,12 @@ class MouseDragEvent(object):
return self.finish return self.finish
def __repr__(self): def __repr__(self):
lp = self.lastPos() if self.currentItem is None:
p = self.pos() lp = self._lastScenePos
p = self._scenePos
else:
lp = self.lastPos()
p = self.pos()
return "<MouseDragEvent (%g,%g)->(%g,%g) buttons=%d start=%s finish=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isStart()), str(self.isFinish())) return "<MouseDragEvent (%g,%g)->(%g,%g) buttons=%d start=%s finish=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isStart()), str(self.isFinish()))
def modifiers(self): def modifiers(self):
@ -221,9 +225,15 @@ class MouseClickEvent(object):
return self._modifiers return self._modifiers
def __repr__(self): def __repr__(self):
p = self.pos() try:
return "<MouseClickEvent (%g,%g) button=%d>" % (p.x(), p.y(), int(self.button())) if self.currentItem is None:
p = self._scenePos
else:
p = self.pos()
return "<MouseClickEvent (%g,%g) button=%d>" % (p.x(), p.y(), int(self.button()))
except:
return "<MouseClickEvent button=%d>" % (int(self.button()))
def time(self): def time(self):
return self._time return self._time
@ -345,8 +355,12 @@ class HoverEvent(object):
return Point(self.currentItem.mapFromScene(self._lastScenePos)) return Point(self.currentItem.mapFromScene(self._lastScenePos))
def __repr__(self): def __repr__(self):
lp = self.lastPos() if self.currentItem is None:
p = self.pos() lp = self._lastScenePos
p = self._scenePos
else:
lp = self.lastPos()
p = self.pos()
return "<HoverEvent (%g,%g)->(%g,%g) buttons=%d enter=%s exit=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isEnter()), str(self.isExit())) return "<HoverEvent (%g,%g)->(%g,%g) buttons=%d enter=%s exit=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isEnter()), str(self.isExit()))
def modifiers(self): def modifiers(self):

20
Qt.py
View File

@ -32,6 +32,23 @@ else:
if USE_PYSIDE: if USE_PYSIDE:
from PySide import QtGui, QtCore, QtOpenGL, QtSvg from PySide import QtGui, QtCore, QtOpenGL, QtSvg
import PySide import PySide
try:
from PySide import shiboken
isQObjectAlive = shiboken.isValid
except ImportError:
def isQObjectAlive(obj):
try:
if hasattr(obj, 'parent'):
obj.parent()
elif hasattr(obj, 'parentItem'):
obj.parentItem()
else:
raise Exception("Cannot determine whether Qt object %s is still alive." % obj)
except RuntimeError:
return False
else:
return True
VERSION_INFO = 'PySide ' + PySide.__version__ VERSION_INFO = 'PySide ' + PySide.__version__
# Make a loadUiType function like PyQt has # Make a loadUiType function like PyQt has
@ -78,6 +95,9 @@ else:
pass pass
import sip
def isQObjectAlive(obj):
return not sip.isdeleted(obj)
loadUiType = uic.loadUiType loadUiType = uic.loadUiType
QtCore.Signal = QtCore.pyqtSignal QtCore.Signal = QtCore.pyqtSignal

View File

@ -4,7 +4,6 @@ from .Vector import Vector
from .Transform3D import Transform3D from .Transform3D import Transform3D
from .Vector import Vector from .Vector import Vector
import numpy as np import numpy as np
import scipy.linalg
class SRTTransform3D(Transform3D): class SRTTransform3D(Transform3D):
"""4x4 Transform matrix that can always be represented as a combination of 3 matrices: scale * rotate * translate """4x4 Transform matrix that can always be represented as a combination of 3 matrices: scale * rotate * translate
@ -118,11 +117,13 @@ class SRTTransform3D(Transform3D):
The input matrix must be affine AND have no shear, The input matrix must be affine AND have no shear,
otherwise the conversion will most likely fail. otherwise the conversion will most likely fail.
""" """
import numpy.linalg
for i in range(4): for i in range(4):
self.setRow(i, m.row(i)) self.setRow(i, m.row(i))
m = self.matrix().reshape(4,4) m = self.matrix().reshape(4,4)
## translation is 4th column ## translation is 4th column
self._state['pos'] = m[:3,3] self._state['pos'] = m[:3,3]
## scale is vector-length of first three columns ## scale is vector-length of first three columns
scale = (m[:3,:3]**2).sum(axis=0)**0.5 scale = (m[:3,:3]**2).sum(axis=0)**0.5
## see whether there is an inversion ## see whether there is an inversion
@ -132,9 +133,9 @@ class SRTTransform3D(Transform3D):
self._state['scale'] = scale self._state['scale'] = scale
## rotation axis is the eigenvector with eigenvalue=1 ## rotation axis is the eigenvector with eigenvalue=1
r = m[:3, :3] / scale[:, np.newaxis] r = m[:3, :3] / scale[np.newaxis, :]
try: try:
evals, evecs = scipy.linalg.eig(r) evals, evecs = numpy.linalg.eig(r)
except: except:
print("Rotation matrix: %s" % str(r)) print("Rotation matrix: %s" % str(r))
print("Scale: %s" % str(scale)) print("Scale: %s" % str(scale))

View File

@ -67,4 +67,19 @@ class Vector(QtGui.QVector3D):
yield(self.x()) yield(self.x())
yield(self.y()) yield(self.y())
yield(self.z()) yield(self.z())
def angle(self, a):
"""Returns the angle in degrees between this vector and the vector a."""
n1 = self.length()
n2 = a.length()
if n1 == 0. or n2 == 0.:
return None
## Probably this should be done with arctan2 instead..
ang = np.arccos(np.clip(QtGui.QVector3D.dotProduct(self, a) / (n1 * n2), -1.0, 1.0)) ### in radians
# c = self.crossProduct(a)
# if c > 0:
# ang *= -1.
return ang * 180. / np.pi

View File

@ -52,10 +52,11 @@ CONFIG_OPTIONS = {
'background': 'k', ## default background for GraphicsWidget 'background': 'k', ## default background for GraphicsWidget
'antialias': False, 'antialias': False,
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets 'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
'useWeave': True, ## Use weave to speed up some operations, if it is available 'useWeave': False, ## Use weave to speed up some operations, if it is available
'weaveDebug': False, ## Print full error message if weave compile fails 'weaveDebug': False, ## Print full error message if weave compile fails
'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide 'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide
'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code) 'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code)
'crashWarning': False, # If True, print warnings about situations that may result in a crash
} }
@ -256,6 +257,7 @@ from .graphicsWindows import *
from .SignalProxy import * from .SignalProxy import *
from .colormap import * from .colormap import *
from .ptime import time from .ptime import time
from pyqtgraph.Qt import isQObjectAlive
############################################################## ##############################################################
@ -284,7 +286,12 @@ def cleanup():
s = QtGui.QGraphicsScene() s = QtGui.QGraphicsScene()
for o in gc.get_objects(): for o in gc.get_objects():
try: try:
if isinstance(o, QtGui.QGraphicsItem) and o.scene() is None: if isinstance(o, QtGui.QGraphicsItem) and isQObjectAlive(o) and o.scene() is None:
if getConfigOption('crashWarning'):
sys.stderr.write('Error: graphics item without scene. '
'Make sure ViewBox.close() and GraphicsView.close() '
'are properly called before app shutdown (%s)\n' % (o,))
s.addItem(o) s.addItem(o)
except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object
continue continue
@ -393,6 +400,7 @@ def dbg(*args, **kwds):
consoles.append(c) consoles.append(c)
except NameError: except NameError:
consoles = [c] consoles = [c]
return c
def mkQApp(): def mkQApp():

View File

@ -431,9 +431,12 @@ class CanvasItem(QtCore.QObject):
def selectionChanged(self, sel, multi): def selectionChanged(self, sel, multi):
""" """
Inform the item that its selection state has changed. Inform the item that its selection state has changed.
Arguments: ============== =========================================================
sel: bool, whether the item is currently selected **Arguments:**
multi: bool, whether there are multiple items currently selected sel (bool) whether the item is currently selected
multi (bool) whether there are multiple items currently
selected
============== =========================================================
""" """
self.selectedAlone = sel and not multi self.selectedAlone = sel and not multi
self.showSelectBox() self.showSelectBox()

View File

@ -1,5 +1,4 @@
import numpy as np import numpy as np
import scipy.interpolate
from .Qt import QtGui, QtCore from .Qt import QtGui, QtCore
class ColorMap(object): class ColorMap(object):
@ -52,20 +51,20 @@ class ColorMap(object):
def __init__(self, pos, color, mode=None): def __init__(self, pos, color, mode=None):
""" """
========= ============================================================== =============== ==============================================================
Arguments **Arguments:**
pos Array of positions where each color is defined pos Array of positions where each color is defined
color Array of RGBA colors. color Array of RGBA colors.
Integer data types are interpreted as 0-255; float data types Integer data types are interpreted as 0-255; float data types
are interpreted as 0.0-1.0 are interpreted as 0.0-1.0
mode Array of color modes (ColorMap.RGB, HSV_POS, or HSV_NEG) mode Array of color modes (ColorMap.RGB, HSV_POS, or HSV_NEG)
indicating the color space that should be used when indicating the color space that should be used when
interpolating between stops. Note that the last mode value is interpolating between stops. Note that the last mode value is
ignored. By default, the mode is entirely RGB. ignored. By default, the mode is entirely RGB.
========= ============================================================== =============== ==============================================================
""" """
self.pos = pos self.pos = np.array(pos)
self.color = color self.color = np.array(color)
if mode is None: if mode is None:
mode = np.ones(len(pos)) mode = np.ones(len(pos))
self.mode = mode self.mode = mode
@ -92,15 +91,24 @@ class ColorMap(object):
else: else:
pos, color = self.getStops(mode) pos, color = self.getStops(mode)
data = np.clip(data, pos.min(), pos.max()) # don't need this--np.interp takes care of it.
#data = np.clip(data, pos.min(), pos.max())
if not isinstance(data, np.ndarray): # Interpolate
interp = scipy.interpolate.griddata(pos, color, np.array([data]))[0] # TODO: is griddata faster?
# interp = scipy.interpolate.griddata(pos, color, data)
if np.isscalar(data):
interp = np.empty((color.shape[1],), dtype=color.dtype)
else: else:
interp = scipy.interpolate.griddata(pos, color, data)
if mode == self.QCOLOR:
if not isinstance(data, np.ndarray): if not isinstance(data, np.ndarray):
data = np.array(data)
interp = np.empty(data.shape + (color.shape[1],), dtype=color.dtype)
for i in range(color.shape[1]):
interp[...,i] = np.interp(data, pos, color[:,i])
# Convert to QColor if requested
if mode == self.QCOLOR:
if np.isscalar(data):
return QtGui.QColor(*interp) return QtGui.QColor(*interp)
else: else:
return [QtGui.QColor(*x) for x in interp] return [QtGui.QColor(*x) for x in interp]
@ -193,16 +201,16 @@ class ColorMap(object):
""" """
Return an RGB(A) lookup table (ndarray). Return an RGB(A) lookup table (ndarray).
============= ============================================================================ =============== =============================================================================
**Arguments** **Arguments:**
start The starting value in the lookup table (default=0.0) start The starting value in the lookup table (default=0.0)
stop The final value in the lookup table (default=1.0) stop The final value in the lookup table (default=1.0)
nPts The number of points in the returned lookup table. nPts The number of points in the returned lookup table.
alpha True, False, or None - Specifies whether or not alpha values are included alpha True, False, or None - Specifies whether or not alpha values are included
in the table. If alpha is None, it will be automatically determined. in the table. If alpha is None, it will be automatically determined.
mode Determines return type: 'byte' (0-255), 'float' (0.0-1.0), or 'qcolor'. mode Determines return type: 'byte' (0-255), 'float' (0.0-1.0), or 'qcolor'.
See :func:`map() <pyqtgraph.ColorMap.map>`. See :func:`map() <pyqtgraph.ColorMap.map>`.
============= ============================================================================ =============== =============================================================================
""" """
if isinstance(mode, basestring): if isinstance(mode, basestring):
mode = self.enumMap[mode.lower()] mode = self.enumMap[mode.lower()]

View File

@ -31,16 +31,16 @@ class ConsoleWidget(QtGui.QWidget):
def __init__(self, parent=None, namespace=None, historyFile=None, text=None, editor=None): def __init__(self, parent=None, namespace=None, historyFile=None, text=None, editor=None):
""" """
============ ============================================================================ ============== ============================================================================
Arguments: **Arguments:**
namespace dictionary containing the initial variables present in the default namespace namespace dictionary containing the initial variables present in the default namespace
historyFile optional file for storing command history historyFile optional file for storing command history
text initial text to display in the console window text initial text to display in the console window
editor optional string for invoking code editor (called when stack trace entries are editor optional string for invoking code editor (called when stack trace entries are
double-clicked). May contain {fileName} and {lineNum} format keys. Example:: double-clicked). May contain {fileName} and {lineNum} format keys. Example::
editorCommand --loadfile {fileName} --gotoline {lineNum} editorCommand --loadfile {fileName} --gotoline {lineNum}
============ ============================================================================= ============== =============================================================================
""" """
QtGui.QWidget.__init__(self, parent) QtGui.QWidget.__init__(self, parent)
if namespace is None: if namespace is None:

171
debug.py
View File

@ -7,10 +7,12 @@ Distributed under MIT/X11 license. See license.txt for more infomation.
from __future__ import print_function from __future__ import print_function
import sys, traceback, time, gc, re, types, weakref, inspect, os, cProfile import sys, traceback, time, gc, re, types, weakref, inspect, os, cProfile, threading
from . import ptime from . import ptime
from numpy import ndarray from numpy import ndarray
from .Qt import QtCore, QtGui from .Qt import QtCore, QtGui
from .util.mutex import Mutex
from .util import cprint
__ftraceDepth = 0 __ftraceDepth = 0
def ftrace(func): def ftrace(func):
@ -238,7 +240,8 @@ def refPathString(chain):
def objectSize(obj, ignore=None, verbose=False, depth=0, recursive=False): def objectSize(obj, ignore=None, verbose=False, depth=0, recursive=False):
"""Guess how much memory an object is using""" """Guess how much memory an object is using"""
ignoreTypes = [types.MethodType, types.UnboundMethodType, types.BuiltinMethodType, types.FunctionType, types.BuiltinFunctionType] ignoreTypes = ['MethodType', 'UnboundMethodType', 'BuiltinMethodType', 'FunctionType', 'BuiltinFunctionType']
ignoreTypes = [getattr(types, key) for key in ignoreTypes if hasattr(types, key)]
ignoreRegex = re.compile('(method-wrapper|Flag|ItemChange|Option|Mode)') ignoreRegex = re.compile('(method-wrapper|Flag|ItemChange|Option|Mode)')
@ -399,7 +402,9 @@ class Profiler(object):
only the initial "pyqtgraph." prefix from the module. only the initial "pyqtgraph." prefix from the module.
""" """
_profilers = os.environ.get("PYQTGRAPHPROFILE", "") _profilers = os.environ.get("PYQTGRAPHPROFILE", None)
_profilers = _profilers.split(",") if _profilers is not None else []
_depth = 0 _depth = 0
_msgs = [] _msgs = []
@ -415,38 +420,36 @@ class Profiler(object):
_disabledProfiler = DisabledProfiler() _disabledProfiler = DisabledProfiler()
if _profilers: def __new__(cls, msg=None, disabled='env', delayed=True):
_profilers = _profilers.split(",") """Optionally create a new profiler based on caller's qualname.
def __new__(cls, msg=None, disabled='env', delayed=True): """
"""Optionally create a new profiler based on caller's qualname. if disabled is True or (disabled=='env' and len(cls._profilers) == 0):
""" return cls._disabledProfiler
if disabled is True:
return cls._disabledProfiler # determine the qualified name of the caller function
caller_frame = sys._getframe(1)
# determine the qualified name of the caller function try:
caller_frame = sys._getframe(1) caller_object_type = type(caller_frame.f_locals["self"])
try: except KeyError: # we are in a regular function
caller_object_type = type(caller_frame.f_locals["self"]) qualifier = caller_frame.f_globals["__name__"].split(".", 1)[-1]
except KeyError: # we are in a regular function else: # we are in a method
qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1] qualifier = caller_object_type.__name__
else: # we are in a method func_qualname = qualifier + "." + caller_frame.f_code.co_name
qualifier = caller_object_type.__name__ if disabled=='env' and func_qualname not in cls._profilers: # don't do anything
func_qualname = qualifier + "." + caller_frame.f_code.co_name return cls._disabledProfiler
if func_qualname not in cls._profilers: # don't do anything # create an actual profiling object
return cls._disabledProfiler cls._depth += 1
# create an actual profiling object obj = super(Profiler, cls).__new__(cls)
cls._depth += 1 obj._name = msg or func_qualname
obj = super(Profiler, cls).__new__(cls) obj._delayed = delayed
obj._name = msg or func_qualname obj._markCount = 0
obj._delayed = delayed obj._finished = False
obj._markCount = 0 obj._firstTime = obj._lastTime = ptime.time()
obj._finished = False obj._newMsg("> Entering " + obj._name)
obj._firstTime = obj._lastTime = ptime.time() return obj
obj._newMsg("> Entering " + obj._name) #else:
return obj #def __new__(cls, delayed=True):
else: #return lambda msg=None: None
def __new__(cls, delayed=True):
return lambda msg=None: None
def __call__(self, msg=None): def __call__(self, msg=None):
"""Register or print a new message with timing information. """Register or print a new message with timing information.
@ -467,6 +470,7 @@ class Profiler(object):
if self._delayed: if self._delayed:
self._msgs.append((msg, args)) self._msgs.append((msg, args))
else: else:
self.flush()
print(msg % args) print(msg % args)
def __del__(self): def __del__(self):
@ -483,10 +487,13 @@ class Profiler(object):
self._newMsg("< Exiting %s, total time: %0.4f ms", self._newMsg("< Exiting %s, total time: %0.4f ms",
self._name, (ptime.time() - self._firstTime) * 1000) self._name, (ptime.time() - self._firstTime) * 1000)
type(self)._depth -= 1 type(self)._depth -= 1
if self._depth < 1 and self._msgs: if self._depth < 1:
self.flush()
def flush(self):
if self._msgs:
print("\n".join([m[0]%m[1] for m in self._msgs])) print("\n".join([m[0]%m[1] for m in self._msgs]))
type(self)._msgs = [] type(self)._msgs = []
def profile(code, name='profile_run', sort='cumulative', num=30): def profile(code, name='profile_run', sort='cumulative', num=30):
@ -618,12 +625,12 @@ class ObjTracker(object):
## Which refs have disappeared since call to start() (these are only displayed once, then forgotten.) ## Which refs have disappeared since call to start() (these are only displayed once, then forgotten.)
delRefs = {} delRefs = {}
for i in self.startRefs.keys(): for i in list(self.startRefs.keys()):
if i not in refs: if i not in refs:
delRefs[i] = self.startRefs[i] delRefs[i] = self.startRefs[i]
del self.startRefs[i] del self.startRefs[i]
self.forgetRef(delRefs[i]) self.forgetRef(delRefs[i])
for i in self.newRefs.keys(): for i in list(self.newRefs.keys()):
if i not in refs: if i not in refs:
delRefs[i] = self.newRefs[i] delRefs[i] = self.newRefs[i]
del self.newRefs[i] del self.newRefs[i]
@ -661,7 +668,8 @@ class ObjTracker(object):
for k in self.startCount: for k in self.startCount:
c1[k] = c1.get(k, 0) - self.startCount[k] c1[k] = c1.get(k, 0) - self.startCount[k]
typs = list(c1.keys()) typs = list(c1.keys())
typs.sort(lambda a,b: cmp(c1[a], c1[b])) #typs.sort(lambda a,b: cmp(c1[a], c1[b]))
typs.sort(key=lambda a: c1[a])
for t in typs: for t in typs:
if c1[t] == 0: if c1[t] == 0:
continue continue
@ -761,7 +769,8 @@ class ObjTracker(object):
c = count.get(typ, [0,0]) c = count.get(typ, [0,0])
count[typ] = [c[0]+1, c[1]+objectSize(obj)] count[typ] = [c[0]+1, c[1]+objectSize(obj)]
typs = list(count.keys()) typs = list(count.keys())
typs.sort(lambda a,b: cmp(count[a][1], count[b][1])) #typs.sort(lambda a,b: cmp(count[a][1], count[b][1]))
typs.sort(key=lambda a: count[a][1])
for t in typs: for t in typs:
line = " %d\t%d\t%s" % (count[t][0], count[t][1], t) line = " %d\t%d\t%s" % (count[t][0], count[t][1], t)
@ -821,14 +830,15 @@ def describeObj(obj, depth=4, path=None, ignore=None):
def typeStr(obj): def typeStr(obj):
"""Create a more useful type string by making <instance> types report their class.""" """Create a more useful type string by making <instance> types report their class."""
typ = type(obj) typ = type(obj)
if typ == types.InstanceType: if typ == getattr(types, 'InstanceType', None):
return "<instance of %s>" % obj.__class__.__name__ return "<instance of %s>" % obj.__class__.__name__
else: else:
return str(typ) return str(typ)
def searchRefs(obj, *args): def searchRefs(obj, *args):
"""Pseudo-interactive function for tracing references backward. """Pseudo-interactive function for tracing references backward.
Arguments: **Arguments:**
obj: The initial object from which to start searching obj: The initial object from which to start searching
args: A set of string or int arguments. args: A set of string or int arguments.
each integer selects one of obj's referrers to be the new 'obj' each integer selects one of obj's referrers to be the new 'obj'
@ -840,7 +850,8 @@ def searchRefs(obj, *args):
ro: return obj ro: return obj
rr: return list of obj's referrers rr: return list of obj's referrers
Examples: Examples::
searchRefs(obj, 't') ## Print types of all objects referring to obj searchRefs(obj, 't') ## Print types of all objects referring to obj
searchRefs(obj, 't', 0, 't') ## ..then select the first referrer and print the types of its referrers searchRefs(obj, 't', 0, 't') ## ..then select the first referrer and print the types of its referrers
searchRefs(obj, 't', 0, 't', 'l') ## ..also print lengths of the last set of referrers searchRefs(obj, 't', 0, 't', 'l') ## ..also print lengths of the last set of referrers
@ -989,3 +1000,75 @@ class PrintDetector(object):
def flush(self): def flush(self):
self.stdout.flush() self.stdout.flush()
class PeriodicTrace(object):
"""
Used to debug freezing by starting a new thread that reports on the
location of the main thread periodically.
"""
class ReportThread(QtCore.QThread):
def __init__(self):
self.frame = None
self.ind = 0
self.lastInd = None
self.lock = Mutex()
QtCore.QThread.__init__(self)
def notify(self, frame):
with self.lock:
self.frame = frame
self.ind += 1
def run(self):
while True:
time.sleep(1)
with self.lock:
if self.lastInd != self.ind:
print("== Trace %d: ==" % self.ind)
traceback.print_stack(self.frame)
self.lastInd = self.ind
def __init__(self):
self.mainThread = threading.current_thread()
self.thread = PeriodicTrace.ReportThread()
self.thread.start()
sys.settrace(self.trace)
def trace(self, frame, event, arg):
if threading.current_thread() is self.mainThread: # and 'threading' not in frame.f_code.co_filename:
self.thread.notify(frame)
# print("== Trace ==", event, arg)
# traceback.print_stack(frame)
return self.trace
class ThreadColor(object):
"""
Wrapper on stdout/stderr that colors text by the current thread ID.
*stream* must be 'stdout' or 'stderr'.
"""
colors = {}
lock = Mutex()
def __init__(self, stream):
self.stream = getattr(sys, stream)
self.err = stream == 'stderr'
setattr(sys, stream, self)
def write(self, msg):
with self.lock:
cprint.cprint(self.stream, self.color(), msg, -1, stderr=self.err)
def flush(self):
with self.lock:
self.stream.flush()
def color(self):
tid = threading.current_thread()
if tid not in self.colors:
c = (len(self.colors) % 15) + 1
self.colors[tid] = c
return self.colors[tid]

View File

@ -2,6 +2,7 @@ from ..Qt import QtCore, QtGui
from .DockDrop import * from .DockDrop import *
from ..widgets.VerticalLabel import VerticalLabel from ..widgets.VerticalLabel import VerticalLabel
from ..python2_3 import asUnicode
class Dock(QtGui.QWidget, DockDrop): class Dock(QtGui.QWidget, DockDrop):
@ -167,7 +168,7 @@ class Dock(QtGui.QWidget, DockDrop):
self.resizeOverlay(self.size()) self.resizeOverlay(self.size())
def name(self): def name(self):
return str(self.label.text()) return asUnicode(self.label.text())
def container(self): def container(self):
return self._container return self._container

View File

@ -36,16 +36,16 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
def addDock(self, dock=None, position='bottom', relativeTo=None, **kwds): def addDock(self, dock=None, position='bottom', relativeTo=None, **kwds):
"""Adds a dock to this area. """Adds a dock to this area.
=========== ================================================================= ============== =================================================================
Arguments: **Arguments:**
dock The new Dock object to add. If None, then a new Dock will be dock The new Dock object to add. If None, then a new Dock will be
created. created.
position 'bottom', 'top', 'left', 'right', 'above', or 'below' position 'bottom', 'top', 'left', 'right', 'above', or 'below'
relativeTo If relativeTo is None, then the new Dock is added to fill an relativeTo If relativeTo is None, then the new Dock is added to fill an
entire edge of the window. If relativeTo is another Dock, then entire edge of the window. If relativeTo is another Dock, then
the new Dock is placed adjacent to it (or in a tabbed the new Dock is placed adjacent to it (or in a tabbed
configuration for 'above' and 'below'). configuration for 'above' and 'below').
=========== ================================================================= ============== =================================================================
All extra keyword arguments are passed to Dock.__init__() if *dock* is All extra keyword arguments are passed to Dock.__init__() if *dock* is
None. None.

View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
#import sip
#sip.setapi('QString', 1)
import pyqtgraph as pg
pg.mkQApp()
import pyqtgraph.dockarea as da
def test_dock():
name = pg.asUnicode("évènts_zàhéér")
dock = da.Dock(name=name)
# make sure unicode names work correctly
assert dock.name() == name
# no surprises in return type.
assert type(dock.name()) == type(name)

View File

@ -36,7 +36,7 @@ class PrintExporter(Exporter):
dialog = QtGui.QPrintDialog(printer) dialog = QtGui.QPrintDialog(printer)
dialog.setWindowTitle("Print Document") dialog.setWindowTitle("Print Document")
if dialog.exec_() != QtGui.QDialog.Accepted: if dialog.exec_() != QtGui.QDialog.Accepted:
return; return
#dpi = QtGui.QDesktopWidget().physicalDpiX() #dpi = QtGui.QDesktopWidget().physicalDpiX()

View File

@ -1,7 +1,7 @@
from .Exporter import Exporter from .Exporter import Exporter
from ..python2_3 import asUnicode from ..python2_3 import asUnicode
from ..parametertree import Parameter from ..parametertree import Parameter
from ..Qt import QtGui, QtCore, QtSvg from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE
from .. import debug from .. import debug
from .. import functions as fn from .. import functions as fn
import re import re
@ -219,7 +219,10 @@ def _generateItemSvg(item, nodes=None, root=None):
#if hasattr(item, 'setExportMode'): #if hasattr(item, 'setExportMode'):
#item.setExportMode(False) #item.setExportMode(False)
xmlStr = bytes(arr).decode('utf-8') if USE_PYSIDE:
xmlStr = str(arr)
else:
xmlStr = bytes(arr).decode('utf-8')
doc = xml.parseString(xmlStr) doc = xml.parseString(xmlStr)
try: try:

View File

@ -0,0 +1,67 @@
"""
SVG export test
"""
import pyqtgraph as pg
import pyqtgraph.exporters
app = pg.mkQApp()
def test_plotscene():
pg.setConfigOption('foreground', (0,0,0))
w = pg.GraphicsWindow()
w.show()
p1 = w.addPlot()
p2 = w.addPlot()
p1.plot([1,3,2,3,1,6,9,8,4,2,3,5,3], pen={'color':'k'})
p1.setXRange(0,5)
p2.plot([1,5,2,3,4,6,1,2,4,2,3,5,3], pen={'color':'k', 'cosmetic':False, 'width': 0.3})
app.processEvents()
app.processEvents()
ex = pg.exporters.SVGExporter(w.scene())
ex.export(fileName='test.svg')
def test_simple():
scene = pg.QtGui.QGraphicsScene()
#rect = pg.QtGui.QGraphicsRectItem(0, 0, 100, 100)
#scene.addItem(rect)
#rect.setPos(20,20)
#rect.translate(50, 50)
#rect.rotate(30)
#rect.scale(0.5, 0.5)
#rect1 = pg.QtGui.QGraphicsRectItem(0, 0, 100, 100)
#rect1.setParentItem(rect)
#rect1.setFlag(rect1.ItemIgnoresTransformations)
#rect1.setPos(20, 20)
#rect1.scale(2,2)
#el1 = pg.QtGui.QGraphicsEllipseItem(0, 0, 100, 100)
#el1.setParentItem(rect1)
##grp = pg.ItemGroup()
#grp.setParentItem(rect)
#grp.translate(200,0)
##grp.rotate(30)
#rect2 = pg.QtGui.QGraphicsRectItem(0, 0, 100, 25)
#rect2.setFlag(rect2.ItemClipsChildrenToShape)
#rect2.setParentItem(grp)
#rect2.setPos(0,25)
#rect2.rotate(30)
#el = pg.QtGui.QGraphicsEllipseItem(0, 0, 100, 50)
#el.translate(10,-5)
#el.scale(0.5,2)
#el.setParentItem(rect2)
grp2 = pg.ItemGroup()
scene.addItem(grp2)
grp2.scale(100,100)
rect3 = pg.QtGui.QGraphicsRectItem(0,0,2,2)
rect3.setPen(pg.mkPen(width=1, cosmetic=False))
grp2.addItem(rect3)
ex = pg.exporters.SVGExporter(scene)
ex.export(fileName='test.svg')

View File

@ -227,18 +227,11 @@ class Flowchart(Node):
def nodeClosed(self, node): def nodeClosed(self, node):
del self._nodes[node.name()] del self._nodes[node.name()]
self.widget().removeNode(node) self.widget().removeNode(node)
try: for signal in ['sigClosed', 'sigRenamed', 'sigOutputChanged']:
node.sigClosed.disconnect(self.nodeClosed) try:
except TypeError: getattr(node, signal).disconnect(self.nodeClosed)
pass except (TypeError, RuntimeError):
try: pass
node.sigRenamed.disconnect(self.nodeRenamed)
except TypeError:
pass
try:
node.sigOutputChanged.disconnect(self.nodeOutputChanged)
except TypeError:
pass
self.sigChartChanged.emit(self, 'remove', node) self.sigChartChanged.emit(self, 'remove', node)
def nodeRenamed(self, node, oldName): def nodeRenamed(self, node, oldName):
@ -769,7 +762,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
#self.disconnect(item.bypassBtn, QtCore.SIGNAL('clicked()'), self.bypassClicked) #self.disconnect(item.bypassBtn, QtCore.SIGNAL('clicked()'), self.bypassClicked)
try: try:
item.bypassBtn.clicked.disconnect(self.bypassClicked) item.bypassBtn.clicked.disconnect(self.bypassClicked)
except TypeError: except (TypeError, RuntimeError):
pass pass
self.ui.ctrlList.removeTopLevelItem(item) self.ui.ctrlList.removeTopLevelItem(item)

View File

@ -37,7 +37,7 @@ class Node(QtCore.QObject):
def __init__(self, name, terminals=None, allowAddInput=False, allowAddOutput=False, allowRemove=True): def __init__(self, name, terminals=None, allowAddInput=False, allowAddOutput=False, allowRemove=True):
""" """
============== ============================================================ ============== ============================================================
Arguments **Arguments:**
name The name of this specific node instance. It can be any name The name of this specific node instance. It can be any
string, but must be unique within a flowchart. Usually, string, but must be unique within a flowchart. Usually,
we simply let the flowchart decide on a name when calling we simply let the flowchart decide on a name when calling
@ -501,8 +501,8 @@ class NodeGraphicsItem(GraphicsObject):
bounds = self.boundingRect() bounds = self.boundingRect()
self.nameItem.setPos(bounds.width()/2. - self.nameItem.boundingRect().width()/2., 0) self.nameItem.setPos(bounds.width()/2. - self.nameItem.boundingRect().width()/2., 0)
def setPen(self, pen): def setPen(self, *args, **kwargs):
self.pen = pen self.pen = fn.mkPen(*args, **kwargs)
self.update() self.update()
def setBrush(self, brush): def setBrush(self, brush):

View File

@ -26,12 +26,14 @@ class NodeLibrary:
Register a new node type. If the type's name is already in use, Register a new node type. If the type's name is already in use,
an exception will be raised (unless override=True). an exception will be raised (unless override=True).
Arguments: ============== =========================================================
**Arguments:**
nodeClass - a subclass of Node (must have typ.nodeName) nodeClass a subclass of Node (must have typ.nodeName)
paths - list of tuples specifying the location(s) this paths list of tuples specifying the location(s) this
type will appear in the library tree. type will appear in the library tree.
override - if True, overwrite any class having the same name override if True, overwrite any class having the same name
============== =========================================================
""" """
if not isNodeClass(nodeClass): if not isNodeClass(nodeClass):
raise Exception("Object %s is not a Node subclass" % str(nodeClass)) raise Exception("Object %s is not a Node subclass" % str(nodeClass))

View File

@ -29,7 +29,7 @@ def eq(a, b):
except: except:
return False return False
if (hasattr(e, 'implements') and e.implements('MetaArray')): if (hasattr(e, 'implements') and e.implements('MetaArray')):
return e.asarray().all() return e.asarray().all()
else: else:
return e.all() return e.all()
else: else:

View File

@ -328,7 +328,7 @@ class ColumnJoinNode(Node):
## Node.restoreState should have created all of the terminals we need ## Node.restoreState should have created all of the terminals we need
## However: to maintain support for some older flowchart files, we need ## However: to maintain support for some older flowchart files, we need
## to manually add any terminals that were not taken care of. ## to manually add any terminals that were not taken care of.
for name in [n for n in state['order'] if n not in inputs]: for name in [n for n in state['order'] if n not in inputs]:
Node.addInput(self, name, renamable=True, removable=True, multiable=True) Node.addInput(self, name, renamable=True, removable=True, multiable=True)
inputs = self.inputs() inputs = self.inputs()

View File

@ -1,10 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from ...Qt import QtCore, QtGui from ...Qt import QtCore, QtGui
from ..Node import Node from ..Node import Node
from scipy.signal import detrend
from scipy.ndimage import median_filter, gaussian_filter
#from ...SignalProxy import SignalProxy
from . import functions from . import functions
from ... import functions as pgfn
from .common import * from .common import *
import numpy as np import numpy as np
@ -119,7 +117,11 @@ class Median(CtrlNode):
@metaArrayWrapper @metaArrayWrapper
def processData(self, data): def processData(self, data):
return median_filter(data, self.ctrls['n'].value()) try:
import scipy.ndimage
except ImportError:
raise Exception("MedianFilter node requires the package scipy.ndimage.")
return scipy.ndimage.median_filter(data, self.ctrls['n'].value())
class Mode(CtrlNode): class Mode(CtrlNode):
"""Filters data by taking the mode (histogram-based) of a sliding window""" """Filters data by taking the mode (histogram-based) of a sliding window"""
@ -156,7 +158,11 @@ class Gaussian(CtrlNode):
@metaArrayWrapper @metaArrayWrapper
def processData(self, data): def processData(self, data):
return gaussian_filter(data, self.ctrls['sigma'].value()) try:
import scipy.ndimage
except ImportError:
raise Exception("GaussianFilter node requires the package scipy.ndimage.")
return pgfn.gaussianFilter(data, self.ctrls['sigma'].value())
class Derivative(CtrlNode): class Derivative(CtrlNode):
@ -189,6 +195,10 @@ class Detrend(CtrlNode):
@metaArrayWrapper @metaArrayWrapper
def processData(self, data): def processData(self, data):
try:
from scipy.signal import detrend
except ImportError:
raise Exception("DetrendFilter node requires the package scipy.signal.")
return detrend(data) return detrend(data)

View File

@ -1,4 +1,3 @@
import scipy
import numpy as np import numpy as np
from ...metaarray import MetaArray from ...metaarray import MetaArray
@ -47,6 +46,11 @@ def downsample(data, n, axis=0, xvals='subsample'):
def applyFilter(data, b, a, padding=100, bidir=True): def applyFilter(data, b, a, padding=100, bidir=True):
"""Apply a linear filter with coefficients a, b. Optionally pad the data before filtering """Apply a linear filter with coefficients a, b. Optionally pad the data before filtering
and/or run the filter in both directions.""" and/or run the filter in both directions."""
try:
import scipy.signal
except ImportError:
raise Exception("applyFilter() requires the package scipy.signal.")
d1 = data.view(np.ndarray) d1 = data.view(np.ndarray)
if padding > 0: if padding > 0:
@ -67,6 +71,11 @@ def applyFilter(data, b, a, padding=100, bidir=True):
def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True): def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True):
"""return data passed through bessel filter""" """return data passed through bessel filter"""
try:
import scipy.signal
except ImportError:
raise Exception("besselFilter() requires the package scipy.signal.")
if dt is None: if dt is None:
try: try:
tvals = data.xvals('Time') tvals = data.xvals('Time')
@ -85,6 +94,11 @@ def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True):
def butterworthFilter(data, wPass, wStop=None, gPass=2.0, gStop=20.0, order=1, dt=None, btype='low', bidir=True): def butterworthFilter(data, wPass, wStop=None, gPass=2.0, gStop=20.0, order=1, dt=None, btype='low', bidir=True):
"""return data passed through bessel filter""" """return data passed through bessel filter"""
try:
import scipy.signal
except ImportError:
raise Exception("butterworthFilter() requires the package scipy.signal.")
if dt is None: if dt is None:
try: try:
tvals = data.xvals('Time') tvals = data.xvals('Time')
@ -175,6 +189,11 @@ def denoise(data, radius=2, threshold=4):
def adaptiveDetrend(data, x=None, threshold=3.0): def adaptiveDetrend(data, x=None, threshold=3.0):
"""Return the signal with baseline removed. Discards outliers from baseline measurement.""" """Return the signal with baseline removed. Discards outliers from baseline measurement."""
try:
import scipy.signal
except ImportError:
raise Exception("adaptiveDetrend() requires the package scipy.signal.")
if x is None: if x is None:
x = data.xvals(0) x = data.xvals(0)

View File

@ -1,52 +1,52 @@
## Definitions helpful in frozen environments (eg py2exe) ## Definitions helpful in frozen environments (eg py2exe)
import os, sys, zipfile import os, sys, zipfile
def listdir(path): def listdir(path):
"""Replacement for os.listdir that works in frozen environments.""" """Replacement for os.listdir that works in frozen environments."""
if not hasattr(sys, 'frozen'): if not hasattr(sys, 'frozen'):
return os.listdir(path) return os.listdir(path)
(zipPath, archivePath) = splitZip(path) (zipPath, archivePath) = splitZip(path)
if archivePath is None: if archivePath is None:
return os.listdir(path) return os.listdir(path)
with zipfile.ZipFile(zipPath, "r") as zipobj: with zipfile.ZipFile(zipPath, "r") as zipobj:
contents = zipobj.namelist() contents = zipobj.namelist()
results = set() results = set()
for name in contents: for name in contents:
# components in zip archive paths are always separated by forward slash # components in zip archive paths are always separated by forward slash
if name.startswith(archivePath) and len(name) > len(archivePath): if name.startswith(archivePath) and len(name) > len(archivePath):
name = name[len(archivePath):].split('/')[0] name = name[len(archivePath):].split('/')[0]
results.add(name) results.add(name)
return list(results) return list(results)
def isdir(path): def isdir(path):
"""Replacement for os.path.isdir that works in frozen environments.""" """Replacement for os.path.isdir that works in frozen environments."""
if not hasattr(sys, 'frozen'): if not hasattr(sys, 'frozen'):
return os.path.isdir(path) return os.path.isdir(path)
(zipPath, archivePath) = splitZip(path) (zipPath, archivePath) = splitZip(path)
if archivePath is None: if archivePath is None:
return os.path.isdir(path) return os.path.isdir(path)
with zipfile.ZipFile(zipPath, "r") as zipobj: with zipfile.ZipFile(zipPath, "r") as zipobj:
contents = zipobj.namelist() contents = zipobj.namelist()
archivePath = archivePath.rstrip('/') + '/' ## make sure there's exactly one '/' at the end archivePath = archivePath.rstrip('/') + '/' ## make sure there's exactly one '/' at the end
for c in contents: for c in contents:
if c.startswith(archivePath): if c.startswith(archivePath):
return True return True
return False return False
def splitZip(path): def splitZip(path):
"""Splits a path containing a zip file into (zipfile, subpath). """Splits a path containing a zip file into (zipfile, subpath).
If there is no zip file, returns (path, None)""" If there is no zip file, returns (path, None)"""
components = os.path.normpath(path).split(os.sep) components = os.path.normpath(path).split(os.sep)
for index, component in enumerate(components): for index, component in enumerate(components):
if component.endswith('.zip'): if component.endswith('.zip'):
zipPath = os.sep.join(components[0:index+1]) zipPath = os.sep.join(components[0:index+1])
archivePath = ''.join([x+'/' for x in components[index+1:]]) archivePath = ''.join([x+'/' for x in components[index+1:]])
return (zipPath, archivePath) return (zipPath, archivePath)
else: else:
return (path, None) return (path, None)

View File

@ -34,17 +34,6 @@ import decimal, re
import ctypes import ctypes
import sys, struct import sys, struct
try:
import scipy.ndimage
HAVE_SCIPY = True
if getConfigOption('useWeave'):
try:
import scipy.weave
except ImportError:
setConfigOptions(useWeave=False)
except ImportError:
HAVE_SCIPY = False
from . import debug from . import debug
def siScale(x, minVal=1e-25, allowUnicode=True): def siScale(x, minVal=1e-25, allowUnicode=True):
@ -383,12 +372,12 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
""" """
Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data. Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data.
The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates (see the scipy documentation for more information about this). The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates if it is available (see the scipy documentation for more information about this). If scipy is not available, then a slower implementation of map_coordinates is used.
For a graphical interface to this function, see :func:`ROI.getArrayRegion <pyqtgraph.ROI.getArrayRegion>` For a graphical interface to this function, see :func:`ROI.getArrayRegion <pyqtgraph.ROI.getArrayRegion>`
============== ==================================================================================================== ============== ====================================================================================================
Arguments: **Arguments:**
*data* (ndarray) the original dataset *data* (ndarray) the original dataset
*shape* the shape of the slice to take (Note the return value may have more dimensions than len(shape)) *shape* the shape of the slice to take (Note the return value may have more dimensions than len(shape))
*origin* the location in the original dataset that will become the origin of the sliced data. *origin* the location in the original dataset that will become the origin of the sliced data.
@ -422,8 +411,12 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3)) affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3))
""" """
if not HAVE_SCIPY: try:
raise Exception("This function requires the scipy library, but it does not appear to be importable.") import scipy.ndimage
have_scipy = True
except ImportError:
have_scipy = False
have_scipy = False
# sanity check # sanity check
if len(shape) != len(vectors): if len(shape) != len(vectors):
@ -445,7 +438,6 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
#print "tr1:", tr1 #print "tr1:", tr1
## dims are now [(slice axes), (other axes)] ## dims are now [(slice axes), (other axes)]
## make sure vectors are arrays ## make sure vectors are arrays
if not isinstance(vectors, np.ndarray): if not isinstance(vectors, np.ndarray):
vectors = np.array(vectors) vectors = np.array(vectors)
@ -461,12 +453,18 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
#print "X values:" #print "X values:"
#print x #print x
## iterate manually over unused axes since map_coordinates won't do it for us ## iterate manually over unused axes since map_coordinates won't do it for us
extraShape = data.shape[len(axes):] if have_scipy:
output = np.empty(tuple(shape) + extraShape, dtype=data.dtype) extraShape = data.shape[len(axes):]
for inds in np.ndindex(*extraShape): output = np.empty(tuple(shape) + extraShape, dtype=data.dtype)
ind = (Ellipsis,) + inds for inds in np.ndindex(*extraShape):
#print data[ind].shape, x.shape, output[ind].shape, output.shape ind = (Ellipsis,) + inds
output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs) output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs)
else:
# map_coordinates expects the indexes as the first axis, whereas
# interpolateArray expects indexes at the last axis.
tr = tuple(range(1,x.ndim)) + (0,)
output = interpolateArray(data, x.transpose(tr))
tr = list(range(output.ndim)) tr = list(range(output.ndim))
trb = [] trb = []
@ -483,6 +481,117 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
else: else:
return output return output
def interpolateArray(data, x, default=0.0):
"""
N-dimensional interpolation similar scipy.ndimage.map_coordinates.
This function returns linearly-interpolated values sampled from a regular
grid of data.
*data* is an array of any shape containing the values to be interpolated.
*x* is an array with (shape[-1] <= data.ndim) containing the locations
within *data* to interpolate.
Returns array of shape (x.shape[:-1] + data.shape)
For example, assume we have the following 2D image data::
>>> data = np.array([[1, 2, 4 ],
[10, 20, 40 ],
[100, 200, 400]])
To compute a single interpolated point from this data::
>>> x = np.array([(0.5, 0.5)])
>>> interpolateArray(data, x)
array([ 8.25])
To compute a 1D list of interpolated locations::
>>> x = np.array([(0.5, 0.5),
(1.0, 1.0),
(1.0, 2.0),
(1.5, 0.0)])
>>> interpolateArray(data, x)
array([ 8.25, 20. , 40. , 55. ])
To compute a 2D array of interpolated locations::
>>> x = np.array([[(0.5, 0.5), (1.0, 2.0)],
[(1.0, 1.0), (1.5, 0.0)]])
>>> interpolateArray(data, x)
array([[ 8.25, 40. ],
[ 20. , 55. ]])
..and so on. The *x* argument may have any shape as long as
```x.shape[-1] <= data.ndim```. In the case that
```x.shape[-1] < data.ndim```, then the remaining axes are simply
broadcasted as usual. For example, we can interpolate one location
from an entire row of the data::
>>> x = np.array([[0.5]])
>>> interpolateArray(data, x)
array([[ 5.5, 11. , 22. ]])
This is useful for interpolating from arrays of colors, vertexes, etc.
"""
prof = debug.Profiler()
result = np.empty(x.shape[:-1] + data.shape, dtype=data.dtype)
nd = data.ndim
md = x.shape[-1]
# First we generate arrays of indexes that are needed to
# extract the data surrounding each point
fields = np.mgrid[(slice(0,2),) * md]
xmin = np.floor(x).astype(int)
xmax = xmin + 1
indexes = np.concatenate([xmin[np.newaxis, ...], xmax[np.newaxis, ...]])
fieldInds = []
totalMask = np.ones(x.shape[:-1], dtype=bool) # keep track of out-of-bound indexes
for ax in range(md):
mask = (xmin[...,ax] >= 0) & (x[...,ax] <= data.shape[ax]-1)
# keep track of points that need to be set to default
totalMask &= mask
# ..and keep track of indexes that are out of bounds
# (note that when x[...,ax] == data.shape[ax], then xmax[...,ax] will be out
# of bounds, but the interpolation will work anyway)
mask &= (xmax[...,ax] < data.shape[ax])
axisIndex = indexes[...,ax][fields[ax]]
#axisMask = mask.astype(np.ubyte).reshape((1,)*(fields.ndim-1) + mask.shape)
axisIndex[axisIndex < 0] = 0
axisIndex[axisIndex >= data.shape[ax]] = 0
fieldInds.append(axisIndex)
prof()
# Get data values surrounding each requested point
# fieldData[..., i] contains all 2**nd values needed to interpolate x[i]
fieldData = data[tuple(fieldInds)]
prof()
## Interpolate
s = np.empty((md,) + fieldData.shape, dtype=float)
dx = x - xmin
# reshape fields for arithmetic against dx
for ax in range(md):
f1 = fields[ax].reshape(fields[ax].shape + (1,)*(dx.ndim-1))
sax = f1 * dx[...,ax] + (1-f1) * (1-dx[...,ax])
sax = sax.reshape(sax.shape + (1,) * (s.ndim-1-sax.ndim))
s[ax] = sax
s = np.product(s, axis=0)
result = fieldData * s
for i in range(md):
result = result.sum(axis=0)
prof()
totalMask.shape = totalMask.shape + (1,) * (nd - md)
result[~totalMask] = default
prof()
return result
def transformToArray(tr): def transformToArray(tr):
""" """
Given a QTransform, return a 3x3 numpy array. Given a QTransform, return a 3x3 numpy array.
@ -577,17 +686,25 @@ def transformCoordinates(tr, coords, transpose=False):
def solve3DTransform(points1, points2): def solve3DTransform(points1, points2):
""" """
Find a 3D transformation matrix that maps points1 onto points2. Find a 3D transformation matrix that maps points1 onto points2.
Points must be specified as a list of 4 Vectors. Points must be specified as either lists of 4 Vectors or
(4, 3) arrays.
""" """
if not HAVE_SCIPY: import numpy.linalg
raise Exception("This function depends on the scipy library, but it does not appear to be importable.") pts = []
A = np.array([[points1[i].x(), points1[i].y(), points1[i].z(), 1] for i in range(4)]) for inp in (points1, points2):
B = np.array([[points2[i].x(), points2[i].y(), points2[i].z(), 1] for i in range(4)]) if isinstance(inp, np.ndarray):
A = np.empty((4,4), dtype=float)
A[:,:3] = inp[:,:3]
A[:,3] = 1.0
else:
A = np.array([[inp[i].x(), inp[i].y(), inp[i].z(), 1] for i in range(4)])
pts.append(A)
## solve 3 sets of linear equations to determine transformation matrix elements ## solve 3 sets of linear equations to determine transformation matrix elements
matrix = np.zeros((4,4)) matrix = np.zeros((4,4))
for i in range(3): for i in range(3):
matrix[i] = scipy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix ## solve Ax = B; x is one row of the desired transformation matrix
matrix[i] = numpy.linalg.solve(pts[0], pts[1][:,i])
return matrix return matrix
@ -600,8 +717,7 @@ def solveBilinearTransform(points1, points2):
mapped = np.dot(matrix, [x*y, x, y, 1]) mapped = np.dot(matrix, [x*y, x, y, 1])
""" """
if not HAVE_SCIPY: import numpy.linalg
raise Exception("This function depends on the scipy library, but it does not appear to be importable.")
## A is 4 rows (points) x 4 columns (xy, x, y, 1) ## A is 4 rows (points) x 4 columns (xy, x, y, 1)
## B is 4 rows (points) x 2 columns (x, y) ## B is 4 rows (points) x 2 columns (x, y)
A = np.array([[points1[i].x()*points1[i].y(), points1[i].x(), points1[i].y(), 1] for i in range(4)]) A = np.array([[points1[i].x()*points1[i].y(), points1[i].x(), points1[i].y(), 1] for i in range(4)])
@ -610,7 +726,7 @@ def solveBilinearTransform(points1, points2):
## solve 2 sets of linear equations to determine transformation matrix elements ## solve 2 sets of linear equations to determine transformation matrix elements
matrix = np.zeros((2,4)) matrix = np.zeros((2,4))
for i in range(2): for i in range(2):
matrix[i] = scipy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix matrix[i] = numpy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix
return matrix return matrix
@ -629,6 +745,10 @@ def rescaleData(data, scale, offset, dtype=None):
try: try:
if not getConfigOption('useWeave'): if not getConfigOption('useWeave'):
raise Exception('Weave is disabled; falling back to slower version.') raise Exception('Weave is disabled; falling back to slower version.')
try:
import scipy.weave
except ImportError:
raise Exception('scipy.weave is not importable; falling back to slower version.')
## require native dtype when using weave ## require native dtype when using weave
if not data.dtype.isnative: if not data.dtype.isnative:
@ -671,68 +791,13 @@ def applyLookupTable(data, lut):
Uses values in *data* as indexes to select values from *lut*. Uses values in *data* as indexes to select values from *lut*.
The returned data has shape data.shape + lut.shape[1:] The returned data has shape data.shape + lut.shape[1:]
Uses scipy.weave to improve performance if it is available.
Note: color gradient lookup tables can be generated using GradientWidget. Note: color gradient lookup tables can be generated using GradientWidget.
""" """
if data.dtype.kind not in ('i', 'u'): if data.dtype.kind not in ('i', 'u'):
data = data.astype(int) data = data.astype(int)
## using np.take appears to be faster than even the scipy.weave method and takes care of clipping as well.
return np.take(lut, data, axis=0, mode='clip') return np.take(lut, data, axis=0, mode='clip')
### old methods:
#data = np.clip(data, 0, lut.shape[0]-1)
#try:
#if not USE_WEAVE:
#raise Exception('Weave is disabled; falling back to slower version.')
### number of values to copy for each LUT lookup
#if lut.ndim == 1:
#ncol = 1
#else:
#ncol = sum(lut.shape[1:])
### output array
#newData = np.empty((data.size, ncol), dtype=lut.dtype)
### flattened input arrays
#flatData = data.flatten()
#flatLut = lut.reshape((lut.shape[0], ncol))
#dataSize = data.size
### strides for accessing each item
#newStride = newData.strides[0] / newData.dtype.itemsize
#lutStride = flatLut.strides[0] / flatLut.dtype.itemsize
#dataStride = flatData.strides[0] / flatData.dtype.itemsize
### strides for accessing individual values within a single LUT lookup
#newColStride = newData.strides[1] / newData.dtype.itemsize
#lutColStride = flatLut.strides[1] / flatLut.dtype.itemsize
#code = """
#for( int i=0; i<dataSize; i++ ) {
#for( int j=0; j<ncol; j++ ) {
#newData[i*newStride + j*newColStride] = flatLut[flatData[i*dataStride]*lutStride + j*lutColStride];
#}
#}
#"""
#scipy.weave.inline(code, ['flatData', 'flatLut', 'newData', 'dataSize', 'ncol', 'newStride', 'lutStride', 'dataStride', 'newColStride', 'lutColStride'])
#newData = newData.reshape(data.shape + lut.shape[1:])
##if np.any(newData != lut[data]):
##print "mismatch!"
#data = newData
#except:
#if USE_WEAVE:
#debug.printExc("Error; disabling weave.")
#USE_WEAVE = False
#data = lut[data]
#return data
def makeRGBA(*args, **kwds): def makeRGBA(*args, **kwds):
"""Equivalent to makeARGB(..., useRGBA=True)""" """Equivalent to makeARGB(..., useRGBA=True)"""
@ -751,36 +816,36 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
Both stages are optional. Both stages are optional.
============ ================================================================================== ============== ==================================================================================
Arguments: **Arguments:**
data numpy array of int/float types. If data numpy array of int/float types. If
levels List [min, max]; optionally rescale data before converting through the levels List [min, max]; optionally rescale data before converting through the
lookup table. The data is rescaled such that min->0 and max->*scale*:: lookup table. The data is rescaled such that min->0 and max->*scale*::
rescaled = (clip(data, min, max) - min) * (*scale* / (max - min)) rescaled = (clip(data, min, max) - min) * (*scale* / (max - min))
It is also possible to use a 2D (N,2) array of values for levels. In this case, It is also possible to use a 2D (N,2) array of values for levels. In this case,
it is assumed that each pair of min,max values in the levels array should be it is assumed that each pair of min,max values in the levels array should be
applied to a different subset of the input data (for example, the input data may applied to a different subset of the input data (for example, the input data may
already have RGB values and the levels are used to independently scale each already have RGB values and the levels are used to independently scale each
channel). The use of this feature requires that levels.shape[0] == data.shape[-1]. channel). The use of this feature requires that levels.shape[0] == data.shape[-1].
scale The maximum value to which data will be rescaled before being passed through the scale The maximum value to which data will be rescaled before being passed through the
lookup table (or returned if there is no lookup table). By default this will lookup table (or returned if there is no lookup table). By default this will
be set to the length of the lookup table, or 256 is no lookup table is provided. be set to the length of the lookup table, or 256 is no lookup table is provided.
For OpenGL color specifications (as in GLColor4f) use scale=1.0 For OpenGL color specifications (as in GLColor4f) use scale=1.0
lut Optional lookup table (array with dtype=ubyte). lut Optional lookup table (array with dtype=ubyte).
Values in data will be converted to color by indexing directly from lut. Values in data will be converted to color by indexing directly from lut.
The output data shape will be input.shape + lut.shape[1:]. The output data shape will be input.shape + lut.shape[1:].
Note: the output of makeARGB will have the same dtype as the lookup table, so Note: the output of makeARGB will have the same dtype as the lookup table, so
for conversion to QImage, the dtype must be ubyte. for conversion to QImage, the dtype must be ubyte.
Lookup tables can be built using GradientWidget. Lookup tables can be built using GradientWidget.
useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures). useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures).
The default is False, which returns in ARGB order for use with QImage The default is False, which returns in ARGB order for use with QImage
(Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order (Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order
is BGRA). is BGRA).
============ ================================================================================== ============== ==================================================================================
""" """
profile = debug.Profiler() profile = debug.Profiler()
@ -887,23 +952,23 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
pointing to the array which shares its data to prevent python pointing to the array which shares its data to prevent python
freeing that memory while the image is in use. freeing that memory while the image is in use.
=========== =================================================================== ============== ===================================================================
Arguments: **Arguments:**
imgData Array of data to convert. Must have shape (width, height, 3 or 4) imgData Array of data to convert. Must have shape (width, height, 3 or 4)
and dtype=ubyte. The order of values in the 3rd axis must be and dtype=ubyte. The order of values in the 3rd axis must be
(b, g, r, a). (b, g, r, a).
alpha If True, the QImage returned will have format ARGB32. If False, alpha If True, the QImage returned will have format ARGB32. If False,
the format will be RGB32. By default, _alpha_ is True if the format will be RGB32. By default, _alpha_ is True if
array.shape[2] == 4. array.shape[2] == 4.
copy If True, the data is copied before converting to QImage. copy If True, the data is copied before converting to QImage.
If False, the new QImage points directly to the data in the array. If False, the new QImage points directly to the data in the array.
Note that the array must be contiguous for this to work Note that the array must be contiguous for this to work
(see numpy.ascontiguousarray). (see numpy.ascontiguousarray).
transpose If True (the default), the array x/y axes are transposed before transpose If True (the default), the array x/y axes are transposed before
creating the image. Note that Qt expects the axes to be in creating the image. Note that Qt expects the axes to be in
(height, width) order whereas pyqtgraph usually prefers the (height, width) order whereas pyqtgraph usually prefers the
opposite. opposite.
=========== =================================================================== ============== ===================================================================
""" """
## create QImage from buffer ## create QImage from buffer
profile = debug.Profiler() profile = debug.Profiler()
@ -993,6 +1058,10 @@ def imageToArray(img, copy=False, transpose=True):
else: else:
ptr.setsize(img.byteCount()) ptr.setsize(img.byteCount())
arr = np.asarray(ptr) arr = np.asarray(ptr)
if img.byteCount() != arr.size * arr.itemsize:
# Required for Python 2.6, PyQt 4.10
# If this works on all platforms, then there is no need to use np.asarray..
arr = np.frombuffer(ptr, np.ubyte, img.byteCount())
if fmt == img.Format_RGB32: if fmt == img.Format_RGB32:
arr = arr.reshape(img.height(), img.width(), 3) arr = arr.reshape(img.height(), img.width(), 3)
@ -1051,7 +1120,86 @@ def colorToAlpha(data, color):
#raise Exception() #raise Exception()
return np.clip(output, 0, 255).astype(np.ubyte) return np.clip(output, 0, 255).astype(np.ubyte)
def gaussianFilter(data, sigma):
"""
Drop-in replacement for scipy.ndimage.gaussian_filter.
(note: results are only approximately equal to the output of
gaussian_filter)
"""
if np.isscalar(sigma):
sigma = (sigma,) * data.ndim
baseline = data.mean()
filtered = data - baseline
for ax in range(data.ndim):
s = sigma[ax]
if s == 0:
continue
# generate 1D gaussian kernel
ksize = int(s * 6)
x = np.arange(-ksize, ksize)
kernel = np.exp(-x**2 / (2*s**2))
kshape = [1,] * data.ndim
kshape[ax] = len(kernel)
kernel = kernel.reshape(kshape)
# convolve as product of FFTs
shape = data.shape[ax] + ksize
scale = 1.0 / (abs(s) * (2*np.pi)**0.5)
filtered = scale * np.fft.irfft(np.fft.rfft(filtered, shape, axis=ax) *
np.fft.rfft(kernel, shape, axis=ax),
axis=ax)
# clip off extra data
sl = [slice(None)] * data.ndim
sl[ax] = slice(filtered.shape[ax]-data.shape[ax],None,None)
filtered = filtered[sl]
return filtered + baseline
def downsample(data, n, axis=0, xvals='subsample'):
"""Downsample by averaging points together across axis.
If multiple axes are specified, runs once per axis.
If a metaArray is given, then the axis values can be either subsampled
or downsampled to match.
"""
ma = None
if (hasattr(data, 'implements') and data.implements('MetaArray')):
ma = data
data = data.view(np.ndarray)
if hasattr(axis, '__len__'):
if not hasattr(n, '__len__'):
n = [n]*len(axis)
for i in range(len(axis)):
data = downsample(data, n[i], axis[i])
return data
nPts = int(data.shape[axis] / n)
s = list(data.shape)
s[axis] = nPts
s.insert(axis+1, n)
sl = [slice(None)] * data.ndim
sl[axis] = slice(0, nPts*n)
d1 = data[tuple(sl)]
#print d1.shape, s
d1.shape = tuple(s)
d2 = d1.mean(axis+1)
if ma is None:
return d2
else:
info = ma.infoCopy()
if 'values' in info[axis]:
if xvals == 'subsample':
info[axis]['values'] = info[axis]['values'][::n][:nPts]
elif xvals == 'downsample':
info[axis]['values'] = downsample(info[axis]['values'], n)
return MetaArray(d2, info=info)
def arrayToQPath(x, y, connect='all'): def arrayToQPath(x, y, connect='all'):
@ -1113,6 +1261,8 @@ def arrayToQPath(x, y, connect='all'):
# decide which points are connected by lines # decide which points are connected by lines
if connect == 'pairs': if connect == 'pairs':
connect = np.empty((n/2,2), dtype=np.int32) connect = np.empty((n/2,2), dtype=np.int32)
if connect.size != n:
raise Exception("x,y array lengths must be multiple of 2 to use connect='pairs'")
connect[:,0] = 1 connect[:,0] = 1
connect[:,1] = 0 connect[:,1] = 0
connect = connect.flatten() connect = connect.flatten()
@ -1240,19 +1390,19 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False):
""" """
Generate isocurve from 2D data using marching squares algorithm. Generate isocurve from 2D data using marching squares algorithm.
============= ========================================================= ============== =========================================================
Arguments **Arguments:**
data 2D numpy array of scalar values data 2D numpy array of scalar values
level The level at which to generate an isosurface level The level at which to generate an isosurface
connected If False, return a single long list of point pairs connected If False, return a single long list of point pairs
If True, return multiple long lists of connected point If True, return multiple long lists of connected point
locations. (This is slower but better for drawing locations. (This is slower but better for drawing
continuous lines) continuous lines)
extendToEdge If True, extend the curves to reach the exact edges of extendToEdge If True, extend the curves to reach the exact edges of
the data. the data.
path if True, return a QPainterPath rather than a list of path if True, return a QPainterPath rather than a list of
vertex coordinates. This forces connected=True. vertex coordinates. This forces connected=True.
============= ========================================================= ============== =========================================================
This function is SLOW; plenty of room for optimization here. This function is SLOW; plenty of room for optimization here.
""" """
@ -1274,30 +1424,30 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False):
data = d2 data = d2
sideTable = [ sideTable = [
[], [],
[0,1], [0,1],
[1,2], [1,2],
[0,2], [0,2],
[0,3], [0,3],
[1,3], [1,3],
[0,1,2,3], [0,1,2,3],
[2,3], [2,3],
[2,3], [2,3],
[0,1,2,3], [0,1,2,3],
[1,3], [1,3],
[0,3], [0,3],
[0,2], [0,2],
[1,2], [1,2],
[0,1], [0,1],
[] []
] ]
edgeKey=[ edgeKey=[
[(0,1), (0,0)], [(0,1), (0,0)],
[(0,0), (1,0)], [(0,0), (1,0)],
[(1,0), (1,1)], [(1,0), (1,1)],
[(1,1), (0,1)] [(1,1), (0,1)]
] ]
lines = [] lines = []
@ -1427,7 +1577,11 @@ def traceImage(image, values, smooth=0.5):
If image is RGB or RGBA, then the shape of values should be (nvals, 3/4) If image is RGB or RGBA, then the shape of values should be (nvals, 3/4)
The parameter *smooth* is expressed in pixels. The parameter *smooth* is expressed in pixels.
""" """
import scipy.ndimage as ndi try:
import scipy.ndimage as ndi
except ImportError:
raise Exception("traceImage() requires the package scipy.ndimage, but it is not importable.")
if values.ndim == 2: if values.ndim == 2:
values = values.T values = values.T
values = values[np.newaxis, np.newaxis, ...].astype(float) values = values[np.newaxis, np.newaxis, ...].astype(float)
@ -1441,7 +1595,7 @@ def traceImage(image, values, smooth=0.5):
paths = [] paths = []
for i in range(diff.shape[-1]): for i in range(diff.shape[-1]):
d = (labels==i).astype(float) d = (labels==i).astype(float)
d = ndi.gaussian_filter(d, (smooth, smooth)) d = gaussianFilter(d, (smooth, smooth))
lines = isocurve(d, 0.5, connected=True, extendToEdge=True) lines = isocurve(d, 0.5, connected=True, extendToEdge=True)
path = QtGui.QPainterPath() path = QtGui.QPainterPath()
for line in lines: for line in lines:
@ -1481,38 +1635,39 @@ def isosurface(data, level):
## edge index tells us which edges are cut by the isosurface. ## edge index tells us which edges are cut by the isosurface.
## (Data stolen from Bourk; see above.) ## (Data stolen from Bourk; see above.)
edgeTable = np.array([ edgeTable = np.array([
0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, 0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00, 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c, 0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, 0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
0x230, 0x339, 0x33 , 0x13a, 0x636, 0x73f, 0x435, 0x53c, 0x230, 0x339, 0x33 , 0x13a, 0x636, 0x73f, 0x435, 0x53c,
0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30, 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
0x3a0, 0x2a9, 0x1a3, 0xaa , 0x7a6, 0x6af, 0x5a5, 0x4ac, 0x3a0, 0x2a9, 0x1a3, 0xaa , 0x7a6, 0x6af, 0x5a5, 0x4ac,
0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0, 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
0x460, 0x569, 0x663, 0x76a, 0x66 , 0x16f, 0x265, 0x36c, 0x460, 0x569, 0x663, 0x76a, 0x66 , 0x16f, 0x265, 0x36c,
0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60, 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff , 0x3f5, 0x2fc, 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff , 0x3f5, 0x2fc,
0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0, 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55 , 0x15c, 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55 , 0x15c,
0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950, 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc , 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc ,
0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0, 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc, 0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
0xcc , 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0, 0xcc , 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c, 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
0x15c, 0x55 , 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650, 0x15c, 0x55 , 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc, 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
0x2fc, 0x3f5, 0xff , 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0, 0x2fc, 0x3f5, 0xff , 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c, 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
0x36c, 0x265, 0x16f, 0x66 , 0x76a, 0x663, 0x569, 0x460, 0x36c, 0x265, 0x16f, 0x66 , 0x76a, 0x663, 0x569, 0x460,
0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac, 0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa , 0x1a3, 0x2a9, 0x3a0, 0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa , 0x1a3, 0x2a9, 0x3a0,
0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c, 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c,
0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33 , 0x339, 0x230, 0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33 , 0x339, 0x230,
0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c, 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190, 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190,
0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c, 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 ], dtype=np.uint16) 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0
], dtype=np.uint16)
## Table of triangles to use for filling each grid cell. ## Table of triangles to use for filling each grid cell.
## Each set of three integers tells us which three edges to ## Each set of three integers tells us which three edges to
@ -1790,7 +1945,7 @@ def isosurface(data, level):
[1, 1, 0, 2], [1, 1, 0, 2],
[0, 1, 0, 2], [0, 1, 0, 2],
#[9, 9, 9, 9] ## fake #[9, 9, 9, 9] ## fake
], dtype=np.ubyte) ], dtype=np.uint16) # don't use ubyte here! This value gets added to cell index later; will need the extra precision.
nTableFaces = np.array([len(f)/3 for f in triTable], dtype=np.ubyte) nTableFaces = np.array([len(f)/3 for f in triTable], dtype=np.ubyte)
faceShiftTables = [None] faceShiftTables = [None]
for i in range(1,6): for i in range(1,6):
@ -1889,7 +2044,6 @@ def isosurface(data, level):
#profiler() #profiler()
if cells.shape[0] == 0: if cells.shape[0] == 0:
continue continue
#cellInds = index[(cells*ins[np.newaxis,:]).sum(axis=1)]
cellInds = index[cells[:,0], cells[:,1], cells[:,2]] ## index values of cells to process for this round cellInds = index[cells[:,0], cells[:,1], cells[:,2]] ## index values of cells to process for this round
#profiler() #profiler()
@ -1901,9 +2055,7 @@ def isosurface(data, level):
#profiler() #profiler()
### expensive: ### expensive:
#print verts.shape
verts = (verts * cs[np.newaxis, np.newaxis, :]).sum(axis=2) verts = (verts * cs[np.newaxis, np.newaxis, :]).sum(axis=2)
#vertInds = cutEdges[verts[...,0], verts[...,1], verts[...,2], verts[...,3]] ## and these are the vertex indexes we want.
vertInds = cutEdges[verts] vertInds = cutEdges[verts]
#profiler() #profiler()
nv = vertInds.shape[0] nv = vertInds.shape[0]
@ -1924,14 +2076,16 @@ def invertQTransform(tr):
bugs in that method. (specifically, Qt has floating-point precision issues bugs in that method. (specifically, Qt has floating-point precision issues
when determining whether a matrix is invertible) when determining whether a matrix is invertible)
""" """
if not HAVE_SCIPY: try:
import numpy.linalg
arr = np.array([[tr.m11(), tr.m12(), tr.m13()], [tr.m21(), tr.m22(), tr.m23()], [tr.m31(), tr.m32(), tr.m33()]])
inv = numpy.linalg.inv(arr)
return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1])
except ImportError:
inv = tr.inverted() inv = tr.inverted()
if inv[1] is False: if inv[1] is False:
raise Exception("Transform is not invertible.") raise Exception("Transform is not invertible.")
return inv[0] return inv[0]
arr = np.array([[tr.m11(), tr.m12(), tr.m13()], [tr.m21(), tr.m22(), tr.m23()], [tr.m31(), tr.m32(), tr.m33()]])
inv = scipy.linalg.inv(arr)
return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1])
def pseudoScatter(data, spacing=None, shuffle=True, bidir=False): def pseudoScatter(data, spacing=None, shuffle=True, bidir=False):

View File

@ -16,12 +16,14 @@ class ArrowItem(QtGui.QGraphicsPathItem):
Arrows can be initialized with any keyword arguments accepted by Arrows can be initialized with any keyword arguments accepted by
the setStyle() method. the setStyle() method.
""" """
self.opts = {}
QtGui.QGraphicsPathItem.__init__(self, opts.get('parent', None)) QtGui.QGraphicsPathItem.__init__(self, opts.get('parent', None))
if 'size' in opts: if 'size' in opts:
opts['headLen'] = opts['size'] opts['headLen'] = opts['size']
if 'width' in opts: if 'width' in opts:
opts['headWidth'] = opts['width'] opts['headWidth'] = opts['width']
defOpts = { defaultOpts = {
'pxMode': True, 'pxMode': True,
'angle': -150, ## If the angle is 0, the arrow points left 'angle': -150, ## If the angle is 0, the arrow points left
'pos': (0,0), 'pos': (0,0),
@ -33,12 +35,9 @@ class ArrowItem(QtGui.QGraphicsPathItem):
'pen': (200,200,200), 'pen': (200,200,200),
'brush': (50,50,200), 'brush': (50,50,200),
} }
defOpts.update(opts) defaultOpts.update(opts)
self.setStyle(**defOpts) self.setStyle(**defaultOpts)
self.setPen(fn.mkPen(defOpts['pen']))
self.setBrush(fn.mkBrush(defOpts['brush']))
self.rotate(self.opts['angle']) self.rotate(self.opts['angle'])
self.moveBy(*self.opts['pos']) self.moveBy(*self.opts['pos'])
@ -48,35 +47,38 @@ class ArrowItem(QtGui.QGraphicsPathItem):
Changes the appearance of the arrow. Changes the appearance of the arrow.
All arguments are optional: All arguments are optional:
================= ================================================= ====================== =================================================
Keyword Arguments **Keyword Arguments:**
angle Orientation of the arrow in degrees. Default is angle Orientation of the arrow in degrees. Default is
0; arrow pointing to the left. 0; arrow pointing to the left.
headLen Length of the arrow head, from tip to base. headLen Length of the arrow head, from tip to base.
default=20 default=20
headWidth Width of the arrow head at its base. headWidth Width of the arrow head at its base.
tipAngle Angle of the tip of the arrow in degrees. Smaller tipAngle Angle of the tip of the arrow in degrees. Smaller
values make a 'sharper' arrow. If tipAngle is values make a 'sharper' arrow. If tipAngle is
specified, ot overrides headWidth. default=25 specified, ot overrides headWidth. default=25
baseAngle Angle of the base of the arrow head. Default is baseAngle Angle of the base of the arrow head. Default is
0, which means that the base of the arrow head 0, which means that the base of the arrow head
is perpendicular to the arrow shaft. is perpendicular to the arrow tail.
tailLen Length of the arrow tail, measured from the base tailLen Length of the arrow tail, measured from the base
of the arrow head to the tip of the tail. If of the arrow head to the end of the tail. If
this value is None, no tail will be drawn. this value is None, no tail will be drawn.
default=None default=None
tailWidth Width of the tail. default=3 tailWidth Width of the tail. default=3
pen The pen used to draw the outline of the arrow. pen The pen used to draw the outline of the arrow.
brush The brush used to fill the arrow. brush The brush used to fill the arrow.
================= ================================================= ====================== =================================================
""" """
self.opts = opts self.opts.update(opts)
opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']]) opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']])
self.path = fn.makeArrowPath(**opt) self.path = fn.makeArrowPath(**opt)
self.setPath(self.path) self.setPath(self.path)
if opts['pxMode']: self.setPen(fn.mkPen(self.opts['pen']))
self.setBrush(fn.mkBrush(self.opts['brush']))
if self.opts['pxMode']:
self.setFlags(self.flags() | self.ItemIgnoresTransformations) self.setFlags(self.flags() | self.ItemIgnoresTransformations)
else: else:
self.setFlags(self.flags() & ~self.ItemIgnoresTransformations) self.setFlags(self.flags() & ~self.ItemIgnoresTransformations)
@ -121,4 +123,4 @@ class ArrowItem(QtGui.QGraphicsPathItem):
return pad return pad

View File

@ -33,7 +33,6 @@ class AxisItem(GraphicsWidget):
GraphicsWidget.__init__(self, parent) GraphicsWidget.__init__(self, parent)
self.label = QtGui.QGraphicsTextItem(self) self.label = QtGui.QGraphicsTextItem(self)
self.showValues = showValues
self.picture = None self.picture = None
self.orientation = orientation self.orientation = orientation
if orientation not in ['left', 'right', 'top', 'bottom']: if orientation not in ['left', 'right', 'top', 'bottom']:
@ -42,7 +41,7 @@ class AxisItem(GraphicsWidget):
self.label.rotate(-90) self.label.rotate(-90)
self.style = { self.style = {
'tickTextOffset': (5, 2), ## (horizontal, vertical) spacing between text and axis 'tickTextOffset': [5, 2], ## (horizontal, vertical) spacing between text and axis
'tickTextWidth': 30, ## space reserved for tick text 'tickTextWidth': 30, ## space reserved for tick text
'tickTextHeight': 18, 'tickTextHeight': 18,
'autoExpandTextSpace': True, ## automatically expand text space if needed 'autoExpandTextSpace': True, ## automatically expand text space if needed
@ -53,7 +52,9 @@ class AxisItem(GraphicsWidget):
(2, 0.6), ## If we already have 2 ticks with text, fill no more than 60% of the axis (2, 0.6), ## If we already have 2 ticks with text, fill no more than 60% of the axis
(4, 0.4), ## If we already have 4 ticks with text, fill no more than 40% of the axis (4, 0.4), ## If we already have 4 ticks with text, fill no more than 40% of the axis
(6, 0.2), ## If we already have 6 ticks with text, fill no more than 20% of the axis (6, 0.2), ## If we already have 6 ticks with text, fill no more than 20% of the axis
] ],
'showValues': showValues,
'tickLength': maxTickLength,
} }
self.textWidth = 30 ## Keeps track of maximum width / height of tick text self.textWidth = 30 ## Keeps track of maximum width / height of tick text
@ -66,7 +67,6 @@ class AxisItem(GraphicsWidget):
self.logMode = False self.logMode = False
self.tickFont = None self.tickFont = None
self.tickLength = maxTickLength
self._tickLevels = None ## used to override the automatic ticking system with explicit ticks self._tickLevels = None ## used to override the automatic ticking system with explicit ticks
self.scale = 1.0 self.scale = 1.0
self.autoSIPrefix = True self.autoSIPrefix = True
@ -74,7 +74,10 @@ class AxisItem(GraphicsWidget):
self.setRange(0, 1) self.setRange(0, 1)
self.setPen(pen) if pen is None:
self.setPen()
else:
self.setPen(pen)
self._linkedView = None self._linkedView = None
if linkView is not None: if linkView is not None:
@ -84,6 +87,73 @@ class AxisItem(GraphicsWidget):
self.grid = False self.grid = False
#self.setCacheMode(self.DeviceCoordinateCache) #self.setCacheMode(self.DeviceCoordinateCache)
def setStyle(self, **kwds):
"""
Set various style options.
=================== =======================================================
Keyword Arguments:
tickLength (int) The maximum length of ticks in pixels.
Positive values point toward the text; negative
values point away.
tickTextOffset (int) reserved spacing between text and axis in px
tickTextWidth (int) Horizontal space reserved for tick text in px
tickTextHeight (int) Vertical space reserved for tick text in px
autoExpandTextSpace (bool) Automatically expand text space if the tick
strings become too long.
tickFont (QFont or None) Determines the font used for tick
values. Use None for the default font.
stopAxisAtTick (tuple: (bool min, bool max)) If True, the axis
line is drawn only as far as the last tick.
Otherwise, the line is drawn to the edge of the
AxisItem boundary.
textFillLimits (list of (tick #, % fill) tuples). This structure
determines how the AxisItem decides how many ticks
should have text appear next to them. Each tuple in
the list specifies what fraction of the axis length
may be occupied by text, given the number of ticks
that already have text displayed. For example::
[(0, 0.8), # Never fill more than 80% of the axis
(2, 0.6), # If we already have 2 ticks with text,
# fill no more than 60% of the axis
(4, 0.4), # If we already have 4 ticks with text,
# fill no more than 40% of the axis
(6, 0.2)] # If we already have 6 ticks with text,
# fill no more than 20% of the axis
showValues (bool) indicates whether text is displayed adjacent
to ticks.
=================== =======================================================
Added in version 0.9.9
"""
for kwd,value in kwds.items():
if kwd not in self.style:
raise NameError("%s is not a valid style argument." % kwd)
if kwd in ('tickLength', 'tickTextOffset', 'tickTextWidth', 'tickTextHeight'):
if not isinstance(value, int):
raise ValueError("Argument '%s' must be int" % kwd)
if kwd == 'tickTextOffset':
if self.orientation in ('left', 'right'):
self.style['tickTextOffset'][0] = value
else:
self.style['tickTextOffset'][1] = value
elif kwd == 'stopAxisAtTick':
try:
assert len(value) == 2 and isinstance(value[0], bool) and isinstance(value[1], bool)
except:
raise ValueError("Argument 'stopAxisAtTick' must have type (bool, bool)")
self.style[kwd] = value
else:
self.style[kwd] = value
self.picture = None
self._adjustSize()
self.update()
def close(self): def close(self):
self.scene().removeItem(self.label) self.scene().removeItem(self.label)
@ -125,20 +195,15 @@ class AxisItem(GraphicsWidget):
if self.orientation == 'left': if self.orientation == 'left':
p.setY(int(self.size().height()/2 + br.width()/2)) p.setY(int(self.size().height()/2 + br.width()/2))
p.setX(-nudge) p.setX(-nudge)
#s.setWidth(10)
elif self.orientation == 'right': elif self.orientation == 'right':
#s.setWidth(10)
p.setY(int(self.size().height()/2 + br.width()/2)) p.setY(int(self.size().height()/2 + br.width()/2))
p.setX(int(self.size().width()-br.height()+nudge)) p.setX(int(self.size().width()-br.height()+nudge))
elif self.orientation == 'top': elif self.orientation == 'top':
#s.setHeight(10)
p.setY(-nudge) p.setY(-nudge)
p.setX(int(self.size().width()/2. - br.width()/2.)) p.setX(int(self.size().width()/2. - br.width()/2.))
elif self.orientation == 'bottom': elif self.orientation == 'bottom':
p.setX(int(self.size().width()/2. - br.width()/2.)) p.setX(int(self.size().width()/2. - br.width()/2.))
#s.setHeight(10)
p.setY(int(self.size().height()-br.height()+nudge)) p.setY(int(self.size().height()-br.height()+nudge))
#self.label.resize(s)
self.label.setPos(p) self.label.setPos(p)
self.picture = None self.picture = None
@ -156,17 +221,17 @@ class AxisItem(GraphicsWidget):
def setLabel(self, text=None, units=None, unitPrefix=None, **args): def setLabel(self, text=None, units=None, unitPrefix=None, **args):
"""Set the text displayed adjacent to the axis. """Set the text displayed adjacent to the axis.
============= ============================================================= ============== =============================================================
Arguments **Arguments:**
text The text (excluding units) to display on the label for this text The text (excluding units) to display on the label for this
axis. axis.
units The units for this axis. Units should generally be given units The units for this axis. Units should generally be given
without any scaling prefix (eg, 'V' instead of 'mV'). The without any scaling prefix (eg, 'V' instead of 'mV'). The
scaling prefix will be automatically prepended based on the scaling prefix will be automatically prepended based on the
range of data displayed. range of data displayed.
**args All extra keyword arguments become CSS style options for **args All extra keyword arguments become CSS style options for
the <span> tag which will surround the axis label and units. the <span> tag which will surround the axis label and units.
============= ============================================================= ============== =============================================================
The final text generated for the label will look like:: The final text generated for the label will look like::
@ -239,27 +304,32 @@ class AxisItem(GraphicsWidget):
"""Set the height of this axis reserved for ticks and tick labels. """Set the height of this axis reserved for ticks and tick labels.
The height of the axis label is automatically added.""" The height of the axis label is automatically added."""
if h is None: if h is None:
if self.style['autoExpandTextSpace'] is True: if not self.style['showValues']:
h = 0
elif self.style['autoExpandTextSpace'] is True:
h = self.textHeight h = self.textHeight
else: else:
h = self.style['tickTextHeight'] h = self.style['tickTextHeight']
h += max(0, self.tickLength) + self.style['tickTextOffset'][1] h += self.style['tickTextOffset'][1] if self.style['showValues'] else 0
h += max(0, self.style['tickLength'])
if self.label.isVisible(): if self.label.isVisible():
h += self.label.boundingRect().height() * 0.8 h += self.label.boundingRect().height() * 0.8
self.setMaximumHeight(h) self.setMaximumHeight(h)
self.setMinimumHeight(h) self.setMinimumHeight(h)
self.picture = None self.picture = None
def setWidth(self, w=None): def setWidth(self, w=None):
"""Set the width of this axis reserved for ticks and tick labels. """Set the width of this axis reserved for ticks and tick labels.
The width of the axis label is automatically added.""" The width of the axis label is automatically added."""
if w is None: if w is None:
if self.style['autoExpandTextSpace'] is True: if not self.style['showValues']:
w = 0
elif self.style['autoExpandTextSpace'] is True:
w = self.textWidth w = self.textWidth
else: else:
w = self.style['tickTextWidth'] w = self.style['tickTextWidth']
w += max(0, self.tickLength) + self.style['tickTextOffset'][0] w += self.style['tickTextOffset'][0] if self.style['showValues'] else 0
w += max(0, self.style['tickLength'])
if self.label.isVisible(): if self.label.isVisible():
w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate
self.setMaximumWidth(w) self.setMaximumWidth(w)
@ -271,16 +341,17 @@ class AxisItem(GraphicsWidget):
return fn.mkPen(getConfigOption('foreground')) return fn.mkPen(getConfigOption('foreground'))
return fn.mkPen(self._pen) return fn.mkPen(self._pen)
def setPen(self, pen): def setPen(self, *args, **kwargs):
""" """
Set the pen used for drawing text, axes, ticks, and grid lines. Set the pen used for drawing text, axes, ticks, and grid lines.
if pen == None, the default will be used (see :func:`setConfigOption If no arguments are given, the default foreground color will be used
<pyqtgraph.setConfigOption>`) (see :func:`setConfigOption <pyqtgraph.setConfigOption>`).
""" """
self.picture = None self.picture = None
if pen is None: if args or kwargs:
pen = getConfigOption('foreground') self._pen = fn.mkPen(*args, **kwargs)
self._pen = fn.mkPen(pen) else:
self._pen = fn.mkPen(getConfigOption('foreground'))
self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6] self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6]
self.setLabel() self.setLabel()
self.update() self.update()
@ -391,14 +462,15 @@ class AxisItem(GraphicsWidget):
rect = self.mapRectFromParent(self.geometry()) rect = self.mapRectFromParent(self.geometry())
## extend rect if ticks go in negative direction ## extend rect if ticks go in negative direction
## also extend to account for text that flows past the edges ## also extend to account for text that flows past the edges
tl = self.style['tickLength']
if self.orientation == 'left': if self.orientation == 'left':
rect = rect.adjusted(0, -15, -min(0,self.tickLength), 15) rect = rect.adjusted(0, -15, -min(0,tl), 15)
elif self.orientation == 'right': elif self.orientation == 'right':
rect = rect.adjusted(min(0,self.tickLength), -15, 0, 15) rect = rect.adjusted(min(0,tl), -15, 0, 15)
elif self.orientation == 'top': elif self.orientation == 'top':
rect = rect.adjusted(-15, 0, 15, -min(0,self.tickLength)) rect = rect.adjusted(-15, 0, 15, -min(0,tl))
elif self.orientation == 'bottom': elif self.orientation == 'bottom':
rect = rect.adjusted(-15, min(0,self.tickLength), 15, 0) rect = rect.adjusted(-15, min(0,tl), 15, 0)
return rect return rect
else: else:
return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect()) return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect())
@ -618,7 +690,7 @@ class AxisItem(GraphicsWidget):
def generateDrawSpecs(self, p): def generateDrawSpecs(self, p):
""" """
Calls tickValues() and tickStrings to determine where and how ticks should Calls tickValues() and tickStrings() to determine where and how ticks should
be drawn, then generates from this a set of drawing commands to be be drawn, then generates from this a set of drawing commands to be
interpreted by drawPicture(). interpreted by drawPicture().
""" """
@ -667,6 +739,7 @@ class AxisItem(GraphicsWidget):
if lengthInPixels == 0: if lengthInPixels == 0:
return return
# Determine major / minor / subminor axis ticks
if self._tickLevels is None: if self._tickLevels is None:
tickLevels = self.tickValues(self.range[0], self.range[1], lengthInPixels) tickLevels = self.tickValues(self.range[0], self.range[1], lengthInPixels)
tickStrings = None tickStrings = None
@ -688,7 +761,7 @@ class AxisItem(GraphicsWidget):
## determine mapping between tick values and local coordinates ## determine mapping between tick values and local coordinates
dif = self.range[1] - self.range[0] dif = self.range[1] - self.range[0]
if dif == 0: if dif == 0:
xscale = 1 xScale = 1
offset = 0 offset = 0
else: else:
if axis == 0: if axis == 0:
@ -706,8 +779,7 @@ class AxisItem(GraphicsWidget):
tickPositions = [] # remembers positions of previously drawn ticks tickPositions = [] # remembers positions of previously drawn ticks
## draw ticks ## compute coordinates to draw ticks
## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching)
## draw three different intervals, long ticks first ## draw three different intervals, long ticks first
tickSpecs = [] tickSpecs = []
for i in range(len(tickLevels)): for i in range(len(tickLevels)):
@ -715,7 +787,7 @@ class AxisItem(GraphicsWidget):
ticks = tickLevels[i][1] ticks = tickLevels[i][1]
## length of tick ## length of tick
tickLength = self.tickLength / ((i*0.5)+1.0) tickLength = self.style['tickLength'] / ((i*0.5)+1.0)
lineAlpha = 255 / (i+1) lineAlpha = 255 / (i+1)
if self.grid is not False: if self.grid is not False:
@ -742,7 +814,6 @@ class AxisItem(GraphicsWidget):
tickSpecs.append((tickPen, Point(p1), Point(p2))) tickSpecs.append((tickPen, Point(p1), Point(p2)))
profiler('compute ticks') profiler('compute ticks')
## This is where the long axis line should be drawn
if self.style['stopAxisAtTick'][0] is True: if self.style['stopAxisAtTick'][0] is True:
stop = max(span[0].y(), min(map(min, tickPositions))) stop = max(span[0].y(), min(map(min, tickPositions)))
@ -759,7 +830,6 @@ class AxisItem(GraphicsWidget):
axisSpec = (self.pen(), span[0], span[1]) axisSpec = (self.pen(), span[0], span[1])
textOffset = self.style['tickTextOffset'][axis] ## spacing between axis and text textOffset = self.style['tickTextOffset'][axis] ## spacing between axis and text
#if self.style['autoExpandTextSpace'] is True: #if self.style['autoExpandTextSpace'] is True:
#textWidth = self.textWidth #textWidth = self.textWidth
@ -771,7 +841,11 @@ class AxisItem(GraphicsWidget):
textSize2 = 0 textSize2 = 0
textRects = [] textRects = []
textSpecs = [] ## list of draw textSpecs = [] ## list of draw
textSize2 = 0
# If values are hidden, return early
if not self.style['showValues']:
return (axisSpec, tickSpecs, textSpecs)
for i in range(len(tickLevels)): for i in range(len(tickLevels)):
## Get the list of strings to display for this level ## Get the list of strings to display for this level
if tickStrings is None: if tickStrings is None:
@ -802,15 +876,15 @@ class AxisItem(GraphicsWidget):
rects.append(br) rects.append(br)
textRects.append(rects[-1]) textRects.append(rects[-1])
if i > 0: ## always draw top level ## measure all text, make sure there's enough room
## measure all text, make sure there's enough room if axis == 0:
if axis == 0: textSize = np.sum([r.height() for r in textRects])
textSize = np.sum([r.height() for r in textRects]) textSize2 = np.max([r.width() for r in textRects]) if textRects else 0
textSize2 = np.max([r.width() for r in textRects]) else:
else: textSize = np.sum([r.width() for r in textRects])
textSize = np.sum([r.width() for r in textRects]) textSize2 = np.max([r.height() for r in textRects]) if textRects else 0
textSize2 = np.max([r.height() for r in textRects])
if i > 0: ## always draw top level
## If the strings are too crowded, stop drawing text now. ## If the strings are too crowded, stop drawing text now.
## We use three different crowding limits based on the number ## We use three different crowding limits based on the number
## of texts drawn so far. ## of texts drawn so far.
@ -825,6 +899,7 @@ class AxisItem(GraphicsWidget):
#spacing, values = tickLevels[best] #spacing, values = tickLevels[best]
#strings = self.tickStrings(values, self.scale, spacing) #strings = self.tickStrings(values, self.scale, spacing)
# Determine exactly where tick text should be drawn
for j in range(len(strings)): for j in range(len(strings)):
vstr = strings[j] vstr = strings[j]
if vstr is None: ## this tick was ignored because it is out of bounds if vstr is None: ## this tick was ignored because it is out of bounds
@ -836,7 +911,7 @@ class AxisItem(GraphicsWidget):
height = textRect.height() height = textRect.height()
width = textRect.width() width = textRect.width()
#self.textHeight = height #self.textHeight = height
offset = max(0,self.tickLength) + textOffset offset = max(0,self.style['tickLength']) + textOffset
if self.orientation == 'left': if self.orientation == 'left':
textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter
rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height) rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height)
@ -854,7 +929,7 @@ class AxisItem(GraphicsWidget):
#p.drawText(rect, textFlags, vstr) #p.drawText(rect, textFlags, vstr)
textSpecs.append((rect, textFlags, vstr)) textSpecs.append((rect, textFlags, vstr))
profiler('compute text') profiler('compute text')
## update max text size if needed. ## update max text size if needed.
self._updateMaxTextSize(textSize2) self._updateMaxTextSize(textSize2)

View File

@ -47,16 +47,20 @@ class BarGraphItem(GraphicsObject):
pens=None, pens=None,
brushes=None, brushes=None,
) )
self._shape = None
self.picture = None
self.setOpts(**opts) self.setOpts(**opts)
def setOpts(self, **opts): def setOpts(self, **opts):
self.opts.update(opts) self.opts.update(opts)
self.picture = None self.picture = None
self._shape = None
self.update() self.update()
self.informViewBoundsChanged() self.informViewBoundsChanged()
def drawPicture(self): def drawPicture(self):
self.picture = QtGui.QPicture() self.picture = QtGui.QPicture()
self._shape = QtGui.QPainterPath()
p = QtGui.QPainter(self.picture) p = QtGui.QPainter(self.picture)
pen = self.opts['pen'] pen = self.opts['pen']
@ -122,6 +126,10 @@ class BarGraphItem(GraphicsObject):
if brushes is not None: if brushes is not None:
p.setBrush(fn.mkBrush(brushes[i])) p.setBrush(fn.mkBrush(brushes[i]))
if np.isscalar(x0):
x = x0
else:
x = x0[i]
if np.isscalar(y0): if np.isscalar(y0):
y = y0 y = y0
else: else:
@ -130,9 +138,15 @@ class BarGraphItem(GraphicsObject):
w = width w = width
else: else:
w = width[i] w = width[i]
if np.isscalar(height):
p.drawRect(QtCore.QRectF(x0[i], y, w, height[i])) h = height
else:
h = height[i]
rect = QtCore.QRectF(x, y, w, h)
p.drawRect(rect)
self._shape.addRect(rect)
p.end() p.end()
self.prepareGeometryChange() self.prepareGeometryChange()
@ -148,4 +162,7 @@ class BarGraphItem(GraphicsObject):
self.drawPicture() self.drawPicture()
return QtCore.QRectF(self.picture.boundingRect()) return QtCore.QRectF(self.picture.boundingRect())
def shape(self):
if self.picture is None:
self.drawPicture()
return self._shape

View File

@ -112,6 +112,6 @@ class CurveArrow(CurvePoint):
self.arrow = ArrowItem.ArrowItem(**opts) self.arrow = ArrowItem.ArrowItem(**opts)
self.arrow.setParentItem(self) self.arrow.setParentItem(self)
def setStyle(**opts): def setStyle(self, **opts):
return self.arrow.setStyle(**opts) return self.arrow.setStyle(**opts)

View File

@ -22,13 +22,16 @@ class FillBetweenItem(QtGui.QGraphicsPathItem):
def setCurves(self, curve1, curve2): def setCurves(self, curve1, curve2):
"""Set the curves to fill between. """Set the curves to fill between.
Arguments must be instances of PlotDataItem or PlotCurveItem.""" Arguments must be instances of PlotDataItem or PlotCurveItem.
Added in version 0.9.9
"""
if self.curves is not None: if self.curves is not None:
for c in self.curves: for c in self.curves:
try: try:
c.sigPlotChanged.disconnect(self.curveChanged) c.sigPlotChanged.disconnect(self.curveChanged)
except TypeError: except (TypeError, RuntimeError):
pass pass
curves = [curve1, curve2] curves = [curve1, curve2]

View File

@ -35,14 +35,14 @@ class TickSliderItem(GraphicsWidget):
def __init__(self, orientation='bottom', allowAdd=True, **kargs): def __init__(self, orientation='bottom', allowAdd=True, **kargs):
""" """
============= ================================================================================= ============== =================================================================================
**Arguments** **Arguments:**
orientation Set the orientation of the gradient. Options are: 'left', 'right' orientation Set the orientation of the gradient. Options are: 'left', 'right'
'top', and 'bottom'. 'top', and 'bottom'.
allowAdd Specifies whether ticks can be added to the item by the user. allowAdd Specifies whether ticks can be added to the item by the user.
tickPen Default is white. Specifies the color of the outline of the ticks. tickPen Default is white. Specifies the color of the outline of the ticks.
Can be any of the valid arguments for :func:`mkPen <pyqtgraph.mkPen>` Can be any of the valid arguments for :func:`mkPen <pyqtgraph.mkPen>`
============= ================================================================================= ============== =================================================================================
""" """
## public ## public
GraphicsWidget.__init__(self) GraphicsWidget.__init__(self)
@ -103,13 +103,13 @@ class TickSliderItem(GraphicsWidget):
## public ## public
"""Set the orientation of the TickSliderItem. """Set the orientation of the TickSliderItem.
============= =================================================================== ============== ===================================================================
**Arguments** **Arguments:**
orientation Options are: 'left', 'right', 'top', 'bottom' orientation Options are: 'left', 'right', 'top', 'bottom'
The orientation option specifies which side of the slider the The orientation option specifies which side of the slider the
ticks are on, as well as whether the slider is vertical ('right' ticks are on, as well as whether the slider is vertical ('right'
and 'left') or horizontal ('top' and 'bottom'). and 'left') or horizontal ('top' and 'bottom').
============= =================================================================== ============== ===================================================================
""" """
self.orientation = orientation self.orientation = orientation
self.setMaxDim() self.setMaxDim()
@ -136,13 +136,13 @@ class TickSliderItem(GraphicsWidget):
""" """
Add a tick to the item. Add a tick to the item.
============= ================================================================== ============== ==================================================================
**Arguments** **Arguments:**
x Position where tick should be added. x Position where tick should be added.
color Color of added tick. If color is not specified, the color will be color Color of added tick. If color is not specified, the color will be
white. white.
movable Specifies whether the tick is movable with the mouse. movable Specifies whether the tick is movable with the mouse.
============= ================================================================== ============== ==================================================================
""" """
if color is None: if color is None:
@ -265,14 +265,14 @@ class TickSliderItem(GraphicsWidget):
def setTickColor(self, tick, color): def setTickColor(self, tick, color):
"""Set the color of the specified tick. """Set the color of the specified tick.
============= ================================================================== ============== ==================================================================
**Arguments** **Arguments:**
tick Can be either an integer corresponding to the index of the tick tick Can be either an integer corresponding to the index of the tick
or a Tick object. Ex: if you had a slider with 3 ticks and you or a Tick object. Ex: if you had a slider with 3 ticks and you
wanted to change the middle tick, the index would be 1. wanted to change the middle tick, the index would be 1.
color The color to make the tick. Can be any argument that is valid for color The color to make the tick. Can be any argument that is valid for
:func:`mkBrush <pyqtgraph.mkBrush>` :func:`mkBrush <pyqtgraph.mkBrush>`
============= ================================================================== ============== ==================================================================
""" """
tick = self.getTick(tick) tick = self.getTick(tick)
tick.color = color tick.color = color
@ -284,14 +284,14 @@ class TickSliderItem(GraphicsWidget):
""" """
Set the position (along the slider) of the tick. Set the position (along the slider) of the tick.
============= ================================================================== ============== ==================================================================
**Arguments** **Arguments:**
tick Can be either an integer corresponding to the index of the tick tick Can be either an integer corresponding to the index of the tick
or a Tick object. Ex: if you had a slider with 3 ticks and you or a Tick object. Ex: if you had a slider with 3 ticks and you
wanted to change the middle tick, the index would be 1. wanted to change the middle tick, the index would be 1.
val The desired position of the tick. If val is < 0, position will be val The desired position of the tick. If val is < 0, position will be
set to 0. If val is > 1, position will be set to 1. set to 0. If val is > 1, position will be set to 1.
============= ================================================================== ============== ==================================================================
""" """
tick = self.getTick(tick) tick = self.getTick(tick)
val = min(max(0.0, val), 1.0) val = min(max(0.0, val), 1.0)
@ -305,12 +305,12 @@ class TickSliderItem(GraphicsWidget):
## public ## public
"""Return the value (from 0.0 to 1.0) of the specified tick. """Return the value (from 0.0 to 1.0) of the specified tick.
============= ================================================================== ============== ==================================================================
**Arguments** **Arguments:**
tick Can be either an integer corresponding to the index of the tick tick Can be either an integer corresponding to the index of the tick
or a Tick object. Ex: if you had a slider with 3 ticks and you or a Tick object. Ex: if you had a slider with 3 ticks and you
wanted the value of the middle tick, the index would be 1. wanted the value of the middle tick, the index would be 1.
============= ================================================================== ============== ==================================================================
""" """
tick = self.getTick(tick) tick = self.getTick(tick)
return self.ticks[tick] return self.ticks[tick]
@ -319,11 +319,11 @@ class TickSliderItem(GraphicsWidget):
## public ## public
"""Return the Tick object at the specified index. """Return the Tick object at the specified index.
============= ================================================================== ============== ==================================================================
**Arguments** **Arguments:**
tick An integer corresponding to the index of the desired tick. If the tick An integer corresponding to the index of the desired tick. If the
argument is not an integer it will be returned unchanged. argument is not an integer it will be returned unchanged.
============= ================================================================== ============== ==================================================================
""" """
if type(tick) is int: if type(tick) is int:
tick = self.listTicks()[tick][0] tick = self.listTicks()[tick][0]
@ -349,7 +349,7 @@ class GradientEditorItem(TickSliderItem):
with a GradientEditorItem that can be added to a GUI. with a GradientEditorItem that can be added to a GUI.
================================ =========================================================== ================================ ===========================================================
**Signals** **Signals:**
sigGradientChanged(self) Signal is emitted anytime the gradient changes. The signal sigGradientChanged(self) Signal is emitted anytime the gradient changes. The signal
is emitted in real time while ticks are being dragged or is emitted in real time while ticks are being dragged or
colors are being changed. colors are being changed.
@ -366,14 +366,14 @@ class GradientEditorItem(TickSliderItem):
Create a new GradientEditorItem. Create a new GradientEditorItem.
All arguments are passed to :func:`TickSliderItem.__init__ <pyqtgraph.TickSliderItem.__init__>` All arguments are passed to :func:`TickSliderItem.__init__ <pyqtgraph.TickSliderItem.__init__>`
============= ================================================================================= =============== =================================================================================
**Arguments** **Arguments:**
orientation Set the orientation of the gradient. Options are: 'left', 'right' orientation Set the orientation of the gradient. Options are: 'left', 'right'
'top', and 'bottom'. 'top', and 'bottom'.
allowAdd Default is True. Specifies whether ticks can be added to the item. allowAdd Default is True. Specifies whether ticks can be added to the item.
tickPen Default is white. Specifies the color of the outline of the ticks. tickPen Default is white. Specifies the color of the outline of the ticks.
Can be any of the valid arguments for :func:`mkPen <pyqtgraph.mkPen>` Can be any of the valid arguments for :func:`mkPen <pyqtgraph.mkPen>`
============= ================================================================================= =============== =================================================================================
""" """
self.currentTick = None self.currentTick = None
self.currentTickColor = None self.currentTickColor = None
@ -445,13 +445,13 @@ class GradientEditorItem(TickSliderItem):
""" """
Set the orientation of the GradientEditorItem. Set the orientation of the GradientEditorItem.
============= =================================================================== ============== ===================================================================
**Arguments** **Arguments:**
orientation Options are: 'left', 'right', 'top', 'bottom' orientation Options are: 'left', 'right', 'top', 'bottom'
The orientation option specifies which side of the gradient the The orientation option specifies which side of the gradient the
ticks are on, as well as whether the gradient is vertical ('right' ticks are on, as well as whether the gradient is vertical ('right'
and 'left') or horizontal ('top' and 'bottom'). and 'left') or horizontal ('top' and 'bottom').
============= =================================================================== ============== ===================================================================
""" """
TickSliderItem.setOrientation(self, orientation) TickSliderItem.setOrientation(self, orientation)
self.translate(0, self.rectSize) self.translate(0, self.rectSize)
@ -588,11 +588,11 @@ class GradientEditorItem(TickSliderItem):
""" """
Return a color for a given value. Return a color for a given value.
============= ================================================================== ============== ==================================================================
**Arguments** **Arguments:**
x Value (position on gradient) of requested color. x Value (position on gradient) of requested color.
toQColor If true, returns a QColor object, else returns a (r,g,b,a) tuple. toQColor If true, returns a QColor object, else returns a (r,g,b,a) tuple.
============= ================================================================== ============== ==================================================================
""" """
ticks = self.listTicks() ticks = self.listTicks()
if x <= ticks[0][1]: if x <= ticks[0][1]:
@ -648,12 +648,12 @@ class GradientEditorItem(TickSliderItem):
""" """
Return an RGB(A) lookup table (ndarray). Return an RGB(A) lookup table (ndarray).
============= ============================================================================ ============== ============================================================================
**Arguments** **Arguments:**
nPts The number of points in the returned lookup table. nPts The number of points in the returned lookup table.
alpha True, False, or None - Specifies whether or not alpha values are included alpha True, False, or None - Specifies whether or not alpha values are included
in the table.If alpha is None, alpha will be automatically determined. in the table.If alpha is None, alpha will be automatically determined.
============= ============================================================================ ============== ============================================================================
""" """
if alpha is None: if alpha is None:
alpha = self.usesAlpha() alpha = self.usesAlpha()
@ -702,13 +702,13 @@ class GradientEditorItem(TickSliderItem):
""" """
Add a tick to the gradient. Return the tick. Add a tick to the gradient. Return the tick.
============= ================================================================== ============== ==================================================================
**Arguments** **Arguments:**
x Position where tick should be added. x Position where tick should be added.
color Color of added tick. If color is not specified, the color will be color Color of added tick. If color is not specified, the color will be
the color of the gradient at the specified position. the color of the gradient at the specified position.
movable Specifies whether the tick is movable with the mouse. movable Specifies whether the tick is movable with the mouse.
============= ================================================================== ============== ==================================================================
""" """
@ -748,16 +748,16 @@ class GradientEditorItem(TickSliderItem):
""" """
Restore the gradient specified in state. Restore the gradient specified in state.
============= ==================================================================== ============== ====================================================================
**Arguments** **Arguments:**
state A dictionary with same structure as those returned by state A dictionary with same structure as those returned by
:func:`saveState <pyqtgraph.GradientEditorItem.saveState>` :func:`saveState <pyqtgraph.GradientEditorItem.saveState>`
Keys must include: Keys must include:
- 'mode': hsv or rgb - 'mode': hsv or rgb
- 'ticks': a list of tuples (pos, (r,g,b,a)) - 'ticks': a list of tuples (pos, (r,g,b,a))
============= ==================================================================== ============== ====================================================================
""" """
## public ## public
self.setColorMode(state['mode']) self.setColorMode(state['mode'])

View File

@ -28,48 +28,72 @@ class GraphItem(GraphicsObject):
""" """
Change the data displayed by the graph. Change the data displayed by the graph.
============ ========================================================= ============== =======================================================================
Arguments **Arguments:**
pos (N,2) array of the positions of each node in the graph. pos (N,2) array of the positions of each node in the graph.
adj (M,2) array of connection data. Each row contains indexes adj (M,2) array of connection data. Each row contains indexes
of two nodes that are connected. of two nodes that are connected.
pen The pen to use when drawing lines between connected pen The pen to use when drawing lines between connected
nodes. May be one of: nodes. May be one of:
* QPen * QPen
* a single argument to pass to pg.mkPen * a single argument to pass to pg.mkPen
* a record array of length M * a record array of length M
with fields (red, green, blue, alpha, width). Note with fields (red, green, blue, alpha, width). Note
that using this option may have a significant performance that using this option may have a significant performance
cost. cost.
* None (to disable connection drawing) * None (to disable connection drawing)
* 'default' to use the default foreground color. * 'default' to use the default foreground color.
symbolPen The pen used for drawing nodes. symbolPen The pen(s) used for drawing nodes.
``**opts`` All other keyword arguments are given to symbolBrush The brush(es) used for drawing nodes.
:func:`ScatterPlotItem.setData() <pyqtgraph.ScatterPlotItem.setData>` ``**opts`` All other keyword arguments are given to
to affect the appearance of nodes (symbol, size, brush, :func:`ScatterPlotItem.setData() <pyqtgraph.ScatterPlotItem.setData>`
etc.) to affect the appearance of nodes (symbol, size, brush,
============ ========================================================= etc.)
============== =======================================================================
""" """
if 'adj' in kwds: if 'adj' in kwds:
self.adjacency = kwds.pop('adj') self.adjacency = kwds.pop('adj')
assert self.adjacency.dtype.kind in 'iu' if self.adjacency.dtype.kind not in 'iu':
self.picture = None raise Exception("adjacency array must have int or unsigned type.")
self._update()
if 'pos' in kwds: if 'pos' in kwds:
self.pos = kwds['pos'] self.pos = kwds['pos']
self.picture = None self._update()
if 'pen' in kwds: if 'pen' in kwds:
self.setPen(kwds.pop('pen')) self.setPen(kwds.pop('pen'))
self.picture = None self._update()
if 'symbolPen' in kwds: if 'symbolPen' in kwds:
kwds['pen'] = kwds.pop('symbolPen') kwds['pen'] = kwds.pop('symbolPen')
if 'symbolBrush' in kwds:
kwds['brush'] = kwds.pop('symbolBrush')
self.scatter.setData(**kwds) self.scatter.setData(**kwds)
self.informViewBoundsChanged() self.informViewBoundsChanged()
def setPen(self, pen): def _update(self):
self.pen = pen
self.picture = None self.picture = None
self.prepareGeometryChange()
self.update()
def setPen(self, *args, **kwargs):
"""
Set the pen used to draw graph lines.
May be:
* None to disable line drawing
* Record array with fields (red, green, blue, alpha, width)
* Any set of arguments and keyword arguments accepted by
:func:`mkPen <pyqtgraph.mkPen>`.
* 'default' to use the default foreground color.
"""
if len(args) == 1 and len(kwargs) == 0:
self.pen = args[0]
else:
self.pen = fn.mkPen(*args, **kwargs)
self.picture = None
self.update()
def generatePicture(self): def generatePicture(self):
self.picture = QtGui.QPicture() self.picture = QtGui.QPicture()

View File

@ -1,31 +1,11 @@
from ..Qt import QtGui, QtCore from ..Qt import QtGui, QtCore, isQObjectAlive
from ..GraphicsScene import GraphicsScene from ..GraphicsScene import GraphicsScene
from ..Point import Point from ..Point import Point
from .. import functions as fn from .. import functions as fn
import weakref import weakref
from ..pgcollections import OrderedDict import operator
import operator, sys from ..util.lru_cache import LRUCache
class FiniteCache(OrderedDict):
"""Caches a finite number of objects, removing
least-frequently used items."""
def __init__(self, length):
self._length = length
OrderedDict.__init__(self)
def __setitem__(self, item, val):
self.pop(item, None) # make sure item is added to end
OrderedDict.__setitem__(self, item, val)
while len(self) > self._length:
del self[list(self.keys())[0]]
def __getitem__(self, item):
val = OrderedDict.__getitem__(self, item)
del self[item]
self[item] = val ## promote this key
return val
class GraphicsItem(object): class GraphicsItem(object):
""" """
@ -38,7 +18,7 @@ class GraphicsItem(object):
The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task. The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task.
""" """
_pixelVectorGlobalCache = FiniteCache(100) _pixelVectorGlobalCache = LRUCache(100, 70)
def __init__(self, register=True): def __init__(self, register=True):
if not hasattr(self, '_qtBaseClass'): if not hasattr(self, '_qtBaseClass'):
@ -62,8 +42,11 @@ class GraphicsItem(object):
def getViewWidget(self): def getViewWidget(self):
""" """
Return the view widget for this item. If the scene has multiple views, only the first view is returned. Return the view widget for this item.
The return value is cached; clear the cached value with forgetViewWidget()
If the scene has multiple views, only the first view is returned.
The return value is cached; clear the cached value with forgetViewWidget().
If the view has been deleted by Qt, return None.
""" """
if self._viewWidget is None: if self._viewWidget is None:
scene = self.scene() scene = self.scene()
@ -73,7 +56,12 @@ class GraphicsItem(object):
if len(views) < 1: if len(views) < 1:
return None return None
self._viewWidget = weakref.ref(self.scene().views()[0]) self._viewWidget = weakref.ref(self.scene().views()[0])
return self._viewWidget()
v = self._viewWidget()
if v is not None and not isQObjectAlive(v):
return None
return v
def forgetViewWidget(self): def forgetViewWidget(self):
self._viewWidget = None self._viewWidget = None
@ -479,24 +467,29 @@ class GraphicsItem(object):
## disconnect from previous view ## disconnect from previous view
if oldView is not None: if oldView is not None:
#print "disconnect:", self, oldView for signal, slot in [('sigRangeChanged', self.viewRangeChanged),
try: ('sigDeviceRangeChanged', self.viewRangeChanged),
oldView.sigRangeChanged.disconnect(self.viewRangeChanged) ('sigTransformChanged', self.viewTransformChanged),
except TypeError: ('sigDeviceTransformChanged', self.viewTransformChanged)]:
pass try:
getattr(oldView, signal).disconnect(slot)
try: except (TypeError, AttributeError, RuntimeError):
oldView.sigTransformChanged.disconnect(self.viewTransformChanged) # TypeError and RuntimeError are from pyqt and pyside, respectively
except TypeError: pass
pass
self._connectedView = None self._connectedView = None
## connect to new view ## connect to new view
if view is not None: if view is not None:
#print "connect:", self, view #print "connect:", self, view
view.sigRangeChanged.connect(self.viewRangeChanged) if hasattr(view, 'sigDeviceRangeChanged'):
view.sigTransformChanged.connect(self.viewTransformChanged) # connect signals from GraphicsView
view.sigDeviceRangeChanged.connect(self.viewRangeChanged)
view.sigDeviceTransformChanged.connect(self.viewTransformChanged)
else:
# connect signals from ViewBox
view.sigRangeChanged.connect(self.viewRangeChanged)
view.sigTransformChanged.connect(self.viewTransformChanged)
self._connectedView = weakref.ref(view) self._connectedView = weakref.ref(view)
self.viewRangeChanged() self.viewRangeChanged()
self.viewTransformChanged() self.viewTransformChanged()

View File

@ -31,6 +31,15 @@ class GraphicsLayout(GraphicsWidget):
#ret = GraphicsWidget.resizeEvent(self, ev) #ret = GraphicsWidget.resizeEvent(self, ev)
#print self.pos(), self.mapToDevice(self.rect().topLeft()) #print self.pos(), self.mapToDevice(self.rect().topLeft())
#return ret #return ret
def setBorder(self, *args, **kwds):
"""
Set the pen used to draw border between cells.
See :func:`mkPen <pyqtgraph.mkPen>` for arguments.
"""
self.border = fn.mkPen(*args, **kwds)
self.update()
def nextRow(self): def nextRow(self):
"""Advance to next row for automatic item placement""" """Advance to next row for automatic item placement"""

View File

@ -21,8 +21,15 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
ret = QtGui.QGraphicsObject.itemChange(self, change, value) ret = QtGui.QGraphicsObject.itemChange(self, change, value)
if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]: if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
self.parentChanged() self.parentChanged()
if self.__inform_view_on_changes and change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]: try:
self.informViewBoundsChanged() inform_view_on_change = self.__inform_view_on_changes
except AttributeError:
# It's possible that the attribute was already collected when the itemChange happened
# (if it was triggered during the gc of the object).
pass
else:
if inform_view_on_change and change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
self.informViewBoundsChanged()
## workaround for pyqt bug: ## workaround for pyqt bug:
## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html ## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html

View File

@ -58,7 +58,7 @@ class HistogramLUTItem(GraphicsWidget):
self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal) self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal)
self.region.setZValue(1000) self.region.setZValue(1000)
self.vb.addItem(self.region) self.vb.addItem(self.region)
self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, showValues=False) self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10)
self.layout.addItem(self.axis, 0, 0) self.layout.addItem(self.axis, 0, 0)
self.layout.addItem(self.vb, 0, 1) self.layout.addItem(self.vb, 0, 1)
self.layout.addItem(self.gradient, 0, 2) self.layout.addItem(self.gradient, 0, 2)

View File

@ -6,6 +6,7 @@ import collections
from .. import functions as fn from .. import functions as fn
from .. import debug as debug from .. import debug as debug
from .GraphicsObject import GraphicsObject from .GraphicsObject import GraphicsObject
from ..Point import Point
__all__ = ['ImageItem'] __all__ = ['ImageItem']
class ImageItem(GraphicsObject): class ImageItem(GraphicsObject):
@ -34,20 +35,16 @@ class ImageItem(GraphicsObject):
See :func:`setImage <pyqtgraph.ImageItem.setImage>` for all allowed initialization arguments. See :func:`setImage <pyqtgraph.ImageItem.setImage>` for all allowed initialization arguments.
""" """
GraphicsObject.__init__(self) GraphicsObject.__init__(self)
#self.pixmapItem = QtGui.QGraphicsPixmapItem(self)
#self.qimage = QtGui.QImage()
#self._pixmap = None
self.menu = None self.menu = None
self.image = None ## original image data self.image = None ## original image data
self.qimage = None ## rendered image for display self.qimage = None ## rendered image for display
#self.clipMask = None
self.paintMode = None self.paintMode = None
self.levels = None ## [min, max] or [[redMin, redMax], ...] self.levels = None ## [min, max] or [[redMin, redMax], ...]
self.lut = None self.lut = None
self.autoDownsample = False
#self.clipLevel = None
self.drawKernel = None self.drawKernel = None
self.border = None self.border = None
self.removable = False self.removable = False
@ -142,7 +139,18 @@ class ImageItem(GraphicsObject):
if update: if update:
self.updateImage() self.updateImage()
def setAutoDownsample(self, ads):
"""
Set the automatic downsampling mode for this ImageItem.
Added in version 0.9.9
"""
self.autoDownsample = ads
self.qimage = None
self.update()
def setOpts(self, update=True, **kargs): def setOpts(self, update=True, **kargs):
if 'lut' in kargs: if 'lut' in kargs:
self.setLookupTable(kargs['lut'], update=update) self.setLookupTable(kargs['lut'], update=update)
if 'levels' in kargs: if 'levels' in kargs:
@ -158,6 +166,10 @@ class ImageItem(GraphicsObject):
if 'removable' in kargs: if 'removable' in kargs:
self.removable = kargs['removable'] self.removable = kargs['removable']
self.menu = None self.menu = None
if 'autoDownsample' in kargs:
self.setAutoDownsample(kargs['autoDownsample'])
if update:
self.update()
def setRect(self, rect): def setRect(self, rect):
"""Scale and translate the image to fit within rect (must be a QRect or QRectF).""" """Scale and translate the image to fit within rect (must be a QRect or QRectF)."""
@ -188,6 +200,9 @@ class ImageItem(GraphicsObject):
opacity (float 0.0-1.0) opacity (float 0.0-1.0)
compositionMode see :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>` compositionMode see :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>`
border Sets the pen used when drawing the image border. Default is None. border Sets the pen used when drawing the image border. Default is None.
autoDownsample (bool) If True, the image is automatically downsampled to match the
screen resolution. This improves performance for large images and
reduces aliasing.
================= ========================================================================= ================= =========================================================================
""" """
profile = debug.Profiler() profile = debug.Profiler()
@ -200,6 +215,9 @@ class ImageItem(GraphicsObject):
gotNewData = True gotNewData = True
shapeChanged = (self.image is None or image.shape != self.image.shape) shapeChanged = (self.image is None or image.shape != self.image.shape)
self.image = image.view(np.ndarray) self.image = image.view(np.ndarray)
if self.image.shape[0] > 2**15-1 or self.image.shape[1] > 2**15-1:
if 'autoDownsample' not in kargs:
kargs['autoDownsample'] = True
if shapeChanged: if shapeChanged:
self.prepareGeometryChange() self.prepareGeometryChange()
self.informViewBoundsChanged() self.informViewBoundsChanged()
@ -246,11 +264,10 @@ class ImageItem(GraphicsObject):
} }
defaults.update(kargs) defaults.update(kargs)
return self.setImage(*args, **defaults) return self.setImage(*args, **defaults)
def render(self): def render(self):
# Convert data to QImage for display.
profile = debug.Profiler() profile = debug.Profiler()
if self.image is None or self.image.size == 0: if self.image is None or self.image.size == 0:
return return
@ -258,10 +275,22 @@ class ImageItem(GraphicsObject):
lut = self.lut(self.image) lut = self.lut(self.image)
else: else:
lut = self.lut lut = self.lut
#print lut.shape
#print self.lut if self.autoDownsample:
# reduce dimensions of image based on screen resolution
argb, alpha = fn.makeARGB(self.image.transpose((1, 0, 2)[:self.image.ndim]), lut=lut, levels=self.levels) o = self.mapToDevice(QtCore.QPointF(0,0))
x = self.mapToDevice(QtCore.QPointF(1,0))
y = self.mapToDevice(QtCore.QPointF(0,1))
w = Point(x-o).length()
h = Point(y-o).length()
xds = max(1, int(1/w))
yds = max(1, int(1/h))
image = fn.downsample(self.image, xds, axis=0)
image = fn.downsample(image, yds, axis=1)
else:
image = self.image
argb, alpha = fn.makeARGB(image.transpose((1, 0, 2)[:image.ndim]), lut=lut, levels=self.levels)
self.qimage = fn.makeQImage(argb, alpha, transpose=False) self.qimage = fn.makeQImage(argb, alpha, transpose=False)
def paint(self, p, *args): def paint(self, p, *args):
@ -277,7 +306,7 @@ class ImageItem(GraphicsObject):
p.setCompositionMode(self.paintMode) p.setCompositionMode(self.paintMode)
profile('set comp mode') profile('set comp mode')
p.drawImage(QtCore.QPointF(0,0), self.qimage) p.drawImage(QtCore.QRectF(0,0,self.image.shape[0],self.image.shape[1]), self.qimage)
profile('p.drawImage') profile('p.drawImage')
if self.border is not None: if self.border is not None:
p.setPen(self.border) p.setPen(self.border)
@ -322,6 +351,8 @@ class ImageItem(GraphicsObject):
mx = stepData.max() mx = stepData.max()
step = np.ceil((mx-mn) / 500.) step = np.ceil((mx-mn) / 500.)
bins = np.arange(mn, mx+1.01*step, step, dtype=np.int) bins = np.arange(mn, mx+1.01*step, step, dtype=np.int)
if len(bins) == 0:
bins = [mn, mx]
else: else:
bins = 500 bins = 500
@ -355,6 +386,11 @@ class ImageItem(GraphicsObject):
if self.image is None: if self.image is None:
return 1,1 return 1,1
return br.width()/self.width(), br.height()/self.height() return br.width()/self.width(), br.height()/self.height()
def viewTransformChanged(self):
if self.autoDownsample:
self.qimage = None
self.update()
#def mousePressEvent(self, ev): #def mousePressEvent(self, ev):
#if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton: #if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton:

View File

@ -15,7 +15,7 @@ class InfiniteLine(GraphicsObject):
This line may be dragged to indicate a position in data coordinates. This line may be dragged to indicate a position in data coordinates.
=============================== =================================================== =============================== ===================================================
**Signals** **Signals:**
sigDragged(self) sigDragged(self)
sigPositionChangeFinished(self) sigPositionChangeFinished(self)
sigPositionChanged(self) sigPositionChanged(self)
@ -28,18 +28,18 @@ class InfiniteLine(GraphicsObject):
def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None): def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None):
""" """
============= ================================================================== =============== ==================================================================
**Arguments** **Arguments:**
pos Position of the line. This can be a QPointF or a single value for pos Position of the line. This can be a QPointF or a single value for
vertical/horizontal lines. vertical/horizontal lines.
angle Angle of line in degrees. 0 is horizontal, 90 is vertical. angle Angle of line in degrees. 0 is horizontal, 90 is vertical.
pen Pen to use when drawing line. Can be any arguments that are valid pen Pen to use when drawing line. Can be any arguments that are valid
for :func:`mkPen <pyqtgraph.mkPen>`. Default pen is transparent for :func:`mkPen <pyqtgraph.mkPen>`. Default pen is transparent
yellow. yellow.
movable If True, the line can be dragged to a new position by the user. movable If True, the line can be dragged to a new position by the user.
bounds Optional [min, max] bounding values. Bounds are only valid if the bounds Optional [min, max] bounding values. Bounds are only valid if the
line is vertical or horizontal. line is vertical or horizontal.
============= ================================================================== =============== ==================================================================
""" """
GraphicsObject.__init__(self) GraphicsObject.__init__(self)
@ -73,10 +73,10 @@ class InfiniteLine(GraphicsObject):
self.maxRange = bounds self.maxRange = bounds
self.setValue(self.value()) self.setValue(self.value())
def setPen(self, pen): def setPen(self, *args, **kwargs):
"""Set the pen for drawing the line. Allowable arguments are any that are valid """Set the pen for drawing the line. Allowable arguments are any that are valid
for :func:`mkPen <pyqtgraph.mkPen>`.""" for :func:`mkPen <pyqtgraph.mkPen>`."""
self.pen = fn.mkPen(pen) self.pen = fn.mkPen(*args, **kwargs)
self.currentPen = self.pen self.currentPen = self.pen
self.update() self.update()

View File

@ -18,14 +18,14 @@ class IsocurveItem(GraphicsObject):
""" """
Create a new isocurve item. Create a new isocurve item.
============= =============================================================== ============== ===============================================================
**Arguments** **Arguments:**
data A 2-dimensional ndarray. Can be initialized as None, and set data A 2-dimensional ndarray. Can be initialized as None, and set
later using :func:`setData <pyqtgraph.IsocurveItem.setData>` later using :func:`setData <pyqtgraph.IsocurveItem.setData>`
level The cutoff value at which to draw the isocurve. level The cutoff value at which to draw the isocurve.
pen The color of the curve item. Can be anything valid for pen The color of the curve item. Can be anything valid for
:func:`mkPen <pyqtgraph.mkPen>` :func:`mkPen <pyqtgraph.mkPen>`
============= =============================================================== ============== ===============================================================
""" """
GraphicsObject.__init__(self) GraphicsObject.__init__(self)
@ -45,12 +45,12 @@ class IsocurveItem(GraphicsObject):
""" """
Set the data/image to draw isocurves for. Set the data/image to draw isocurves for.
============= ======================================================================== ============== ========================================================================
**Arguments** **Arguments:**
data A 2-dimensional ndarray. data A 2-dimensional ndarray.
level The cutoff value at which to draw the curve. If level is not specified, level The cutoff value at which to draw the curve. If level is not specified,
the previously set level is used. the previously set level is used.
============= ======================================================================== ============== ========================================================================
""" """
if level is None: if level is None:
level = self.level level = self.level

View File

@ -21,17 +21,17 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
""" """
def __init__(self, size=None, offset=None): def __init__(self, size=None, offset=None):
""" """
========== =============================================================== ============== ===============================================================
Arguments **Arguments:**
size Specifies the fixed size (width, height) of the legend. If size Specifies the fixed size (width, height) of the legend. If
this argument is omitted, the legend will autimatically resize this argument is omitted, the legend will autimatically resize
to fit its contents. to fit its contents.
offset Specifies the offset position relative to the legend's parent. offset Specifies the offset position relative to the legend's parent.
Positive values offset from the left or top; negative values Positive values offset from the left or top; negative values
offset from the right or bottom. If offset is None, the offset from the right or bottom. If offset is None, the
legend must be anchored manually by calling anchor() or legend must be anchored manually by calling anchor() or
positioned by calling setPos(). positioned by calling setPos().
========== =============================================================== ============== ===============================================================
""" """
@ -61,14 +61,14 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
""" """
Add a new entry to the legend. Add a new entry to the legend.
=========== ======================================================== ============== ========================================================
Arguments **Arguments:**
item A PlotDataItem from which the line and point style item A PlotDataItem from which the line and point style
of the item will be determined or an instance of of the item will be determined or an instance of
ItemSample (or a subclass), allowing the item display ItemSample (or a subclass), allowing the item display
to be customized. to be customized.
title The title to display for this item. Simple HTML allowed. title The title to display for this item. Simple HTML allowed.
=========== ======================================================== ============== ========================================================
""" """
label = LabelItem(name) label = LabelItem(name)
if isinstance(item, ItemSample): if isinstance(item, ItemSample):
@ -85,10 +85,10 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
""" """
Removes one item from the legend. Removes one item from the legend.
=========== ======================================================== ============== ========================================================
Arguments **Arguments:**
title The title displayed for this item. title The title displayed for this item.
=========== ======================================================== ============== ========================================================
""" """
# Thanks, Ulrich! # Thanks, Ulrich!
# cycle for a match # cycle for a match

View File

@ -30,19 +30,19 @@ class LinearRegionItem(UIGraphicsItem):
def __init__(self, values=[0,1], orientation=None, brush=None, movable=True, bounds=None): def __init__(self, values=[0,1], orientation=None, brush=None, movable=True, bounds=None):
"""Create a new LinearRegionItem. """Create a new LinearRegionItem.
============= ===================================================================== ============== =====================================================================
**Arguments** **Arguments:**
values A list of the positions of the lines in the region. These are not values A list of the positions of the lines in the region. These are not
limits; limits can be set by specifying bounds. limits; limits can be set by specifying bounds.
orientation Options are LinearRegionItem.Vertical or LinearRegionItem.Horizontal. orientation Options are LinearRegionItem.Vertical or LinearRegionItem.Horizontal.
If not specified it will be vertical. If not specified it will be vertical.
brush Defines the brush that fills the region. Can be any arguments that brush Defines the brush that fills the region. Can be any arguments that
are valid for :func:`mkBrush <pyqtgraph.mkBrush>`. Default is are valid for :func:`mkBrush <pyqtgraph.mkBrush>`. Default is
transparent blue. transparent blue.
movable If True, the region and individual lines are movable by the user; if movable If True, the region and individual lines are movable by the user; if
False, they are static. False, they are static.
bounds Optional [min, max] bounding values for the region bounds Optional [min, max] bounding values for the region
============= ===================================================================== ============== =====================================================================
""" """
UIGraphicsItem.__init__(self) UIGraphicsItem.__init__(self)
@ -89,10 +89,10 @@ class LinearRegionItem(UIGraphicsItem):
def setRegion(self, rgn): def setRegion(self, rgn):
"""Set the values for the edges of the region. """Set the values for the edges of the region.
============= ============================================== ============== ==============================================
**Arguments** **Arguments:**
rgn A list or tuple of the lower and upper values. rgn A list or tuple of the lower and upper values.
============= ============================================== ============== ==============================================
""" """
if self.lines[0].value() == rgn[0] and self.lines[1].value() == rgn[1]: if self.lines[0].value() == rgn[0] and self.lines[1].value() == rgn[1]:
return return

View File

@ -7,26 +7,23 @@ Distributed under MIT/X11 license. See license.txt for more infomation.
from numpy import ndarray from numpy import ndarray
from . import GraphicsLayout from . import GraphicsLayout
from ..metaarray import *
try:
from metaarray import *
HAVE_METAARRAY = True
except:
#raise
HAVE_METAARRAY = False
__all__ = ['MultiPlotItem'] __all__ = ['MultiPlotItem']
class MultiPlotItem(GraphicsLayout.GraphicsLayout): class MultiPlotItem(GraphicsLayout.GraphicsLayout):
""" """
Automaticaly generates a grid of plots from a multi-dimensional array Automatically generates a grid of plots from a multi-dimensional array
""" """
def __init__(self, *args, **kwds):
GraphicsLayout.GraphicsLayout.__init__(self, *args, **kwds)
self.plots = []
def plot(self, data): def plot(self, data):
#self.layout.clear() #self.layout.clear()
self.plots = []
if hasattr(data, 'implements') and data.implements('MetaArray'):
if HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')):
if data.ndim != 2: if data.ndim != 2:
raise Exception("MultiPlot currently only accepts 2D MetaArray.") raise Exception("MultiPlot currently only accepts 2D MetaArray.")
ic = data.infoCopy() ic = data.infoCopy()
@ -44,21 +41,17 @@ class MultiPlotItem(GraphicsLayout.GraphicsLayout):
pi.plot(data[tuple(sl)]) pi.plot(data[tuple(sl)])
#self.layout.addItem(pi, i, 0) #self.layout.addItem(pi, i, 0)
self.plots.append((pi, i, 0)) self.plots.append((pi, i, 0))
title = None
units = None
info = ic[ax]['cols'][i] info = ic[ax]['cols'][i]
if 'title' in info: title = info.get('title', info.get('name', None))
title = info['title'] units = info.get('units', None)
elif 'name' in info:
title = info['name']
if 'units' in info:
units = info['units']
pi.setLabel('left', text=title, units=units) pi.setLabel('left', text=title, units=units)
info = ic[1-ax]
title = info.get('title', info.get('name', None))
units = info.get('units', None)
pi.setLabel('bottom', text=title, units=units)
else: else:
raise Exception("Data type %s not (yet?) supported for MultiPlot." % type(data)) raise Exception("Data type %s not (yet?) supported for MultiPlot." % type(data))
def close(self): def close(self):
for p in self.plots: for p in self.plots:
p[0].close() p[0].close()

View File

@ -173,8 +173,14 @@ class PlotCurveItem(GraphicsObject):
if pxPad > 0: if pxPad > 0:
# determine length of pixel in local x, y directions # determine length of pixel in local x, y directions
px, py = self.pixelVectors() px, py = self.pixelVectors()
px = 0 if px is None else px.length() try:
py = 0 if py is None else py.length() px = 0 if px is None else px.length()
except OverflowError:
px = 0
try:
py = 0 if py is None else py.length()
except OverflowError:
py = 0
# return bounds expanded by pixel size # return bounds expanded by pixel size
px *= pxPad px *= pxPad
@ -486,7 +492,7 @@ class PlotCurveItem(GraphicsObject):
gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP) gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP)
## draw stencil pattern ## draw stencil pattern
gl.glStencilMask(0xFF); gl.glStencilMask(0xFF)
gl.glClear(gl.GL_STENCIL_BUFFER_BIT) gl.glClear(gl.GL_STENCIL_BUFFER_BIT)
gl.glBegin(gl.GL_TRIANGLES) gl.glBegin(gl.GL_TRIANGLES)
gl.glVertex2f(rect.x(), rect.y()) gl.glVertex2f(rect.x(), rect.y())
@ -520,7 +526,7 @@ class PlotCurveItem(GraphicsObject):
gl.glEnable(gl.GL_LINE_SMOOTH) gl.glEnable(gl.GL_LINE_SMOOTH)
gl.glEnable(gl.GL_BLEND) gl.glEnable(gl.GL_BLEND)
gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST); gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)
gl.glDrawArrays(gl.GL_LINE_STRIP, 0, pos.size / pos.shape[-1]) gl.glDrawArrays(gl.GL_LINE_STRIP, 0, pos.size / pos.shape[-1])
finally: finally:
gl.glDisableClientState(gl.GL_VERTEX_ARRAY) gl.glDisableClientState(gl.GL_VERTEX_ARRAY)

View File

@ -56,10 +56,11 @@ class PlotDataItem(GraphicsObject):
=========================== ========================================= =========================== =========================================
**Line style keyword arguments:** **Line style keyword arguments:**
========== ================================================
connect Specifies how / whether vertexes should be connected. ========== ==============================================================================
See :func:`arrayToQPath() <pyqtgraph.arrayToQPath>` connect Specifies how / whether vertexes should be connected. See
pen Pen to use for drawing line between points. :func:`arrayToQPath() <pyqtgraph.arrayToQPath>`
pen Pen to use for drawing line between points.
Default is solid grey, 1px width. Use None to disable line drawing. Default is solid grey, 1px width. Use None to disable line drawing.
May be any single argument accepted by :func:`mkPen() <pyqtgraph.mkPen>` May be any single argument accepted by :func:`mkPen() <pyqtgraph.mkPen>`
shadowPen Pen for secondary line to draw behind the primary line. disabled by default. shadowPen Pen for secondary line to draw behind the primary line. disabled by default.
@ -67,21 +68,29 @@ class PlotDataItem(GraphicsObject):
fillLevel Fill the area between the curve and fillLevel fillLevel Fill the area between the curve and fillLevel
fillBrush Fill to use when fillLevel is specified. fillBrush Fill to use when fillLevel is specified.
May be any single argument accepted by :func:`mkBrush() <pyqtgraph.mkBrush>` May be any single argument accepted by :func:`mkBrush() <pyqtgraph.mkBrush>`
========== ================================================ stepMode If True, two orthogonal lines are drawn for each sample
as steps. This is commonly used when drawing histograms.
Note that in this case, `len(x) == len(y) + 1`
(added in version 0.9.9)
========== ==============================================================================
**Point style keyword arguments:** (see :func:`ScatterPlotItem.setData() <pyqtgraph.ScatterPlotItem.setData>` for more information) **Point style keyword arguments:** (see :func:`ScatterPlotItem.setData() <pyqtgraph.ScatterPlotItem.setData>` for more information)
============ ================================================ ============ =====================================================
symbol Symbol to use for drawing points OR list of symbols, one per point. Default is no symbol. symbol Symbol to use for drawing points OR list of symbols,
one per point. Default is no symbol.
Options are o, s, t, d, +, or any QPainterPath Options are o, s, t, d, +, or any QPainterPath
symbolPen Outline pen for drawing points OR list of pens, one per point. symbolPen Outline pen for drawing points OR list of pens, one
May be any single argument accepted by :func:`mkPen() <pyqtgraph.mkPen>` per point. May be any single argument accepted by
symbolBrush Brush for filling points OR list of brushes, one per point. :func:`mkPen() <pyqtgraph.mkPen>`
May be any single argument accepted by :func:`mkBrush() <pyqtgraph.mkBrush>` symbolBrush Brush for filling points OR list of brushes, one per
point. May be any single argument accepted by
:func:`mkBrush() <pyqtgraph.mkBrush>`
symbolSize Diameter of symbols OR list of diameters. symbolSize Diameter of symbols OR list of diameters.
pxMode (bool) If True, then symbolSize is specified in pixels. If False, then symbolSize is pxMode (bool) If True, then symbolSize is specified in
pixels. If False, then symbolSize is
specified in data coordinates. specified in data coordinates.
============ ================================================ ============ =====================================================
**Optimization keyword arguments:** **Optimization keyword arguments:**
@ -92,11 +101,11 @@ class PlotDataItem(GraphicsObject):
decimate deprecated. decimate deprecated.
downsample (int) Reduce the number of samples displayed by this value downsample (int) Reduce the number of samples displayed by this value
downsampleMethod 'subsample': Downsample by taking the first of N samples. downsampleMethod 'subsample': Downsample by taking the first of N samples.
This method is fastest and least accurate. This method is fastest and least accurate.
'mean': Downsample by taking the mean of N samples. 'mean': Downsample by taking the mean of N samples.
'peak': Downsample by drawing a saw wave that follows the min 'peak': Downsample by drawing a saw wave that follows the min
and max of the original data. This method produces the best and max of the original data. This method produces the best
visual representation of the data but is slower. visual representation of the data but is slower.
autoDownsample (bool) If True, resample the data before plotting to avoid plotting autoDownsample (bool) If True, resample the data before plotting to avoid plotting
multiple line segments per pixel. This can improve performance when multiple line segments per pixel. This can improve performance when
viewing very high-density data, but increases the initial overhead viewing very high-density data, but increases the initial overhead
@ -145,6 +154,7 @@ class PlotDataItem(GraphicsObject):
'shadowPen': None, 'shadowPen': None,
'fillLevel': None, 'fillLevel': None,
'fillBrush': None, 'fillBrush': None,
'stepMode': None,
'symbol': None, 'symbol': None,
'symbolSize': 10, 'symbolSize': 10,
@ -290,18 +300,18 @@ class PlotDataItem(GraphicsObject):
Set the downsampling mode of this item. Downsampling reduces the number Set the downsampling mode of this item. Downsampling reduces the number
of samples drawn to increase performance. of samples drawn to increase performance.
=========== ================================================================= ============== =================================================================
Arguments **Arguments:**
ds (int) Reduce visible plot samples by this factor. To disable, ds (int) Reduce visible plot samples by this factor. To disable,
set ds=1. set ds=1.
auto (bool) If True, automatically pick *ds* based on visible range auto (bool) If True, automatically pick *ds* based on visible range
mode 'subsample': Downsample by taking the first of N samples. mode 'subsample': Downsample by taking the first of N samples.
This method is fastest and least accurate. This method is fastest and least accurate.
'mean': Downsample by taking the mean of N samples. 'mean': Downsample by taking the mean of N samples.
'peak': Downsample by drawing a saw wave that follows the min 'peak': Downsample by drawing a saw wave that follows the min
and max of the original data. This method produces the best and max of the original data. This method produces the best
visual representation of the data but is slower. visual representation of the data but is slower.
=========== ================================================================= ============== =================================================================
""" """
changed = False changed = False
if ds is not None: if ds is not None:
@ -451,7 +461,7 @@ class PlotDataItem(GraphicsObject):
def updateItems(self): def updateItems(self):
curveArgs = {} curveArgs = {}
for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillBrush', 'brush'), ('antialias', 'antialias'), ('connect', 'connect')]: for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillBrush', 'brush'), ('antialias', 'antialias'), ('connect', 'connect'), ('stepMode', 'stepMode')]:
curveArgs[v] = self.opts[k] curveArgs[v] = self.opts[k]
scatterArgs = {} scatterArgs = {}
@ -527,7 +537,8 @@ class PlotDataItem(GraphicsObject):
x0 = (range.left()-x[0]) / dx x0 = (range.left()-x[0]) / dx
x1 = (range.right()-x[0]) / dx x1 = (range.right()-x[0]) / dx
width = self.getViewBox().width() width = self.getViewBox().width()
ds = int(max(1, int(0.2 * (x1-x0) / width))) if width != 0.0:
ds = int(max(1, int(0.2 * (x1-x0) / width)))
## downsampling is expensive; delay until after clipping. ## downsampling is expensive; delay until after clipping.
if self.opts['clipToView']: if self.opts['clipToView']:
@ -646,13 +657,12 @@ class PlotDataItem(GraphicsObject):
def _fourierTransform(self, x, y): def _fourierTransform(self, x, y):
## Perform fourier transform. If x values are not sampled uniformly, ## Perform fourier transform. If x values are not sampled uniformly,
## then use interpolate.griddata to resample before taking fft. ## then use np.interp to resample before taking fft.
dx = np.diff(x) dx = np.diff(x)
uniform = not np.any(np.abs(dx-dx[0]) > (abs(dx[0]) / 1000.)) uniform = not np.any(np.abs(dx-dx[0]) > (abs(dx[0]) / 1000.))
if not uniform: if not uniform:
import scipy.interpolate as interp
x2 = np.linspace(x[0], x[-1], len(x)) x2 = np.linspace(x[0], x[-1], len(x))
y = interp.griddata(x, y, x2, method='linear') y = np.interp(x2, x, y)
x = x2 x = x2
f = np.fft.fft(y) / len(y) f = np.fft.fft(y) / len(y)
y = abs(f[1:len(f)/2]) y = abs(f[1:len(f)/2])

View File

@ -18,6 +18,7 @@ This class is very heavily featured:
""" """
from ...Qt import QtGui, QtCore, QtSvg, USE_PYSIDE from ...Qt import QtGui, QtCore, QtSvg, USE_PYSIDE
from ... import pixmaps from ... import pixmaps
import sys
if USE_PYSIDE: if USE_PYSIDE:
from .plotConfigTemplate_pyside import * from .plotConfigTemplate_pyside import *
@ -69,6 +70,7 @@ class PlotItem(GraphicsWidget):
:func:`setYLink <pyqtgraph.ViewBox.setYLink>`, :func:`setYLink <pyqtgraph.ViewBox.setYLink>`,
:func:`setAutoPan <pyqtgraph.ViewBox.setAutoPan>`, :func:`setAutoPan <pyqtgraph.ViewBox.setAutoPan>`,
:func:`setAutoVisible <pyqtgraph.ViewBox.setAutoVisible>`, :func:`setAutoVisible <pyqtgraph.ViewBox.setAutoVisible>`,
:func:`setLimits <pyqtgraph.ViewBox.setLimits>`,
:func:`viewRect <pyqtgraph.ViewBox.viewRect>`, :func:`viewRect <pyqtgraph.ViewBox.viewRect>`,
:func:`viewRange <pyqtgraph.ViewBox.viewRange>`, :func:`viewRange <pyqtgraph.ViewBox.viewRange>`,
:func:`setMouseEnabled <pyqtgraph.ViewBox.setMouseEnabled>`, :func:`setMouseEnabled <pyqtgraph.ViewBox.setMouseEnabled>`,
@ -82,7 +84,7 @@ class PlotItem(GraphicsWidget):
The ViewBox itself can be accessed by calling :func:`getViewBox() <pyqtgraph.PlotItem.getViewBox>` The ViewBox itself can be accessed by calling :func:`getViewBox() <pyqtgraph.PlotItem.getViewBox>`
==================== ======================================================================= ==================== =======================================================================
**Signals** **Signals:**
sigYRangeChanged wrapped from :class:`ViewBox <pyqtgraph.ViewBox>` sigYRangeChanged wrapped from :class:`ViewBox <pyqtgraph.ViewBox>`
sigXRangeChanged wrapped from :class:`ViewBox <pyqtgraph.ViewBox>` sigXRangeChanged wrapped from :class:`ViewBox <pyqtgraph.ViewBox>`
sigRangeChanged wrapped from :class:`ViewBox <pyqtgraph.ViewBox>` sigRangeChanged wrapped from :class:`ViewBox <pyqtgraph.ViewBox>`
@ -102,7 +104,7 @@ class PlotItem(GraphicsWidget):
Any extra keyword arguments are passed to PlotItem.plot(). Any extra keyword arguments are passed to PlotItem.plot().
============== ========================================================================================== ============== ==========================================================================================
**Arguments** **Arguments:**
*title* Title to display at the top of the item. Html is allowed. *title* Title to display at the top of the item. Html is allowed.
*labels* A dictionary specifying the axis labels to display:: *labels* A dictionary specifying the axis labels to display::
@ -192,14 +194,6 @@ class PlotItem(GraphicsWidget):
self.layout.setColumnStretchFactor(1, 100) self.layout.setColumnStretchFactor(1, 100)
## Wrap a few methods from viewBox
for m in [
'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible',
'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled',
'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'invertY',
'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well.
setattr(self, m, getattr(self.vb, m))
self.items = [] self.items = []
self.curves = [] self.curves = []
self.itemMeta = weakref.WeakKeyDictionary() self.itemMeta = weakref.WeakKeyDictionary()
@ -296,7 +290,26 @@ class PlotItem(GraphicsWidget):
def getViewBox(self): def getViewBox(self):
"""Return the :class:`ViewBox <pyqtgraph.ViewBox>` contained within.""" """Return the :class:`ViewBox <pyqtgraph.ViewBox>` contained within."""
return self.vb return self.vb
## Wrap a few methods from viewBox.
#Important: don't use a settattr(m, getattr(self.vb, m)) as we'd be leaving the viebox alive
#because we had a reference to an instance method (creating wrapper methods at runtime instead).
for m in ['setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', # NOTE:
'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', # If you update this list, please
'setMouseEnabled', 'setLimits', 'enableAutoRange', 'disableAutoRange', # update the class docstring
'setAspectLocked', 'invertY', 'register', 'unregister']: # as well.
def _create_method(name):
def method(self, *args, **kwargs):
return getattr(self.vb, name)(*args, **kwargs)
method.__name__ = name
return method
locals()[m] = _create_method(m)
del _create_method
def setLogMode(self, x=None, y=None): def setLogMode(self, x=None, y=None):
@ -355,10 +368,8 @@ class PlotItem(GraphicsWidget):
self.ctrlMenu.setParent(None) self.ctrlMenu.setParent(None)
self.ctrlMenu = None self.ctrlMenu = None
#self.ctrlBtn.setParent(None) self.autoBtn.setParent(None)
#self.ctrlBtn = None self.autoBtn = None
#self.autoBtn.setParent(None)
#self.autoBtn = None
for k in self.axes: for k in self.axes:
i = self.axes[k]['item'] i = self.axes[k]['item']
@ -930,18 +941,18 @@ class PlotItem(GraphicsWidget):
def setDownsampling(self, ds=None, auto=None, mode=None): def setDownsampling(self, ds=None, auto=None, mode=None):
"""Change the default downsampling mode for all PlotDataItems managed by this plot. """Change the default downsampling mode for all PlotDataItems managed by this plot.
=========== ================================================================= =============== =================================================================
Arguments **Arguments:**
ds (int) Reduce visible plot samples by this factor, or ds (int) Reduce visible plot samples by this factor, or
(bool) To enable/disable downsampling without changing the value. (bool) To enable/disable downsampling without changing the value.
auto (bool) If True, automatically pick *ds* based on visible range auto (bool) If True, automatically pick *ds* based on visible range
mode 'subsample': Downsample by taking the first of N samples. mode 'subsample': Downsample by taking the first of N samples.
This method is fastest and least accurate. This method is fastest and least accurate.
'mean': Downsample by taking the mean of N samples. 'mean': Downsample by taking the mean of N samples.
'peak': Downsample by drawing a saw wave that follows the min 'peak': Downsample by drawing a saw wave that follows the min
and max of the original data. This method produces the best and max of the original data. This method produces the best
visual representation of the data but is slower. visual representation of the data but is slower.
=========== ================================================================= =============== =================================================================
""" """
if ds is not None: if ds is not None:
if ds is False: if ds is False:
@ -1112,15 +1123,15 @@ class PlotItem(GraphicsWidget):
""" """
Set the label for an axis. Basic HTML formatting is allowed. Set the label for an axis. Basic HTML formatting is allowed.
============= ================================================================= ============== =================================================================
**Arguments** **Arguments:**
axis must be one of 'left', 'bottom', 'right', or 'top' axis must be one of 'left', 'bottom', 'right', or 'top'
text text to display along the axis. HTML allowed. text text to display along the axis. HTML allowed.
units units to display after the title. If units are given, units units to display after the title. If units are given,
then an SI prefix will be automatically appended then an SI prefix will be automatically appended
and the axis values will be scaled accordingly. and the axis values will be scaled accordingly.
(ie, use 'V' instead of 'mV'; 'm' will be added automatically) (ie, use 'V' instead of 'mV'; 'm' will be added automatically)
============= ================================================================= ============== =================================================================
""" """
self.getAxis(axis).setLabel(text=text, units=units, **args) self.getAxis(axis).setLabel(text=text, units=units, **args)
self.showAxis(axis) self.showAxis(axis)

View File

@ -13,11 +13,8 @@ of how to build an ROI at the bottom of the file.
""" """
from ..Qt import QtCore, QtGui from ..Qt import QtCore, QtGui
#if not hasattr(QtCore, 'Signal'):
#QtCore.Signal = QtCore.pyqtSignal
import numpy as np import numpy as np
from numpy.linalg import norm #from numpy.linalg import norm
import scipy.ndimage as ndimage
from ..Point import * from ..Point import *
from ..SRTTransform import SRTTransform from ..SRTTransform import SRTTransform
from math import cos, sin from math import cos, sin
@ -36,11 +33,56 @@ def rectStr(r):
return "[%f, %f] + [%f, %f]" % (r.x(), r.y(), r.width(), r.height()) return "[%f, %f] + [%f, %f]" % (r.x(), r.y(), r.width(), r.height())
class ROI(GraphicsObject): class ROI(GraphicsObject):
"""Generic region-of-interest widget. """
Can be used for implementing many types of selection box with rotate/translate/scale handles. Generic region-of-interest widget.
Signals Can be used for implementing many types of selection box with
----------------------- ---------------------------------------------------- rotate/translate/scale handles.
ROIs can be customized to have a variety of shapes (by subclassing or using
any of the built-in subclasses) and any combination of draggable handles
that allow the user to manipulate the ROI.
================ ===========================================================
**Arguments**
pos (length-2 sequence) Indicates the position of the ROI's
origin. For most ROIs, this is the lower-left corner of
its bounding rectangle.
size (length-2 sequence) Indicates the width and height of the
ROI.
angle (float) The rotation of the ROI in degrees. Default is 0.
invertible (bool) If True, the user may resize the ROI to have
negative width or height (assuming the ROI has scale
handles). Default is False.
maxBounds (QRect, QRectF, or None) Specifies boundaries that the ROI
cannot be dragged outside of by the user. Default is None.
snapSize (float) The spacing of snap positions used when *scaleSnap*
or *translateSnap* are enabled. Default is 1.0.
scaleSnap (bool) If True, the width and height of the ROI are forced
to be integer multiples of *snapSize* when being resized
by the user. Default is False.
translateSnap (bool) If True, the x and y positions of the ROI are forced
to be integer multiples of *snapSize* when being resized
by the user. Default is False.
rotateSnap (bool) If True, the ROI angle is forced to a multiple of
15 degrees when rotated by the user. Default is False.
parent (QGraphicsItem) The graphics item parent of this ROI. It
is generally not necessary to specify the parent.
pen (QPen or argument to pg.mkPen) The pen to use when drawing
the shape of the ROI.
movable (bool) If True, the ROI can be moved by dragging anywhere
inside the ROI. Default is True.
removable (bool) If True, the ROI will be given a context menu with
an option to remove the ROI. The ROI emits
sigRemoveRequested when this menu action is selected.
Default is False.
================ ===========================================================
======================= ====================================================
**Signals**
sigRegionChangeFinished Emitted when the user stops dragging the ROI (or sigRegionChangeFinished Emitted when the user stops dragging the ROI (or
one of its handles) or if the ROI is changed one of its handles) or if the ROI is changed
programatically. programatically.
@ -58,7 +100,7 @@ class ROI(GraphicsObject):
details. details.
sigRemoveRequested Emitted when the user selects 'remove' from the sigRemoveRequested Emitted when the user selects 'remove' from the
ROI's context menu (if available). ROI's context menu (if available).
----------------------- ---------------------------------------------------- ======================= ====================================================
""" """
sigRegionChangeFinished = QtCore.Signal(object) sigRegionChangeFinished = QtCore.Signal(object)
@ -117,7 +159,11 @@ class ROI(GraphicsObject):
return sc return sc
def saveState(self): def saveState(self):
"""Return the state of the widget in a format suitable for storing to disk. (Points are converted to tuple)""" """Return the state of the widget in a format suitable for storing to
disk. (Points are converted to tuple)
Combined with setState(), this allows ROIs to be easily saved and
restored."""
state = {} state = {}
state['pos'] = tuple(self.state['pos']) state['pos'] = tuple(self.state['pos'])
state['size'] = tuple(self.state['size']) state['size'] = tuple(self.state['size'])
@ -125,6 +171,10 @@ class ROI(GraphicsObject):
return state return state
def setState(self, state, update=True): def setState(self, state, update=True):
"""
Set the state of the ROI from a structure generated by saveState() or
getState().
"""
self.setPos(state['pos'], update=False) self.setPos(state['pos'], update=False)
self.setSize(state['size'], update=False) self.setSize(state['size'], update=False)
self.setAngle(state['angle'], update=update) self.setAngle(state['angle'], update=update)
@ -135,20 +185,32 @@ class ROI(GraphicsObject):
h['item'].setZValue(z+1) h['item'].setZValue(z+1)
def parentBounds(self): def parentBounds(self):
"""
Return the bounding rectangle of this ROI in the coordinate system
of its parent.
"""
return self.mapToParent(self.boundingRect()).boundingRect() return self.mapToParent(self.boundingRect()).boundingRect()
def setPen(self, pen): def setPen(self, *args, **kwargs):
self.pen = fn.mkPen(pen) """
Set the pen to use when drawing the ROI shape.
For arguments, see :func:`mkPen <pyqtgraph.mkPen>`.
"""
self.pen = fn.mkPen(*args, **kwargs)
self.currentPen = self.pen self.currentPen = self.pen
self.update() self.update()
def size(self): def size(self):
"""Return the size (w,h) of the ROI."""
return self.getState()['size'] return self.getState()['size']
def pos(self): def pos(self):
"""Return the position (x,y) of the ROI's origin.
For most ROIs, this will be the lower-left corner."""
return self.getState()['pos'] return self.getState()['pos']
def angle(self): def angle(self):
"""Return the angle of the ROI in degrees."""
return self.getState()['angle'] return self.getState()['angle']
def setPos(self, pos, update=True, finish=True): def setPos(self, pos, update=True, finish=True):
@ -214,11 +276,14 @@ class ROI(GraphicsObject):
If the ROI is bounded and the move would exceed boundaries, then the ROI If the ROI is bounded and the move would exceed boundaries, then the ROI
is moved to the nearest acceptable position instead. is moved to the nearest acceptable position instead.
snap can be: *snap* can be:
None (default): use self.translateSnap and self.snapSize to determine whether/how to snap
False: do not snap =============== ==========================================================================
Point(w,h) snap to rectangular grid with spacing (w,h) None (default) use self.translateSnap and self.snapSize to determine whether/how to snap
True: snap using self.snapSize (and ignoring self.translateSnap) False do not snap
Point(w,h) snap to rectangular grid with spacing (w,h)
True snap using self.snapSize (and ignoring self.translateSnap)
=============== ==========================================================================
Also accepts *update* and *finish* arguments (see setPos() for a description of these). Also accepts *update* and *finish* arguments (see setPos() for a description of these).
""" """
@ -264,21 +329,86 @@ class ROI(GraphicsObject):
#self.stateChanged() #self.stateChanged()
def rotate(self, angle, update=True, finish=True): def rotate(self, angle, update=True, finish=True):
"""
Rotate the ROI by *angle* degrees.
Also accepts *update* and *finish* arguments (see setPos() for a
description of these).
"""
self.setAngle(self.angle()+angle, update=update, finish=finish) self.setAngle(self.angle()+angle, update=update, finish=finish)
def handleMoveStarted(self): def handleMoveStarted(self):
self.preMoveState = self.getState() self.preMoveState = self.getState()
def addTranslateHandle(self, pos, axes=None, item=None, name=None, index=None): def addTranslateHandle(self, pos, axes=None, item=None, name=None, index=None):
"""
Add a new translation handle to the ROI. Dragging the handle will move
the entire ROI without changing its angle or shape.
Note that, by default, ROIs may be moved by dragging anywhere inside the
ROI. However, for larger ROIs it may be desirable to disable this and
instead provide one or more translation handles.
=================== ====================================================
**Arguments**
pos (length-2 sequence) The position of the handle
relative to the shape of the ROI. A value of (0,0)
indicates the origin, whereas (1, 1) indicates the
upper-right corner, regardless of the ROI's size.
item The Handle instance to add. If None, a new handle
will be created.
name The name of this handle (optional). Handles are
identified by name when calling
getLocalHandlePositions and getSceneHandlePositions.
=================== ====================================================
"""
pos = Point(pos) pos = Point(pos)
return self.addHandle({'name': name, 'type': 't', 'pos': pos, 'item': item}, index=index) return self.addHandle({'name': name, 'type': 't', 'pos': pos, 'item': item}, index=index)
def addFreeHandle(self, pos=None, axes=None, item=None, name=None, index=None): def addFreeHandle(self, pos=None, axes=None, item=None, name=None, index=None):
"""
Add a new free handle to the ROI. Dragging free handles has no effect
on the position or shape of the ROI.
=================== ====================================================
**Arguments**
pos (length-2 sequence) The position of the handle
relative to the shape of the ROI. A value of (0,0)
indicates the origin, whereas (1, 1) indicates the
upper-right corner, regardless of the ROI's size.
item The Handle instance to add. If None, a new handle
will be created.
name The name of this handle (optional). Handles are
identified by name when calling
getLocalHandlePositions and getSceneHandlePositions.
=================== ====================================================
"""
if pos is not None: if pos is not None:
pos = Point(pos) pos = Point(pos)
return self.addHandle({'name': name, 'type': 'f', 'pos': pos, 'item': item}, index=index) return self.addHandle({'name': name, 'type': 'f', 'pos': pos, 'item': item}, index=index)
def addScaleHandle(self, pos, center, axes=None, item=None, name=None, lockAspect=False, index=None): def addScaleHandle(self, pos, center, axes=None, item=None, name=None, lockAspect=False, index=None):
"""
Add a new scale handle to the ROI. Dragging a scale handle allows the
user to change the height and/or width of the ROI.
=================== ====================================================
**Arguments**
pos (length-2 sequence) The position of the handle
relative to the shape of the ROI. A value of (0,0)
indicates the origin, whereas (1, 1) indicates the
upper-right corner, regardless of the ROI's size.
center (length-2 sequence) The center point around which
scaling takes place. If the center point has the
same x or y value as the handle position, then
scaling will be disabled for that axis.
item The Handle instance to add. If None, a new handle
will be created.
name The name of this handle (optional). Handles are
identified by name when calling
getLocalHandlePositions and getSceneHandlePositions.
=================== ====================================================
"""
pos = Point(pos) pos = Point(pos)
center = Point(center) center = Point(center)
info = {'name': name, 'type': 's', 'center': center, 'pos': pos, 'item': item, 'lockAspect': lockAspect} info = {'name': name, 'type': 's', 'center': center, 'pos': pos, 'item': item, 'lockAspect': lockAspect}
@ -289,11 +419,51 @@ class ROI(GraphicsObject):
return self.addHandle(info, index=index) return self.addHandle(info, index=index)
def addRotateHandle(self, pos, center, item=None, name=None, index=None): def addRotateHandle(self, pos, center, item=None, name=None, index=None):
"""
Add a new rotation handle to the ROI. Dragging a rotation handle allows
the user to change the angle of the ROI.
=================== ====================================================
**Arguments**
pos (length-2 sequence) The position of the handle
relative to the shape of the ROI. A value of (0,0)
indicates the origin, whereas (1, 1) indicates the
upper-right corner, regardless of the ROI's size.
center (length-2 sequence) The center point around which
rotation takes place.
item The Handle instance to add. If None, a new handle
will be created.
name The name of this handle (optional). Handles are
identified by name when calling
getLocalHandlePositions and getSceneHandlePositions.
=================== ====================================================
"""
pos = Point(pos) pos = Point(pos)
center = Point(center) center = Point(center)
return self.addHandle({'name': name, 'type': 'r', 'center': center, 'pos': pos, 'item': item}, index=index) return self.addHandle({'name': name, 'type': 'r', 'center': center, 'pos': pos, 'item': item}, index=index)
def addScaleRotateHandle(self, pos, center, item=None, name=None, index=None): def addScaleRotateHandle(self, pos, center, item=None, name=None, index=None):
"""
Add a new scale+rotation handle to the ROI. When dragging a handle of
this type, the user can simultaneously rotate the ROI around an
arbitrary center point as well as scale the ROI by dragging the handle
toward or away from the center point.
=================== ====================================================
**Arguments**
pos (length-2 sequence) The position of the handle
relative to the shape of the ROI. A value of (0,0)
indicates the origin, whereas (1, 1) indicates the
upper-right corner, regardless of the ROI's size.
center (length-2 sequence) The center point around which
scaling and rotation take place.
item The Handle instance to add. If None, a new handle
will be created.
name The name of this handle (optional). Handles are
identified by name when calling
getLocalHandlePositions and getSceneHandlePositions.
=================== ====================================================
"""
pos = Point(pos) pos = Point(pos)
center = Point(center) center = Point(center)
if pos[0] != center[0] and pos[1] != center[1]: if pos[0] != center[0] and pos[1] != center[1]:
@ -301,6 +471,27 @@ class ROI(GraphicsObject):
return self.addHandle({'name': name, 'type': 'sr', 'center': center, 'pos': pos, 'item': item}, index=index) return self.addHandle({'name': name, 'type': 'sr', 'center': center, 'pos': pos, 'item': item}, index=index)
def addRotateFreeHandle(self, pos, center, axes=None, item=None, name=None, index=None): def addRotateFreeHandle(self, pos, center, axes=None, item=None, name=None, index=None):
"""
Add a new rotation+free handle to the ROI. When dragging a handle of
this type, the user can rotate the ROI around an
arbitrary center point, while moving toward or away from the center
point has no effect on the shape of the ROI.
=================== ====================================================
**Arguments**
pos (length-2 sequence) The position of the handle
relative to the shape of the ROI. A value of (0,0)
indicates the origin, whereas (1, 1) indicates the
upper-right corner, regardless of the ROI's size.
center (length-2 sequence) The center point around which
rotation takes place.
item The Handle instance to add. If None, a new handle
will be created.
name The name of this handle (optional). Handles are
identified by name when calling
getLocalHandlePositions and getSceneHandlePositions.
=================== ====================================================
"""
pos = Point(pos) pos = Point(pos)
center = Point(center) center = Point(center)
return self.addHandle({'name': name, 'type': 'rf', 'center': center, 'pos': pos, 'item': item}, index=index) return self.addHandle({'name': name, 'type': 'rf', 'center': center, 'pos': pos, 'item': item}, index=index)
@ -329,6 +520,9 @@ class ROI(GraphicsObject):
return h return h
def indexOfHandle(self, handle): def indexOfHandle(self, handle):
"""
Return the index of *handle* in the list of this ROI's handles.
"""
if isinstance(handle, Handle): if isinstance(handle, Handle):
index = [i for i, info in enumerate(self.handles) if info['item'] is handle] index = [i for i, info in enumerate(self.handles) if info['item'] is handle]
if len(index) == 0: if len(index) == 0:
@ -338,7 +532,8 @@ class ROI(GraphicsObject):
return handle return handle
def removeHandle(self, handle): def removeHandle(self, handle):
"""Remove a handle from this ROI. Argument may be either a Handle instance or the integer index of the handle.""" """Remove a handle from this ROI. Argument may be either a Handle
instance or the integer index of the handle."""
index = self.indexOfHandle(handle) index = self.indexOfHandle(handle)
handle = self.handles[index]['item'] handle = self.handles[index]['item']
@ -349,20 +544,17 @@ class ROI(GraphicsObject):
self.stateChanged() self.stateChanged()
def replaceHandle(self, oldHandle, newHandle): def replaceHandle(self, oldHandle, newHandle):
"""Replace one handle in the ROI for another. This is useful when connecting multiple ROIs together. """Replace one handle in the ROI for another. This is useful when
*oldHandle* may be a Handle instance or the index of a handle.""" connecting multiple ROIs together.
#print "========================="
#print "replace", oldHandle, newHandle *oldHandle* may be a Handle instance or the index of a handle to be
#print self replaced."""
#print self.handles
#print "-----------------"
index = self.indexOfHandle(oldHandle) index = self.indexOfHandle(oldHandle)
info = self.handles[index] info = self.handles[index]
self.removeHandle(index) self.removeHandle(index)
info['item'] = newHandle info['item'] = newHandle
info['pos'] = newHandle.pos() info['pos'] = newHandle.pos()
self.addHandle(info, index=index) self.addHandle(info, index=index)
#print self.handles
def checkRemoveHandle(self, handle): def checkRemoveHandle(self, handle):
## This is used when displaying a Handle's context menu to determine ## This is used when displaying a Handle's context menu to determine
@ -373,7 +565,10 @@ class ROI(GraphicsObject):
def getLocalHandlePositions(self, index=None): def getLocalHandlePositions(self, index=None):
"""Returns the position of a handle in ROI coordinates""" """Returns the position of handles in the ROI's coordinate system.
The format returned is a list of (name, pos) tuples.
"""
if index == None: if index == None:
positions = [] positions = []
for h in self.handles: for h in self.handles:
@ -383,6 +578,10 @@ class ROI(GraphicsObject):
return (self.handles[index]['name'], self.handles[index]['pos']) return (self.handles[index]['name'], self.handles[index]['pos'])
def getSceneHandlePositions(self, index=None): def getSceneHandlePositions(self, index=None):
"""Returns the position of handles in the scene coordinate system.
The format returned is a list of (name, pos) tuples.
"""
if index == None: if index == None:
positions = [] positions = []
for h in self.handles: for h in self.handles:
@ -392,6 +591,9 @@ class ROI(GraphicsObject):
return (self.handles[index]['name'], self.handles[index]['item'].scenePos()) return (self.handles[index]['name'], self.handles[index]['item'].scenePos())
def getHandles(self): def getHandles(self):
"""
Return a list of this ROI's Handles.
"""
return [h['item'] for h in self.handles] return [h['item'] for h in self.handles]
def mapSceneToParent(self, pt): def mapSceneToParent(self, pt):
@ -463,12 +665,8 @@ class ROI(GraphicsObject):
def removeClicked(self): def removeClicked(self):
## Send remove event only after we have exited the menu event handler ## Send remove event only after we have exited the menu event handler
self.removeTimer = QtCore.QTimer() QtCore.QTimer.singleShot(0, lambda: self.sigRemoveRequested.emit(self))
self.removeTimer.timeout.connect(lambda: self.sigRemoveRequested.emit(self))
self.removeTimer.start(0)
def mouseDragEvent(self, ev): def mouseDragEvent(self, ev):
if ev.isStart(): if ev.isStart():
#p = ev.pos() #p = ev.pos()
@ -510,56 +708,16 @@ class ROI(GraphicsObject):
self.sigClicked.emit(self, ev) self.sigClicked.emit(self, ev)
else: else:
ev.ignore() ev.ignore()
def cancelMove(self): def cancelMove(self):
self.isMoving = False self.isMoving = False
self.setState(self.preMoveState) self.setState(self.preMoveState)
#def pointDragEvent(self, pt, ev):
### just for handling drag start/stop.
### drag moves are handled through movePoint()
#if ev.isStart():
#self.isMoving = True
#self.preMoveState = self.getState()
#self.sigRegionChangeStarted.emit(self)
#elif ev.isFinish():
#self.isMoving = False
#self.sigRegionChangeFinished.emit(self)
#return
#def pointPressEvent(self, pt, ev):
##print "press"
#self.isMoving = True
#self.preMoveState = self.getState()
##self.emit(QtCore.SIGNAL('regionChangeStarted'), self)
#self.sigRegionChangeStarted.emit(self)
##self.pressPos = self.mapFromScene(ev.scenePos())
##self.pressHandlePos = self.handles[pt]['item'].pos()
#def pointReleaseEvent(self, pt, ev):
##print "release"
#self.isMoving = False
##self.emit(QtCore.SIGNAL('regionChangeFinished'), self)
#self.sigRegionChangeFinished.emit(self)
#def pointMoveEvent(self, pt, ev):
#self.movePoint(pt, ev.scenePos(), ev.modifiers())
def checkPointMove(self, handle, pos, modifiers): def checkPointMove(self, handle, pos, modifiers):
"""When handles move, they must ask the ROI if the move is acceptable. """When handles move, they must ask the ROI if the move is acceptable.
By default, this always returns True. Subclasses may wish override. By default, this always returns True. Subclasses may wish override.
""" """
return True return True
def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True, coords='parent'): def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True, coords='parent'):
## called by Handles when they are moved. ## called by Handles when they are moved.
@ -664,7 +822,10 @@ class ROI(GraphicsObject):
if not self.rotateAllowed: if not self.rotateAllowed:
return return
## If the handle is directly over its center point, we can't compute an angle. ## If the handle is directly over its center point, we can't compute an angle.
if lp1.length() == 0 or lp0.length() == 0: try:
if lp1.length() == 0 or lp0.length() == 0:
return
except OverflowError:
return return
## determine new rotation angle, constrained if necessary ## determine new rotation angle, constrained if necessary
@ -704,7 +865,10 @@ class ROI(GraphicsObject):
else: else:
scaleAxis = 0 scaleAxis = 0
if lp1.length() == 0 or lp0.length() == 0: try:
if lp1.length() == 0 or lp0.length() == 0:
return
except OverflowError:
return return
ang = newState['angle'] - lp0.angle(lp1) ang = newState['angle'] - lp0.angle(lp1)
@ -804,7 +968,6 @@ class ROI(GraphicsObject):
round(pos[1] / snap[1]) * snap[1] round(pos[1] / snap[1]) * snap[1]
) )
def boundingRect(self): def boundingRect(self):
return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized() return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized()
@ -871,7 +1034,25 @@ class ROI(GraphicsObject):
return bounds, tr return bounds, tr
def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds): def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds):
"""Use the position and orientation of this ROI relative to an imageItem to pull a slice from an array. """Use the position and orientation of this ROI relative to an imageItem
to pull a slice from an array.
=================== ====================================================
**Arguments**
data The array to slice from. Note that this array does
*not* have to be the same data that is represented
in *img*.
img (ImageItem or other suitable QGraphicsItem)
Used to determine the relationship between the
ROI and the boundaries of *data*.
axes (length-2 tuple) Specifies the axes in *data* that
correspond to the x and y axes of *img*.
returnMappedCoords (bool) If True, the array slice is returned along
with a corresponding array of coordinates that were
used to extract data from the original array.
\**kwds All keyword arguments are passed to
:func:`affineSlice <pyqtgraph.affineSlice>`.
=================== ====================================================
This method uses :func:`affineSlice <pyqtgraph.affineSlice>` to generate This method uses :func:`affineSlice <pyqtgraph.affineSlice>` to generate
the slice from *data* and uses :func:`getAffineSliceParams <pyqtgraph.ROI.getAffineSliceParams>` to determine the parameters to the slice from *data* and uses :func:`getAffineSliceParams <pyqtgraph.ROI.getAffineSliceParams>` to determine the parameters to
@ -905,105 +1086,6 @@ class ROI(GraphicsObject):
#mapped += translate.reshape((2,1,1)) #mapped += translate.reshape((2,1,1))
mapped = fn.transformCoordinates(img.transform(), coords) mapped = fn.transformCoordinates(img.transform(), coords)
return result, mapped return result, mapped
### transpose data so x and y are the first 2 axes
#trAx = range(0, data.ndim)
#trAx.remove(axes[0])
#trAx.remove(axes[1])
#tr1 = tuple(axes) + tuple(trAx)
#arr = data.transpose(tr1)
### Determine the minimal area of the data we will need
#(dataBounds, roiDataTransform) = self.getArraySlice(data, img, returnSlice=False, axes=axes)
### Pad data boundaries by 1px if possible
#dataBounds = (
#(max(dataBounds[0][0]-1, 0), min(dataBounds[0][1]+1, arr.shape[0])),
#(max(dataBounds[1][0]-1, 0), min(dataBounds[1][1]+1, arr.shape[1]))
#)
### Extract minimal data from array
#arr1 = arr[dataBounds[0][0]:dataBounds[0][1], dataBounds[1][0]:dataBounds[1][1]]
### Update roiDataTransform to reflect this extraction
#roiDataTransform *= QtGui.QTransform().translate(-dataBounds[0][0], -dataBounds[1][0])
#### (roiDataTransform now maps from ROI coords to extracted data coords)
### Rotate array
#if abs(self.state['angle']) > 1e-5:
#arr2 = ndimage.rotate(arr1, self.state['angle'] * 180 / np.pi, order=1)
### update data transforms to reflect this rotation
#rot = QtGui.QTransform().rotate(self.state['angle'] * 180 / np.pi)
#roiDataTransform *= rot
### The rotation also causes a shift which must be accounted for:
#dataBound = QtCore.QRectF(0, 0, arr1.shape[0], arr1.shape[1])
#rotBound = rot.mapRect(dataBound)
#roiDataTransform *= QtGui.QTransform().translate(-rotBound.left(), -rotBound.top())
#else:
#arr2 = arr1
#### Shift off partial pixels
## 1. map ROI into current data space
#roiBounds = roiDataTransform.mapRect(self.boundingRect())
## 2. Determine amount to shift data
#shift = (int(roiBounds.left()) - roiBounds.left(), int(roiBounds.bottom()) - roiBounds.bottom())
#if abs(shift[0]) > 1e-6 or abs(shift[1]) > 1e-6:
## 3. pad array with 0s before shifting
#arr2a = np.zeros((arr2.shape[0]+2, arr2.shape[1]+2) + arr2.shape[2:], dtype=arr2.dtype)
#arr2a[1:-1, 1:-1] = arr2
## 4. shift array and udpate transforms
#arr3 = ndimage.shift(arr2a, shift + (0,)*(arr2.ndim-2), order=1)
#roiDataTransform *= QtGui.QTransform().translate(1+shift[0], 1+shift[1])
#else:
#arr3 = arr2
#### Extract needed region from rotated/shifted array
## 1. map ROI into current data space (round these values off--they should be exact integer values at this point)
#roiBounds = roiDataTransform.mapRect(self.boundingRect())
##print self, roiBounds.height()
##import traceback
##traceback.print_stack()
#roiBounds = QtCore.QRect(round(roiBounds.left()), round(roiBounds.top()), round(roiBounds.width()), round(roiBounds.height()))
##2. intersect ROI with data bounds
#dataBounds = roiBounds.intersect(QtCore.QRect(0, 0, arr3.shape[0], arr3.shape[1]))
##3. Extract data from array
#db = dataBounds
#bounds = (
#(db.left(), db.right()+1),
#(db.top(), db.bottom()+1)
#)
#arr4 = arr3[bounds[0][0]:bounds[0][1], bounds[1][0]:bounds[1][1]]
#### Create zero array in size of ROI
#arr5 = np.zeros((roiBounds.width(), roiBounds.height()) + arr4.shape[2:], dtype=arr4.dtype)
### Fill array with ROI data
#orig = Point(dataBounds.topLeft() - roiBounds.topLeft())
#subArr = arr5[orig[0]:orig[0]+arr4.shape[0], orig[1]:orig[1]+arr4.shape[1]]
#subArr[:] = arr4[:subArr.shape[0], :subArr.shape[1]]
### figure out the reverse transpose order
#tr2 = np.array(tr1)
#for i in range(0, len(tr2)):
#tr2[tr1[i]] = i
#tr2 = tuple(tr2)
### Untranspose array before returning
#return arr5.transpose(tr2)
def getAffineSliceParams(self, data, img, axes=(0,1)): def getAffineSliceParams(self, data, img, axes=(0,1)):
""" """
@ -1088,7 +1170,18 @@ class ROI(GraphicsObject):
class Handle(UIGraphicsItem): class Handle(UIGraphicsItem):
"""
Handle represents a single user-interactable point attached to an ROI. They
are usually created by a call to one of the ROI.add___Handle() methods.
Handles are represented as a square, diamond, or circle, and are drawn with
fixed pixel size regardless of the scaling of the view they are displayed in.
Handles may be dragged to change the position, size, orientation, or other
properties of the ROI they are attached to.
"""
types = { ## defines number of sides, start angle for each handle type types = { ## defines number of sides, start angle for each handle type
't': (4, np.pi/4), 't': (4, np.pi/4),
'f': (4, np.pi/4), 'f': (4, np.pi/4),
@ -1360,6 +1453,22 @@ class TestROI(ROI):
class RectROI(ROI): class RectROI(ROI):
"""
Rectangular ROI subclass with a single scale handle at the top-right corner.
============== =============================================================
**Arguments**
pos (length-2 sequence) The position of the ROI origin.
See ROI().
size (length-2 sequence) The size of the ROI. See ROI().
centered (bool) If True, scale handles affect the ROI relative to its
center, rather than its origin.
sideScalers (bool) If True, extra scale handles are added at the top and
right edges.
\**args All extra keyword arguments are passed to ROI()
============== =============================================================
"""
def __init__(self, pos, size, centered=False, sideScalers=False, **args): def __init__(self, pos, size, centered=False, sideScalers=False, **args):
#QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1])
ROI.__init__(self, pos, size, **args) ROI.__init__(self, pos, size, **args)
@ -1375,6 +1484,22 @@ class RectROI(ROI):
self.addScaleHandle([0.5, 1], [0.5, center[1]]) self.addScaleHandle([0.5, 1], [0.5, center[1]])
class LineROI(ROI): class LineROI(ROI):
"""
Rectangular ROI subclass with scale-rotate handles on either side. This
allows the ROI to be positioned as if moving the ends of a line segment.
A third handle controls the width of the ROI orthogonal to its "line" axis.
============== =============================================================
**Arguments**
pos1 (length-2 sequence) The position of the center of the ROI's
left edge.
pos2 (length-2 sequence) The position of the center of the ROI's
right edge.
width (float) The width of the ROI.
\**args All extra keyword arguments are passed to ROI()
============== =============================================================
"""
def __init__(self, pos1, pos2, width, **args): def __init__(self, pos1, pos2, width, **args):
pos1 = Point(pos1) pos1 = Point(pos1)
pos2 = Point(pos2) pos2 = Point(pos2)
@ -1399,6 +1524,13 @@ class MultiRectROI(QtGui.QGraphicsObject):
This is generally used to mark a curved path through This is generally used to mark a curved path through
an image similarly to PolyLineROI. It differs in that each segment an image similarly to PolyLineROI. It differs in that each segment
of the chain is rectangular instead of linear and thus has width. of the chain is rectangular instead of linear and thus has width.
============== =============================================================
**Arguments**
points (list of length-2 sequences) The list of points in the path.
width (float) The width of the ROIs orthogonal to the path.
\**args All extra keyword arguments are passed to ROI()
============== =============================================================
""" """
sigRegionChangeFinished = QtCore.Signal(object) sigRegionChangeFinished = QtCore.Signal(object)
sigRegionChangeStarted = QtCore.Signal(object) sigRegionChangeStarted = QtCore.Signal(object)
@ -1523,6 +1655,18 @@ class MultiLineROI(MultiRectROI):
print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)") print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)")
class EllipseROI(ROI): class EllipseROI(ROI):
"""
Elliptical ROI subclass with one scale handle and one rotation handle.
============== =============================================================
**Arguments**
pos (length-2 sequence) The position of the ROI's origin.
size (length-2 sequence) The size of the ROI's bounding rectangle.
\**args All extra keyword arguments are passed to ROI()
============== =============================================================
"""
def __init__(self, pos, size, **args): def __init__(self, pos, size, **args):
#QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1])
ROI.__init__(self, pos, size, **args) ROI.__init__(self, pos, size, **args)
@ -1540,6 +1684,10 @@ class EllipseROI(ROI):
p.drawEllipse(r) p.drawEllipse(r)
def getArrayRegion(self, arr, img=None): def getArrayRegion(self, arr, img=None):
"""
Return the result of ROI.getArrayRegion() masked by the elliptical shape
of the ROI. Regions outside the ellipse are set to 0.
"""
arr = ROI.getArrayRegion(self, arr, img) arr = ROI.getArrayRegion(self, arr, img)
if arr is None or arr.shape[0] == 0 or arr.shape[1] == 0: if arr is None or arr.shape[0] == 0 or arr.shape[1] == 0:
return None return None
@ -1557,12 +1705,25 @@ class EllipseROI(ROI):
class CircleROI(EllipseROI): class CircleROI(EllipseROI):
"""
Circular ROI subclass. Behaves exactly as EllipseROI, but may only be scaled
proportionally to maintain its aspect ratio.
============== =============================================================
**Arguments**
pos (length-2 sequence) The position of the ROI's origin.
size (length-2 sequence) The size of the ROI's bounding rectangle.
\**args All extra keyword arguments are passed to ROI()
============== =============================================================
"""
def __init__(self, pos, size, **args): def __init__(self, pos, size, **args):
ROI.__init__(self, pos, size, **args) ROI.__init__(self, pos, size, **args)
self.aspectLocked = True self.aspectLocked = True
#self.addTranslateHandle([0.5, 0.5]) #self.addTranslateHandle([0.5, 0.5])
self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5])
class PolygonROI(ROI): class PolygonROI(ROI):
## deprecated. Use PloyLineROI instead. ## deprecated. Use PloyLineROI instead.
@ -1616,8 +1777,24 @@ class PolygonROI(ROI):
return sc return sc
class PolyLineROI(ROI): class PolyLineROI(ROI):
"""Container class for multiple connected LineSegmentROIs. Responsible for adding new """
line segments, and for translation/(rotation?) of multiple lines together.""" Container class for multiple connected LineSegmentROIs.
This class allows the user to draw paths of multiple line segments.
============== =============================================================
**Arguments**
positions (list of length-2 sequences) The list of points in the path.
Note that, unlike the handle positions specified in other
ROIs, these positions must be expressed in the normal
coordinate system of the ROI, rather than (0 to 1) relative
to the size of the ROI.
closed (bool) if True, an extra LineSegmentROI is added connecting
the beginning and end points.
\**args All extra keyword arguments are passed to ROI()
============== =============================================================
"""
def __init__(self, positions, closed=False, pos=None, **args): def __init__(self, positions, closed=False, pos=None, **args):
if pos is None: if pos is None:
@ -1730,6 +1907,10 @@ class PolyLineROI(ROI):
return p return p
def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds): def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds):
"""
Return the result of ROI.getArrayRegion(), masked by the shape of the
ROI. Values outside the ROI shape are set to 0.
"""
sl = self.getArraySlice(data, img, axes=(0,1)) sl = self.getArraySlice(data, img, axes=(0,1))
if sl is None: if sl is None:
return None return None
@ -1758,6 +1939,16 @@ class PolyLineROI(ROI):
class LineSegmentROI(ROI): class LineSegmentROI(ROI):
""" """
ROI subclass with two freely-moving handles defining a line. ROI subclass with two freely-moving handles defining a line.
============== =============================================================
**Arguments**
positions (list of two length-2 sequences) The endpoints of the line
segment. Note that, unlike the handle positions specified in
other ROIs, these positions must be expressed in the normal
coordinate system of the ROI, rather than (0 to 1) relative
to the size of the ROI.
\**args All extra keyword arguments are passed to ROI()
============== =============================================================
""" """
def __init__(self, positions=(None, None), pos=None, handles=(None,None), **args): def __init__(self, positions=(None, None), pos=None, handles=(None,None), **args):
@ -1810,8 +2001,13 @@ class LineSegmentROI(ROI):
def getArrayRegion(self, data, img, axes=(0,1)): def getArrayRegion(self, data, img, axes=(0,1)):
""" """
Use the position of this ROI relative to an imageItem to pull a slice from an array. Use the position of this ROI relative to an imageItem to pull a slice
Since this pulls 1D data from a 2D coordinate system, the return value will have ndim = data.ndim-1 from an array.
Since this pulls 1D data from a 2D coordinate system, the return value
will have ndim = data.ndim-1
See ROI.getArrayRegion() for a description of the arguments.
""" """
imgPts = [self.mapToItem(img, h['item'].pos()) for h in self.handles] imgPts = [self.mapToItem(img, h['item'].pos()) for h in self.handles]

View File

@ -664,8 +664,14 @@ class ScatterPlotItem(GraphicsObject):
if pxPad > 0: if pxPad > 0:
# determine length of pixel in local x, y directions # determine length of pixel in local x, y directions
px, py = self.pixelVectors() px, py = self.pixelVectors()
px = 0 if px is None else px.length() try:
py = 0 if py is None else py.length() px = 0 if px is None else px.length()
except OverflowError:
px = 0
try:
py = 0 if py is None else py.length()
except OverflowError:
py = 0
# return bounds expanded by pixel size # return bounds expanded by pixel size
px *= pxPad px *= pxPad

View File

@ -9,18 +9,18 @@ class TextItem(UIGraphicsItem):
""" """
def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None, angle=0): def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None, angle=0):
""" """
=========== ================================================================================= ============== =================================================================================
Arguments: **Arguments:**
*text* The text to display *text* The text to display
*color* The color of the text (any format accepted by pg.mkColor) *color* The color of the text (any format accepted by pg.mkColor)
*html* If specified, this overrides both *text* and *color* *html* If specified, this overrides both *text* and *color*
*anchor* A QPointF or (x,y) sequence indicating what region of the text box will *anchor* A QPointF or (x,y) sequence indicating what region of the text box will
be anchored to the item's position. A value of (0,0) sets the upper-left corner be anchored to the item's position. A value of (0,0) sets the upper-left corner
of the text box to be at the position specified by setPos(), while a value of (1,1) of the text box to be at the position specified by setPos(), while a value of (1,1)
sets the lower-right corner. sets the lower-right corner.
*border* A pen to use when drawing the border *border* A pen to use when drawing the border
*fill* A brush to use when filling within the border *fill* A brush to use when filling within the border
=========== ================================================================================= ============== =================================================================================
""" """
## not working yet ## not working yet

View File

@ -19,15 +19,15 @@ class VTickGroup(UIGraphicsItem):
""" """
def __init__(self, xvals=None, yrange=None, pen=None): def __init__(self, xvals=None, yrange=None, pen=None):
""" """
============= =================================================================== ============== ===================================================================
**Arguments** **Arguments:**
xvals A list of x values (in data coordinates) at which to draw ticks. xvals A list of x values (in data coordinates) at which to draw ticks.
yrange A list of [low, high] limits for the tick. 0 is the bottom of yrange A list of [low, high] limits for the tick. 0 is the bottom of
the view, 1 is the top. [0.8, 1] would draw ticks in the top the view, 1 is the top. [0.8, 1] would draw ticks in the top
fifth of the view. fifth of the view.
pen The pen to use for drawing ticks. Default is grey. Can be specified pen The pen to use for drawing ticks. Default is grey. Can be specified
as any argument valid for :func:`mkPen<pyqtgraph.mkPen>` as any argument valid for :func:`mkPen<pyqtgraph.mkPen>`
============= =================================================================== ============== ===================================================================
""" """
if yrange is None: if yrange is None:
yrange = [0, 1] yrange = [0, 1]
@ -56,10 +56,10 @@ class VTickGroup(UIGraphicsItem):
def setXVals(self, vals): def setXVals(self, vals):
"""Set the x values for the ticks. """Set the x values for the ticks.
============= ===================================================================== ============== =====================================================================
**Arguments** **Arguments:**
vals A list of x values (in data/plot coordinates) at which to draw ticks. vals A list of x values (in data/plot coordinates) at which to draw ticks.
============= ===================================================================== ============== =====================================================================
""" """
self.xvals = vals self.xvals = vals
self.rebuildTicks() self.rebuildTicks()

View File

@ -5,28 +5,63 @@ from ...Point import Point
from ... import functions as fn from ... import functions as fn
from .. ItemGroup import ItemGroup from .. ItemGroup import ItemGroup
from .. GraphicsWidget import GraphicsWidget from .. GraphicsWidget import GraphicsWidget
from ...GraphicsScene import GraphicsScene
import weakref import weakref
from copy import deepcopy from copy import deepcopy
from ... import debug as debug from ... import debug as debug
from ... import getConfigOption from ... import getConfigOption
import sys
from pyqtgraph.Qt import isQObjectAlive
__all__ = ['ViewBox'] __all__ = ['ViewBox']
class WeakList(object):
def __init__(self):
self._items = []
def append(self, obj):
#Add backwards to iterate backwards (to make iterating more efficient on removal).
self._items.insert(0, weakref.ref(obj))
def __iter__(self):
i = len(self._items)-1
while i >= 0:
ref = self._items[i]
d = ref()
if d is None:
del self._items[i]
else:
yield d
i -= 1
class ChildGroup(ItemGroup): class ChildGroup(ItemGroup):
sigItemsChanged = QtCore.Signal()
def __init__(self, parent): def __init__(self, parent):
ItemGroup.__init__(self, parent) ItemGroup.__init__(self, parent)
# Used as callback to inform ViewBox when items are added/removed from
# the group.
# Note 1: We would prefer to override itemChange directly on the
# ViewBox, but this causes crashes on PySide.
# Note 2: We might also like to use a signal rather than this callback
# mechanism, but this causes a different PySide crash.
self.itemsChangedListeners = WeakList()
# excempt from telling view when transform changes # excempt from telling view when transform changes
self._GraphicsObject__inform_view_on_change = False self._GraphicsObject__inform_view_on_change = False
def itemChange(self, change, value): def itemChange(self, change, value):
ret = ItemGroup.itemChange(self, change, value) ret = ItemGroup.itemChange(self, change, value)
if change == self.ItemChildAddedChange or change == self.ItemChildRemovedChange: if change == self.ItemChildAddedChange or change == self.ItemChildRemovedChange:
self.sigItemsChanged.emit() try:
itemsChangedListeners = self.itemsChangedListeners
except AttributeError:
# It's possible that the attribute was already collected when the itemChange happened
# (if it was triggered during the gc of the object).
pass
else:
for listener in itemsChangedListeners:
listener.itemsChanged()
return ret return ret
@ -71,16 +106,16 @@ class ViewBox(GraphicsWidget):
def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None): def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None):
""" """
============= ============================================================= ============== =============================================================
**Arguments** **Arguments:**
*parent* (QGraphicsWidget) Optional parent widget *parent* (QGraphicsWidget) Optional parent widget
*border* (QPen) Do draw a border around the view, give any *border* (QPen) Do draw a border around the view, give any
single argument accepted by :func:`mkPen <pyqtgraph.mkPen>` single argument accepted by :func:`mkPen <pyqtgraph.mkPen>`
*lockAspect* (False or float) The aspect ratio to lock the view *lockAspect* (False or float) The aspect ratio to lock the view
coorinates to. (or False to allow the ratio to change) coorinates to. (or False to allow the ratio to change)
*enableMouse* (bool) Whether mouse can be used to scale/pan the view *enableMouse* (bool) Whether mouse can be used to scale/pan the view
*invertY* (bool) See :func:`invertY <pyqtgraph.ViewBox.invertY>` *invertY* (bool) See :func:`invertY <pyqtgraph.ViewBox.invertY>`
============= ============================================================= ============== =============================================================
""" """
@ -118,6 +153,15 @@ class ViewBox(GraphicsWidget):
'wheelScaleFactor': -1.0 / 8.0, 'wheelScaleFactor': -1.0 / 8.0,
'background': None, 'background': None,
# Limits
'limits': {
'xLimits': [None, None], # Maximum and minimum visible X values
'yLimits': [None, None], # Maximum and minimum visible Y values
'xRange': [None, None], # Maximum and minimum X range
'yRange': [None, None], # Maximum and minimum Y range
}
} }
self._updatingRange = False ## Used to break recursive loops. See updateAutoRange. self._updatingRange = False ## Used to break recursive loops. See updateAutoRange.
self._itemBoundsCache = weakref.WeakKeyDictionary() self._itemBoundsCache = weakref.WeakKeyDictionary()
@ -131,7 +175,7 @@ class ViewBox(GraphicsWidget):
## this is a workaround for a Qt + OpenGL bug that causes improper clipping ## this is a workaround for a Qt + OpenGL bug that causes improper clipping
## https://bugreports.qt.nokia.com/browse/QTBUG-23723 ## https://bugreports.qt.nokia.com/browse/QTBUG-23723
self.childGroup = ChildGroup(self) self.childGroup = ChildGroup(self)
self.childGroup.sigItemsChanged.connect(self.itemsChanged) self.childGroup.itemsChangedListeners.append(self)
self.background = QtGui.QGraphicsRectItem(self.rect()) self.background = QtGui.QGraphicsRectItem(self.rect())
self.background.setParentItem(self) self.background.setParentItem(self)
@ -197,6 +241,7 @@ class ViewBox(GraphicsWidget):
del ViewBox.NamedViews[self.name] del ViewBox.NamedViews[self.name]
def close(self): def close(self):
self.clear()
self.unregister() self.unregister()
def implements(self, interface): def implements(self, interface):
@ -276,6 +321,17 @@ class ViewBox(GraphicsWidget):
self.updateViewRange() self.updateViewRange()
self.sigStateChanged.emit(self) self.sigStateChanged.emit(self)
def setBackgroundColor(self, color):
"""
Set the background color of the ViewBox.
If color is None, then no background will be drawn.
Added in version 0.9.9
"""
self.background.setVisible(color is not None)
self.state['background'] = color
self.updateBackground()
def setMouseMode(self, mode): def setMouseMode(self, mode):
""" """
@ -398,13 +454,20 @@ class ViewBox(GraphicsWidget):
print("make qrectf failed:", self.state['targetRange']) print("make qrectf failed:", self.state['targetRange'])
raise raise
def _resetTarget(self):
# Reset target range to exactly match current view range.
# This is used during mouse interaction to prevent unpredictable
# behavior (because the user is unaware of targetRange).
if self.state['aspectLocked'] is False: # (interferes with aspect locking)
self.state['targetRange'] = [self.state['viewRange'][0][:], self.state['viewRange'][1][:]]
def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=True, disableAutoRange=True): def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=True, disableAutoRange=True):
""" """
Set the visible range of the ViewBox. Set the visible range of the ViewBox.
Must specify at least one of *rect*, *xRange*, or *yRange*. Must specify at least one of *rect*, *xRange*, or *yRange*.
================== ===================================================================== ================== =====================================================================
**Arguments** **Arguments:**
*rect* (QRectF) The full range that should be visible in the view box. *rect* (QRectF) The full range that should be visible in the view box.
*xRange* (min,max) The range that should be visible along the x-axis. *xRange* (min,max) The range that should be visible along the x-axis.
*yRange* (min,max) The range that should be visible along the y-axis. *yRange* (min,max) The range that should be visible along the y-axis.
@ -546,14 +609,14 @@ class ViewBox(GraphicsWidget):
Note that this is not the same as enableAutoRange, which causes the view to Note that this is not the same as enableAutoRange, which causes the view to
automatically auto-range whenever its contents are changed. automatically auto-range whenever its contents are changed.
=========== ============================================================ ============== ============================================================
Arguments **Arguments:**
padding The fraction of the total data range to add on to the final padding The fraction of the total data range to add on to the final
visible range. By default, this value is set between 0.02 visible range. By default, this value is set between 0.02
and 0.1 depending on the size of the ViewBox. and 0.1 depending on the size of the ViewBox.
items If specified, this is a list of items to consider when items If specified, this is a list of items to consider when
determining the visible range. determining the visible range.
=========== ============================================================ ============== ============================================================
""" """
if item is None: if item is None:
bounds = self.childrenBoundingRect(items=items) bounds = self.childrenBoundingRect(items=items)
@ -571,6 +634,57 @@ class ViewBox(GraphicsWidget):
else: else:
padding = 0.02 padding = 0.02
return padding return padding
def setLimits(self, **kwds):
"""
Set limits that constrain the possible view ranges.
**Panning limits**. The following arguments define the region within the
viewbox coordinate system that may be accessed by panning the view.
=========== ============================================================
xMin Minimum allowed x-axis value
xMax Maximum allowed x-axis value
yMin Minimum allowed y-axis value
yMax Maximum allowed y-axis value
=========== ============================================================
**Scaling limits**. These arguments prevent the view being zoomed in or
out too far.
=========== ============================================================
minXRange Minimum allowed left-to-right span across the view.
maxXRange Maximum allowed left-to-right span across the view.
minYRange Minimum allowed top-to-bottom span across the view.
maxYRange Maximum allowed top-to-bottom span across the view.
=========== ============================================================
Added in version 0.9.9
"""
update = False
#for kwd in ['xLimits', 'yLimits', 'minRange', 'maxRange']:
#if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]:
#self.state['limits'][kwd] = kwds[kwd]
#update = True
for axis in [0,1]:
for mnmx in [0,1]:
kwd = [['xMin', 'xMax'], ['yMin', 'yMax']][axis][mnmx]
lname = ['xLimits', 'yLimits'][axis]
if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]:
self.state['limits'][lname][mnmx] = kwds[kwd]
update = True
kwd = [['minXRange', 'maxXRange'], ['minYRange', 'maxYRange']][axis][mnmx]
lname = ['xRange', 'yRange'][axis]
if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]:
self.state['limits'][lname][mnmx] = kwds[kwd]
update = True
if update:
self.updateViewRange()
def scaleBy(self, s=None, center=None, x=None, y=None): def scaleBy(self, s=None, center=None, x=None, y=None):
""" """
@ -818,7 +932,7 @@ class ViewBox(GraphicsWidget):
try: try:
getattr(oldLink, signal).disconnect(slot) getattr(oldLink, signal).disconnect(slot)
oldLink.sigResized.disconnect(slot) oldLink.sigResized.disconnect(slot)
except TypeError: except (TypeError, RuntimeError):
## This can occur if the view has been deleted already ## This can occur if the view has been deleted already
pass pass
@ -1056,6 +1170,7 @@ class ViewBox(GraphicsWidget):
center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos())) center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos()))
#center = ev.pos() #center = ev.pos()
self._resetTarget()
self.scaleBy(s, center) self.scaleBy(s, center)
self.sigRangeChangedManually.emit(self.state['mouseEnabled']) self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
ev.accept() ev.accept()
@ -1113,7 +1228,9 @@ class ViewBox(GraphicsWidget):
x = tr.x() if mask[0] == 1 else None x = tr.x() if mask[0] == 1 else None
y = tr.y() if mask[1] == 1 else None y = tr.y() if mask[1] == 1 else None
self.translateBy(x=x, y=y) self._resetTarget()
if x is not None or y is not None:
self.translateBy(x=x, y=y)
self.sigRangeChangedManually.emit(self.state['mouseEnabled']) self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
elif ev.button() & QtCore.Qt.RightButton: elif ev.button() & QtCore.Qt.RightButton:
#print "vb.rightDrag" #print "vb.rightDrag"
@ -1132,6 +1249,7 @@ class ViewBox(GraphicsWidget):
y = s[1] if mouseEnabled[1] == 1 else None y = s[1] if mouseEnabled[1] == 1 else None
center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton))) center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton)))
self._resetTarget()
self.scaleBy(x=x, y=y, center=center) self.scaleBy(x=x, y=y, center=center)
self.sigRangeChangedManually.emit(self.state['mouseEnabled']) self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
@ -1327,9 +1445,9 @@ class ViewBox(GraphicsWidget):
viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]]
changed = [False, False] changed = [False, False]
# Make correction for aspect ratio constraint #-------- Make correction for aspect ratio constraint ----------
## aspect is (widget w/h) / (view range w/h) # aspect is (widget w/h) / (view range w/h)
aspect = self.state['aspectLocked'] # size ratio / view ratio aspect = self.state['aspectLocked'] # size ratio / view ratio
tr = self.targetRect() tr = self.targetRect()
bounds = self.rect() bounds = self.rect()
@ -1351,7 +1469,6 @@ class ViewBox(GraphicsWidget):
# then make the entire target range visible # then make the entire target range visible
ax = 0 if targetRatio > viewRatio else 1 ax = 0 if targetRatio > viewRatio else 1
#### these should affect viewRange, not targetRange!
if ax == 0: if ax == 0:
## view range needs to be taller than target ## view range needs to be taller than target
dy = 0.5 * (tr.width() / viewRatio - tr.height()) dy = 0.5 * (tr.width() / viewRatio - tr.height())
@ -1364,8 +1481,59 @@ class ViewBox(GraphicsWidget):
if dx != 0: if dx != 0:
changed[0] = True changed[0] = True
viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx]
# ----------- Make corrections for view limits -----------
limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits'])
minRng = [self.state['limits']['xRange'][0], self.state['limits']['yRange'][0]]
maxRng = [self.state['limits']['xRange'][1], self.state['limits']['yRange'][1]]
for axis in [0, 1]:
if limits[axis][0] is None and limits[axis][1] is None and minRng[axis] is None and maxRng[axis] is None:
continue
changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) and (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] # max range cannot be larger than bounds, if they are given
if limits[axis][0] is not None and limits[axis][1] is not None:
if maxRng[axis] is not None:
maxRng[axis] = min(maxRng[axis], limits[axis][1]-limits[axis][0])
else:
maxRng[axis] = limits[axis][1]-limits[axis][0]
#print "\nLimits for axis %d: range=%s min=%s max=%s" % (axis, limits[axis], minRng[axis], maxRng[axis])
#print "Starting range:", viewRange[axis]
# Apply xRange, yRange
diff = viewRange[axis][1] - viewRange[axis][0]
if maxRng[axis] is not None and diff > maxRng[axis]:
delta = maxRng[axis] - diff
changed[axis] = True
elif minRng[axis] is not None and diff < minRng[axis]:
delta = minRng[axis] - diff
changed[axis] = True
else:
delta = 0
viewRange[axis][0] -= delta/2.
viewRange[axis][1] += delta/2.
#print "after applying min/max:", viewRange[axis]
# Apply xLimits, yLimits
mn, mx = limits[axis]
if mn is not None and viewRange[axis][0] < mn:
delta = mn - viewRange[axis][0]
viewRange[axis][0] += delta
viewRange[axis][1] += delta
changed[axis] = True
elif mx is not None and viewRange[axis][1] > mx:
delta = mx - viewRange[axis][1]
viewRange[axis][0] += delta
viewRange[axis][1] += delta
changed[axis] = True
#print "after applying edge limits:", viewRange[axis]
changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) or (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)]
self.state['viewRange'] = viewRange self.state['viewRange'] = viewRange
# emit range change signals # emit range change signals
@ -1493,6 +1661,9 @@ class ViewBox(GraphicsWidget):
## called when the application is about to exit. ## called when the application is about to exit.
## this disables all callbacks, which might otherwise generate errors if invoked during exit. ## this disables all callbacks, which might otherwise generate errors if invoked during exit.
for k in ViewBox.AllViews: for k in ViewBox.AllViews:
if isQObjectAlive(k) and getConfigOption('crashWarning'):
sys.stderr.write('Warning: ViewBox should be closed before application exit.\n')
try: try:
k.destroyed.disconnect() k.destroyed.disconnect()
except RuntimeError: ## signal is already disconnected. except RuntimeError: ## signal is already disconnected.

View File

@ -0,0 +1,47 @@
import gc
import weakref
try:
import faulthandler
faulthandler.enable()
except ImportError:
pass
import pyqtgraph as pg
pg.mkQApp()
def test_getViewWidget():
view = pg.PlotWidget()
vref = weakref.ref(view)
item = pg.InfiniteLine()
view.addItem(item)
assert item.getViewWidget() is view
del view
gc.collect()
assert vref() is None
assert item.getViewWidget() is None
def test_getViewWidget_deleted():
view = pg.PlotWidget()
item = pg.InfiniteLine()
view.addItem(item)
assert item.getViewWidget() is view
# Arrange to have Qt automatically delete the view widget
obj = pg.QtGui.QWidget()
view.setParent(obj)
del obj
gc.collect()
assert not pg.Qt.isQObjectAlive(view)
assert item.getViewWidget() is None
#if __name__ == '__main__':
#view = pg.PlotItem()
#vref = weakref.ref(view)
#item = pg.InfiniteLine()
#view.addItem(item)
#del view
#gc.collect()

View File

@ -19,11 +19,14 @@ def mkQApp():
class GraphicsWindow(GraphicsLayoutWidget): class GraphicsWindow(GraphicsLayoutWidget):
"""
Convenience subclass of :class:`GraphicsLayoutWidget
<pyqtgraph.GraphicsLayoutWidget>`. This class is intended for use from
the interactive python prompt.
"""
def __init__(self, title=None, size=(800,600), **kargs): def __init__(self, title=None, size=(800,600), **kargs):
mkQApp() mkQApp()
#self.win = QtGui.QMainWindow()
GraphicsLayoutWidget.__init__(self, **kargs) GraphicsLayoutWidget.__init__(self, **kargs)
#self.win.setCentralWidget(self)
self.resize(*size) self.resize(*size)
if title is not None: if title is not None:
self.setWindowTitle(title) self.setWindowTitle(title)

View File

@ -33,6 +33,11 @@ from .. import debug as debug
from ..SignalProxy import SignalProxy from ..SignalProxy import SignalProxy
try:
from bottleneck import nanmin, nanmax
except ImportError:
from numpy import nanmin, nanmax
#try: #try:
#from .. import metaarray as metaarray #from .. import metaarray as metaarray
#HAVE_METAARRAY = True #HAVE_METAARRAY = True
@ -196,7 +201,12 @@ class ImageView(QtGui.QWidget):
img = img.asarray() img = img.asarray()
if not isinstance(img, np.ndarray): if not isinstance(img, np.ndarray):
raise Exception("Image must be specified as ndarray.") required = ['dtype', 'max', 'min', 'ndim', 'shape', 'size']
if not all([hasattr(img, attr) for attr in required]):
raise TypeError("Image must be NumPy array or any object "
"that provides compatible attributes/methods:\n"
" %s" % str(required))
self.image = img self.image = img
self.imageDisp = None self.imageDisp = None
@ -319,11 +329,10 @@ class ImageView(QtGui.QWidget):
if self.imageDisp is None: if self.imageDisp is None:
image = self.normalize(self.image) image = self.normalize(self.image)
self.imageDisp = image self.imageDisp = image
self.levelMin, self.levelMax = list(map(float, ImageView.quickMinMax(self.imageDisp))) self.levelMin, self.levelMax = list(map(float, self.quickMinMax(self.imageDisp)))
return self.imageDisp return self.imageDisp
def close(self): def close(self):
"""Closes the widget nicely, making sure to clear the graphics scene and release memory.""" """Closes the widget nicely, making sure to clear the graphics scene and release memory."""
self.ui.roiPlot.close() self.ui.roiPlot.close()
@ -375,7 +384,6 @@ class ImageView(QtGui.QWidget):
else: else:
QtGui.QWidget.keyReleaseEvent(self, ev) QtGui.QWidget.keyReleaseEvent(self, ev)
def evalKeyState(self): def evalKeyState(self):
if len(self.keysPressed) == 1: if len(self.keysPressed) == 1:
key = list(self.keysPressed.keys())[0] key = list(self.keysPressed.keys())[0]
@ -399,16 +407,13 @@ class ImageView(QtGui.QWidget):
else: else:
self.play(0) self.play(0)
def timeout(self): def timeout(self):
now = ptime.time() now = ptime.time()
dt = now - self.lastPlayTime dt = now - self.lastPlayTime
if dt < 0: if dt < 0:
return return
n = int(self.playRate * dt) n = int(self.playRate * dt)
#print n, dt
if n != 0: if n != 0:
#print n, dt, self.lastPlayTime
self.lastPlayTime += (float(n)/self.playRate) self.lastPlayTime += (float(n)/self.playRate)
if self.currentIndex+n > self.image.shape[0]: if self.currentIndex+n > self.image.shape[0]:
self.play(0) self.play(0)
@ -433,17 +438,14 @@ class ImageView(QtGui.QWidget):
self.autoLevels() self.autoLevels()
self.roiChanged() self.roiChanged()
self.sigProcessingChanged.emit(self) self.sigProcessingChanged.emit(self)
def updateNorm(self): def updateNorm(self):
if self.ui.normTimeRangeCheck.isChecked(): if self.ui.normTimeRangeCheck.isChecked():
#print "show!"
self.normRgn.show() self.normRgn.show()
else: else:
self.normRgn.hide() self.normRgn.hide()
if self.ui.normROICheck.isChecked(): if self.ui.normROICheck.isChecked():
#print "show!"
self.normRoi.show() self.normRoi.show()
else: else:
self.normRoi.hide() self.normRoi.hide()
@ -519,21 +521,25 @@ class ImageView(QtGui.QWidget):
coords = coords - coords[:,0,np.newaxis] coords = coords - coords[:,0,np.newaxis]
xvals = (coords**2).sum(axis=0) ** 0.5 xvals = (coords**2).sum(axis=0) ** 0.5
self.roiCurve.setData(y=data, x=xvals) self.roiCurve.setData(y=data, x=xvals)
#self.ui.roiPlot.replot()
def quickMinMax(self, data):
@staticmethod """
def quickMinMax(data): Estimate the min/max values of *data* by subsampling.
"""
while data.size > 1e6: while data.size > 1e6:
ax = np.argmax(data.shape) ax = np.argmax(data.shape)
sl = [slice(None)] * data.ndim sl = [slice(None)] * data.ndim
sl[ax] = slice(None, None, 2) sl[ax] = slice(None, None, 2)
data = data[sl] data = data[sl]
return data.min(), data.max() return nanmin(data), nanmax(data)
def normalize(self, image): def normalize(self, image):
"""
Process *image* using the normalization options configured in the
control panel.
This can be repurposed to process any data through the same filter.
"""
if self.ui.normOffRadio.isChecked(): if self.ui.normOffRadio.isChecked():
return image return image

View File

@ -0,0 +1,11 @@
import pyqtgraph as pg
import numpy as np
app = pg.mkQApp()
def test_nan_image():
img = np.ones((10,10))
img[0,0] = np.nan
v = pg.image(img)
app.processEvents()
v.window().close()

View File

@ -40,7 +40,7 @@ class Parallelize(object):
def __init__(self, tasks=None, workers=None, block=True, progressDialog=None, randomReseed=True, **kwds): def __init__(self, tasks=None, workers=None, block=True, progressDialog=None, randomReseed=True, **kwds):
""" """
=============== =================================================================== =============== ===================================================================
Arguments: **Arguments:**
tasks list of objects to be processed (Parallelize will determine how to tasks list of objects to be processed (Parallelize will determine how to
distribute the tasks). If unspecified, then each worker will receive distribute the tasks). If unspecified, then each worker will receive
a single task with a unique id number. a single task with a unique id number.

View File

@ -1,13 +1,15 @@
from .remoteproxy import RemoteEventHandler, ClosedError, NoResultError, LocalObjectProxy, ObjectProxy
import subprocess, atexit, os, sys, time, random, socket, signal import subprocess, atexit, os, sys, time, random, socket, signal
import multiprocessing.connection import multiprocessing.connection
from ..Qt import USE_PYSIDE
try: try:
import cPickle as pickle import cPickle as pickle
except ImportError: except ImportError:
import pickle import pickle
from .remoteproxy import RemoteEventHandler, ClosedError, NoResultError, LocalObjectProxy, ObjectProxy
from ..Qt import USE_PYSIDE
from ..util import cprint # color printing for debugging
__all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ClosedError', 'NoResultError'] __all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ClosedError', 'NoResultError']
class Process(RemoteEventHandler): class Process(RemoteEventHandler):
@ -35,28 +37,29 @@ class Process(RemoteEventHandler):
return objects either by proxy or by value (if they are picklable). See return objects either by proxy or by value (if they are picklable). See
ProxyObject for more information. ProxyObject for more information.
""" """
_process_count = 1 # just used for assigning colors to each process for debugging
def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None): def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None):
""" """
============ ============================================================= ============== =============================================================
Arguments: **Arguments:**
name Optional name for this process used when printing messages name Optional name for this process used when printing messages
from the remote process. from the remote process.
target Optional function to call after starting remote process. target Optional function to call after starting remote process.
By default, this is startEventLoop(), which causes the remote By default, this is startEventLoop(), which causes the remote
process to process requests from the parent process until it process to process requests from the parent process until it
is asked to quit. If you wish to specify a different target, is asked to quit. If you wish to specify a different target,
it must be picklable (bound methods are not). it must be picklable (bound methods are not).
copySysPath If True, copy the contents of sys.path to the remote process copySysPath If True, copy the contents of sys.path to the remote process
debug If True, print detailed information about communication debug If True, print detailed information about communication
with the child process. with the child process.
wrapStdout If True (default on windows) then stdout and stderr from the wrapStdout If True (default on windows) then stdout and stderr from the
child process will be caught by the parent process and child process will be caught by the parent process and
forwarded to its stdout/stderr. This provides a workaround forwarded to its stdout/stderr. This provides a workaround
for a python bug: http://bugs.python.org/issue3905 for a python bug: http://bugs.python.org/issue3905
but has the side effect that child output is significantly but has the side effect that child output is significantly
delayed relative to the parent output. delayed relative to the parent output.
============ ============================================================= ============== =============================================================
""" """
if target is None: if target is None:
target = startEventLoop target = startEventLoop
@ -64,7 +67,7 @@ class Process(RemoteEventHandler):
name = str(self) name = str(self)
if executable is None: if executable is None:
executable = sys.executable executable = sys.executable
self.debug = debug self.debug = 7 if debug is True else False # 7 causes printing in white
## random authentication key ## random authentication key
authkey = os.urandom(20) authkey = os.urandom(20)
@ -75,21 +78,20 @@ class Process(RemoteEventHandler):
#print "key:", ' '.join([str(ord(x)) for x in authkey]) #print "key:", ' '.join([str(ord(x)) for x in authkey])
## Listen for connection from remote process (and find free port number) ## Listen for connection from remote process (and find free port number)
port = 10000 l = multiprocessing.connection.Listener(('localhost', 0), authkey=authkey)
while True: port = l.address[1]
try:
l = multiprocessing.connection.Listener(('localhost', int(port)), authkey=authkey)
break
except socket.error as ex:
if ex.errno != 98 and ex.errno != 10048: # unix=98, win=10048
raise
port += 1
## start remote process, instruct it to run target function ## start remote process, instruct it to run target function
sysPath = sys.path if copySysPath else None sysPath = sys.path if copySysPath else None
bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py')) bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py'))
self.debugMsg('Starting child process (%s %s)' % (executable, bootstrap)) self.debugMsg('Starting child process (%s %s)' % (executable, bootstrap))
# Decide on printing color for this process
if debug:
procDebug = (Process._process_count%6) + 1 # pick a color for this process to print in
Process._process_count += 1
else:
procDebug = False
if wrapStdout is None: if wrapStdout is None:
wrapStdout = sys.platform.startswith('win') wrapStdout = sys.platform.startswith('win')
@ -102,8 +104,8 @@ class Process(RemoteEventHandler):
self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE, stdout=stdout, stderr=stderr) self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE, stdout=stdout, stderr=stderr)
## to circumvent the bug and still make the output visible, we use ## to circumvent the bug and still make the output visible, we use
## background threads to pass data from pipes to stdout/stderr ## background threads to pass data from pipes to stdout/stderr
self._stdoutForwarder = FileForwarder(self.proc.stdout, "stdout") self._stdoutForwarder = FileForwarder(self.proc.stdout, "stdout", procDebug)
self._stderrForwarder = FileForwarder(self.proc.stderr, "stderr") self._stderrForwarder = FileForwarder(self.proc.stderr, "stderr", procDebug)
else: else:
self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE) self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE)
@ -120,7 +122,7 @@ class Process(RemoteEventHandler):
targetStr=targetStr, targetStr=targetStr,
path=sysPath, path=sysPath,
pyside=USE_PYSIDE, pyside=USE_PYSIDE,
debug=debug debug=procDebug
) )
pickle.dump(data, self.proc.stdin) pickle.dump(data, self.proc.stdin)
self.proc.stdin.close() self.proc.stdin.close()
@ -136,8 +138,8 @@ class Process(RemoteEventHandler):
continue continue
else: else:
raise raise
RemoteEventHandler.__init__(self, conn, name+'_parent', pid=self.proc.pid, debug=debug) RemoteEventHandler.__init__(self, conn, name+'_parent', pid=self.proc.pid, debug=self.debug)
self.debugMsg('Connected to child process.') self.debugMsg('Connected to child process.')
atexit.register(self.join) atexit.register(self.join)
@ -167,10 +169,11 @@ class Process(RemoteEventHandler):
def startEventLoop(name, port, authkey, ppid, debug=False): def startEventLoop(name, port, authkey, ppid, debug=False):
if debug: if debug:
import os import os
print('[%d] connecting to server at port localhost:%d, authkey=%s..' % (os.getpid(), port, repr(authkey))) cprint.cout(debug, '[%d] connecting to server at port localhost:%d, authkey=%s..\n'
% (os.getpid(), port, repr(authkey)), -1)
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey) conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
if debug: if debug:
print('[%d] connected; starting remote proxy.' % os.getpid()) cprint.cout(debug, '[%d] connected; starting remote proxy.\n' % os.getpid(), -1)
global HANDLER global HANDLER
#ppid = 0 if not hasattr(os, 'getppid') else os.getppid() #ppid = 0 if not hasattr(os, 'getppid') else os.getppid()
HANDLER = RemoteEventHandler(conn, name, ppid, debug=debug) HANDLER = RemoteEventHandler(conn, name, ppid, debug=debug)
@ -380,17 +383,17 @@ class QtProcess(Process):
def __init__(self, **kwds): def __init__(self, **kwds):
if 'target' not in kwds: if 'target' not in kwds:
kwds['target'] = startQtEventLoop kwds['target'] = startQtEventLoop
from ..Qt import QtGui ## avoid module-level import to keep bootstrap snappy.
self._processRequests = kwds.pop('processRequests', True) self._processRequests = kwds.pop('processRequests', True)
if self._processRequests and QtGui.QApplication.instance() is None:
raise Exception("Must create QApplication before starting QtProcess, or use QtProcess(processRequests=False)")
Process.__init__(self, **kwds) Process.__init__(self, **kwds)
self.startEventTimer() self.startEventTimer()
def startEventTimer(self): def startEventTimer(self):
from ..Qt import QtGui, QtCore ## avoid module-level import to keep bootstrap snappy. from ..Qt import QtCore ## avoid module-level import to keep bootstrap snappy.
self.timer = QtCore.QTimer() self.timer = QtCore.QTimer()
if self._processRequests: if self._processRequests:
app = QtGui.QApplication.instance()
if app is None:
raise Exception("Must create QApplication before starting QtProcess, or use QtProcess(processRequests=False)")
self.startRequestProcessing() self.startRequestProcessing()
def startRequestProcessing(self, interval=0.01): def startRequestProcessing(self, interval=0.01):
@ -412,10 +415,10 @@ class QtProcess(Process):
def startQtEventLoop(name, port, authkey, ppid, debug=False): def startQtEventLoop(name, port, authkey, ppid, debug=False):
if debug: if debug:
import os import os
print('[%d] connecting to server at port localhost:%d, authkey=%s..' % (os.getpid(), port, repr(authkey))) cprint.cout(debug, '[%d] connecting to server at port localhost:%d, authkey=%s..\n' % (os.getpid(), port, repr(authkey)), -1)
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey) conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
if debug: if debug:
print('[%d] connected; starting remote proxy.' % os.getpid()) cprint.cout(debug, '[%d] connected; starting remote proxy.\n' % os.getpid(), -1)
from ..Qt import QtGui, QtCore from ..Qt import QtGui, QtCore
#from PyQt4 import QtGui, QtCore #from PyQt4 import QtGui, QtCore
app = QtGui.QApplication.instance() app = QtGui.QApplication.instance()
@ -445,11 +448,13 @@ class FileForwarder(threading.Thread):
which ensures that the correct behavior is achieved even if which ensures that the correct behavior is achieved even if
sys.stdout/stderr are replaced at runtime. sys.stdout/stderr are replaced at runtime.
""" """
def __init__(self, input, output): def __init__(self, input, output, color):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.input = input self.input = input
self.output = output self.output = output
self.lock = threading.Lock() self.lock = threading.Lock()
self.daemon = True
self.color = color
self.start() self.start()
def run(self): def run(self):
@ -457,12 +462,12 @@ class FileForwarder(threading.Thread):
while True: while True:
line = self.input.readline() line = self.input.readline()
with self.lock: with self.lock:
sys.stdout.write(line) cprint.cout(self.color, line, -1)
elif self.output == 'stderr': elif self.output == 'stderr':
while True: while True:
line = self.input.readline() line = self.input.readline()
with self.lock: with self.lock:
sys.stderr.write(line) cprint.cerr(self.color, line, -1)
else: else:
while True: while True:
line = self.input.readline() line = self.input.readline()

View File

@ -7,6 +7,9 @@ except ImportError:
import builtins import builtins
import pickle import pickle
# color printing for debugging
from ..util import cprint
class ClosedError(Exception): class ClosedError(Exception):
"""Raised when an event handler receives a request to close the connection """Raised when an event handler receives a request to close the connection
or discovers that the connection has been closed.""" or discovers that the connection has been closed."""
@ -80,7 +83,7 @@ class RemoteEventHandler(object):
def debugMsg(self, msg): def debugMsg(self, msg):
if not self.debug: if not self.debug:
return return
print("[%d] %s" % (os.getpid(), str(msg))) cprint.cout(self.debug, "[%d] %s\n" % (os.getpid(), str(msg)), -1)
def getProxyOption(self, opt): def getProxyOption(self, opt):
return self.proxyOptions[opt] return self.proxyOptions[opt]
@ -299,23 +302,23 @@ class RemoteEventHandler(object):
(The docstring has information that is nevertheless useful to the programmer (The docstring has information that is nevertheless useful to the programmer
as it describes the internal protocol used to communicate between processes) as it describes the internal protocol used to communicate between processes)
========== ==================================================================== ============== ====================================================================
Arguments: **Arguments:**
request String describing the type of request being sent (see below) request String describing the type of request being sent (see below)
reqId Integer uniquely linking a result back to the request that generated reqId Integer uniquely linking a result back to the request that generated
it. (most requests leave this blank) it. (most requests leave this blank)
callSync 'sync': return the actual result of the request callSync 'sync': return the actual result of the request
'async': return a Request object which can be used to look up the 'async': return a Request object which can be used to look up the
result later result later
'off': return no result 'off': return no result
timeout Time in seconds to wait for a response when callSync=='sync' timeout Time in seconds to wait for a response when callSync=='sync'
opts Extra arguments sent to the remote process that determine the way opts Extra arguments sent to the remote process that determine the way
the request will be handled (see below) the request will be handled (see below)
returnType 'proxy', 'value', or 'auto' returnType 'proxy', 'value', or 'auto'
byteData If specified, this is a list of objects to be sent as byte messages byteData If specified, this is a list of objects to be sent as byte messages
to the remote process. to the remote process.
This is used to send large arrays without the cost of pickling. This is used to send large arrays without the cost of pickling.
========== ==================================================================== ============== ====================================================================
Description of request strings and options allowed for each: Description of request strings and options allowed for each:
@ -576,7 +579,7 @@ class Request(object):
return self._result return self._result
if timeout is None: if timeout is None:
timeout = self.timeout timeout = self.timeout
if block: if block:
start = time.time() start = time.time()

View File

@ -36,6 +36,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
## (rotation around z-axis 0 points along x-axis) ## (rotation around z-axis 0 points along x-axis)
'viewport': None, ## glViewport params; None == whole widget 'viewport': None, ## glViewport params; None == whole widget
} }
self.setBackgroundColor('k')
self.items = [] self.items = []
self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown] self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown]
self.keysPressed = {} self.keysPressed = {}
@ -64,9 +65,16 @@ class GLViewWidget(QtOpenGL.QGLWidget):
def initializeGL(self): def initializeGL(self):
glClearColor(0.0, 0.0, 0.0, 0.0)
self.resizeGL(self.width(), self.height()) self.resizeGL(self.width(), self.height())
def setBackgroundColor(self, *args, **kwds):
"""
Set the background color of the widget. Accepts the same arguments as
pg.mkColor().
"""
self.opts['bgcolor'] = fn.mkColor(*args, **kwds)
self.update()
def getViewport(self): def getViewport(self):
vp = self.opts['viewport'] vp = self.opts['viewport']
if vp is None: if vp is None:
@ -129,6 +137,12 @@ class GLViewWidget(QtOpenGL.QGLWidget):
return tr return tr
def itemsAt(self, region=None): def itemsAt(self, region=None):
"""
Return a list of the items displayed in the region (x, y, w, h)
relative to the widget.
"""
region = (region[0], self.height()-(region[1]+region[3]), region[2], region[3])
#buf = np.zeros(100000, dtype=np.uint) #buf = np.zeros(100000, dtype=np.uint)
buf = glSelectBuffer(100000) buf = glSelectBuffer(100000)
try: try:
@ -140,12 +154,12 @@ class GLViewWidget(QtOpenGL.QGLWidget):
finally: finally:
hits = glRenderMode(GL_RENDER) hits = glRenderMode(GL_RENDER)
items = [(h.near, h.names[0]) for h in hits] items = [(h.near, h.names[0]) for h in hits]
items.sort(key=lambda i: i[0]) items.sort(key=lambda i: i[0])
return [self._itemNames[i[1]] for i in items] return [self._itemNames[i[1]] for i in items]
def paintGL(self, region=None, viewport=None, useItemNames=False): def paintGL(self, region=None, viewport=None, useItemNames=False):
""" """
viewport specifies the arguments to glViewport. If None, then we use self.opts['viewport'] viewport specifies the arguments to glViewport. If None, then we use self.opts['viewport']
@ -158,6 +172,8 @@ class GLViewWidget(QtOpenGL.QGLWidget):
glViewport(*viewport) glViewport(*viewport)
self.setProjection(region=region) self.setProjection(region=region)
self.setModelview() self.setModelview()
bgcolor = self.opts['bgcolor']
glClearColor(bgcolor.red(), bgcolor.green(), bgcolor.blue(), 1.0)
glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT ) glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT )
self.drawItemTree(useItemNames=useItemNames) self.drawItemTree(useItemNames=useItemNames)
@ -180,7 +196,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
i.paint() i.paint()
except: except:
from .. import debug from .. import debug
pyqtgraph.debug.printExc() debug.printExc()
msg = "Error while drawing item %s." % str(item) msg = "Error while drawing item %s." % str(item)
ver = glGetString(GL_VERSION) ver = glGetString(GL_VERSION)
if ver is not None: if ver is not None:
@ -294,6 +310,17 @@ class GLViewWidget(QtOpenGL.QGLWidget):
def mouseReleaseEvent(self, ev): def mouseReleaseEvent(self, ev):
pass pass
# Example item selection code:
#region = (ev.pos().x()-5, ev.pos().y()-5, 10, 10)
#print(self.itemsAt(region))
## debugging code: draw the picking region
#glViewport(*self.getViewport())
#glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT )
#region = (region[0], self.height()-(region[1]+region[3]), region[2], region[3])
#self.paintGL(region=region)
#self.swapBuffers()
def wheelEvent(self, ev): def wheelEvent(self, ev):
if (ev.modifiers() & QtCore.Qt.ControlModifier): if (ev.modifiers() & QtCore.Qt.ControlModifier):

View File

@ -1,5 +1,5 @@
from ..Qt import QtGui from pyqtgraph.Qt import QtGui
from .. import functions as fn import pyqtgraph.functions as fn
import numpy as np import numpy as np
class MeshData(object): class MeshData(object):
@ -23,18 +23,18 @@ class MeshData(object):
def __init__(self, vertexes=None, faces=None, edges=None, vertexColors=None, faceColors=None): def __init__(self, vertexes=None, faces=None, edges=None, vertexColors=None, faceColors=None):
""" """
============= ===================================================== ============== =====================================================
Arguments **Arguments:**
vertexes (Nv, 3) array of vertex coordinates. vertexes (Nv, 3) array of vertex coordinates.
If faces is not specified, then this will instead be If faces is not specified, then this will instead be
interpreted as (Nf, 3, 3) array of coordinates. interpreted as (Nf, 3, 3) array of coordinates.
faces (Nf, 3) array of indexes into the vertex array. faces (Nf, 3) array of indexes into the vertex array.
edges [not available yet] edges [not available yet]
vertexColors (Nv, 4) array of vertex colors. vertexColors (Nv, 4) array of vertex colors.
If faces is not specified, then this will instead be If faces is not specified, then this will instead be
interpreted as (Nf, 3, 4) array of colors. interpreted as (Nf, 3, 4) array of colors.
faceColors (Nf, 4) array of face colors. faceColors (Nf, 4) array of face colors.
============= ===================================================== ============== =====================================================
All arguments are optional. All arguments are optional.
""" """
@ -84,64 +84,11 @@ class MeshData(object):
if faceColors is not None: if faceColors is not None:
self.setFaceColors(faceColors) self.setFaceColors(faceColors)
#self.setFaces(vertexes=vertexes, faces=faces, vertexColors=vertexColors, faceColors=faceColors)
#def setFaces(self, vertexes=None, faces=None, vertexColors=None, faceColors=None):
#"""
#Set the faces in this data set.
#Data may be provided either as an Nx3x3 array of floats (9 float coordinate values per face)::
#faces = [ [(x, y, z), (x, y, z), (x, y, z)], ... ]
#or as an Nx3 array of ints (vertex integers) AND an Mx3 array of floats (3 float coordinate values per vertex)::
#faces = [ (p1, p2, p3), ... ]
#vertexes = [ (x, y, z), ... ]
#"""
#if not isinstance(vertexes, np.ndarray):
#vertexes = np.array(vertexes)
#if vertexes.dtype != np.float:
#vertexes = vertexes.astype(float)
#if faces is None:
#self._setIndexedFaces(vertexes, vertexColors, faceColors)
#else:
#self._setUnindexedFaces(faces, vertexes, vertexColors, faceColors)
##print self.vertexes().shape
##print self.faces().shape
#def setMeshColor(self, color):
#"""Set the color of the entire mesh. This removes any per-face or per-vertex colors."""
#color = fn.Color(color)
#self._meshColor = color.glColor()
#self._vertexColors = None
#self._faceColors = None
#def __iter__(self):
#"""Iterate over all faces, yielding a list of three tuples [(position, normal, color), ...] for each face."""
#vnorms = self.vertexNormals()
#vcolors = self.vertexColors()
#for i in range(self._faces.shape[0]):
#face = []
#for j in [0,1,2]:
#vind = self._faces[i,j]
#pos = self._vertexes[vind]
#norm = vnorms[vind]
#if vcolors is None:
#color = self._meshColor
#else:
#color = vcolors[vind]
#face.append((pos, norm, color))
#yield face
#def __len__(self):
#return len(self._faces)
def faces(self): def faces(self):
"""Return an array (Nf, 3) of vertex indexes, three per triangular face in the mesh.""" """Return an array (Nf, 3) of vertex indexes, three per triangular face in the mesh.
If faces have not been computed for this mesh, the function returns None.
"""
return self._faces return self._faces
def edges(self): def edges(self):
@ -161,8 +108,6 @@ class MeshData(object):
self.resetNormals() self.resetNormals()
self._vertexColorsIndexedByFaces = None self._vertexColorsIndexedByFaces = None
self._faceColorsIndexedByFaces = None self._faceColorsIndexedByFaces = None
def vertexes(self, indexed=None): def vertexes(self, indexed=None):
"""Return an array (N,3) of the positions of vertexes in the mesh. """Return an array (N,3) of the positions of vertexes in the mesh.
@ -207,7 +152,6 @@ class MeshData(object):
self._vertexNormalsIndexedByFaces = None self._vertexNormalsIndexedByFaces = None
self._faceNormals = None self._faceNormals = None
self._faceNormalsIndexedByFaces = None self._faceNormalsIndexedByFaces = None
def hasFaceIndexedData(self): def hasFaceIndexedData(self):
"""Return True if this object already has vertex positions indexed by face""" """Return True if this object already has vertex positions indexed by face"""
@ -229,7 +173,6 @@ class MeshData(object):
if v is not None: if v is not None:
return True return True
return False return False
def faceNormals(self, indexed=None): def faceNormals(self, indexed=None):
""" """
@ -242,7 +185,6 @@ class MeshData(object):
v = self.vertexes(indexed='faces') v = self.vertexes(indexed='faces')
self._faceNormals = np.cross(v[:,1]-v[:,0], v[:,2]-v[:,0]) self._faceNormals = np.cross(v[:,1]-v[:,0], v[:,2]-v[:,0])
if indexed is None: if indexed is None:
return self._faceNormals return self._faceNormals
elif indexed == 'faces': elif indexed == 'faces':
@ -266,7 +208,11 @@ class MeshData(object):
vertFaces = self.vertexFaces() vertFaces = self.vertexFaces()
self._vertexNormals = np.empty(self._vertexes.shape, dtype=float) self._vertexNormals = np.empty(self._vertexes.shape, dtype=float)
for vindex in xrange(self._vertexes.shape[0]): for vindex in xrange(self._vertexes.shape[0]):
norms = faceNorms[vertFaces[vindex]] ## get all face normals faces = vertFaces[vindex]
if len(faces) == 0:
self._vertexNormals[vindex] = (0,0,0)
continue
norms = faceNorms[faces] ## get all face normals
norm = norms.sum(axis=0) ## sum normals norm = norms.sum(axis=0) ## sum normals
norm /= (norm**2).sum()**0.5 ## and re-normalize norm /= (norm**2).sum()**0.5 ## and re-normalize
self._vertexNormals[vindex] = norm self._vertexNormals[vindex] = norm
@ -363,7 +309,6 @@ class MeshData(object):
## This is done by collapsing into a list of 'unique' vertexes (difference < 1e-14) ## This is done by collapsing into a list of 'unique' vertexes (difference < 1e-14)
## I think generally this should be discouraged.. ## I think generally this should be discouraged..
faces = self._vertexesIndexedByFaces faces = self._vertexesIndexedByFaces
verts = {} ## used to remember the index of each vertex position verts = {} ## used to remember the index of each vertex position
self._faces = np.empty(faces.shape[:2], dtype=np.uint) self._faces = np.empty(faces.shape[:2], dtype=np.uint)
@ -403,12 +348,10 @@ class MeshData(object):
Return list mapping each vertex index to a list of face indexes that use the vertex. Return list mapping each vertex index to a list of face indexes that use the vertex.
""" """
if self._vertexFaces is None: if self._vertexFaces is None:
self._vertexFaces = [None] * len(self.vertexes()) self._vertexFaces = [[] for i in xrange(len(self.vertexes()))]
for i in xrange(self._faces.shape[0]): for i in xrange(self._faces.shape[0]):
face = self._faces[i] face = self._faces[i]
for ind in face: for ind in face:
if self._vertexFaces[ind] is None:
self._vertexFaces[ind] = [] ## need a unique/empty list to fill
self._vertexFaces[ind].append(i) self._vertexFaces[ind].append(i)
return self._vertexFaces return self._vertexFaces
@ -426,22 +369,35 @@ class MeshData(object):
#pass #pass
def _computeEdges(self): def _computeEdges(self):
## generate self._edges from self._faces if not self.hasFaceIndexedData:
#print self._faces ## generate self._edges from self._faces
nf = len(self._faces) nf = len(self._faces)
edges = np.empty(nf*3, dtype=[('i', np.uint, 2)]) edges = np.empty(nf*3, dtype=[('i', np.uint, 2)])
edges['i'][0:nf] = self._faces[:,:2] edges['i'][0:nf] = self._faces[:,:2]
edges['i'][nf:2*nf] = self._faces[:,1:3] edges['i'][nf:2*nf] = self._faces[:,1:3]
edges['i'][-nf:,0] = self._faces[:,2] edges['i'][-nf:,0] = self._faces[:,2]
edges['i'][-nf:,1] = self._faces[:,0] edges['i'][-nf:,1] = self._faces[:,0]
# sort per-edge # sort per-edge
mask = edges['i'][:,0] > edges['i'][:,1] mask = edges['i'][:,0] > edges['i'][:,1]
edges['i'][mask] = edges['i'][mask][:,::-1] edges['i'][mask] = edges['i'][mask][:,::-1]
# remove duplicate entries # remove duplicate entries
self._edges = np.unique(edges)['i'] self._edges = np.unique(edges)['i']
#print self._edges #print self._edges
elif self._vertexesIndexedByFaces is not None:
verts = self._vertexesIndexedByFaces
edges = np.empty((verts.shape[0], 3, 2), dtype=np.uint)
nf = verts.shape[0]
edges[:,0,0] = np.arange(nf) * 3
edges[:,0,1] = edges[:,0,0] + 1
edges[:,1,0] = edges[:,0,1]
edges[:,1,1] = edges[:,1,0] + 1
edges[:,2,0] = edges[:,1,1]
edges[:,2,1] = edges[:,0,0]
self._edges = edges
else:
raise Exception("MeshData cannot generate edges--no faces in this data.")
def save(self): def save(self):
@ -516,4 +472,33 @@ class MeshData(object):
return MeshData(vertexes=verts, faces=faces) return MeshData(vertexes=verts, faces=faces)
@staticmethod
def cylinder(rows, cols, radius=[1.0, 1.0], length=1.0, offset=False):
"""
Return a MeshData instance with vertexes and faces computed
for a cylindrical surface.
The cylinder may be tapered with different radii at each end (truncated cone)
"""
verts = np.empty((rows+1, cols, 3), dtype=float)
if isinstance(radius, int):
radius = [radius, radius] # convert to list
## compute vertexes
th = np.linspace(2 * np.pi, 0, cols).reshape(1, cols)
r = np.linspace(radius[0],radius[1],num=rows+1, endpoint=True).reshape(rows+1, 1) # radius as a function of z
verts[...,2] = np.linspace(0, length, num=rows+1, endpoint=True).reshape(rows+1, 1) # z
if offset:
th = th + ((np.pi / cols) * np.arange(rows+1).reshape(rows+1,1)) ## rotate each row by 1/2 column
verts[...,0] = r * np.cos(th) # x = r cos(th)
verts[...,1] = r * np.sin(th) # y = r sin(th)
verts = verts.reshape((rows+1)*cols, 3) # just reshape: no redundant vertices...
## compute faces
faces = np.empty((rows*cols*2, 3), dtype=np.uint)
rowtemplate1 = ((np.arange(cols).reshape(cols, 1) + np.array([[0, 1, 0]])) % cols) + np.array([[0, 0, cols]])
rowtemplate2 = ((np.arange(cols).reshape(cols, 1) + np.array([[0, 1, 1]])) % cols) + np.array([[cols, 0, cols]])
for row in range(rows):
start = row * cols * 2
faces[start:start+cols] = rowtemplate1 + row * cols
faces[start+cols:start+(cols*2)] = rowtemplate2 + row * cols
return MeshData(vertexes=verts, faces=faces)

View File

@ -45,7 +45,7 @@ class GLAxisItem(GLGraphicsItem):
if self.antialias: if self.antialias:
glEnable(GL_LINE_SMOOTH) glEnable(GL_LINE_SMOOTH)
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); glHint(GL_LINE_SMOOTH_HINT, GL_NICEST)
glBegin( GL_LINES ) glBegin( GL_LINES )

View File

@ -1,3 +1,5 @@
import numpy as np
from OpenGL.GL import * from OpenGL.GL import *
from .. GLGraphicsItem import GLGraphicsItem from .. GLGraphicsItem import GLGraphicsItem
from ... import QtGui from ... import QtGui
@ -16,8 +18,9 @@ class GLGridItem(GLGraphicsItem):
self.setGLOptions(glOptions) self.setGLOptions(glOptions)
self.antialias = antialias self.antialias = antialias
if size is None: if size is None:
size = QtGui.QVector3D(1,1,1) size = QtGui.QVector3D(20,20,1)
self.setSize(size=size) self.setSize(size=size)
self.setSpacing(1, 1, 1)
def setSize(self, x=None, y=None, z=None, size=None): def setSize(self, x=None, y=None, z=None, size=None):
""" """
@ -33,8 +36,22 @@ class GLGridItem(GLGraphicsItem):
def size(self): def size(self):
return self.__size[:] return self.__size[:]
def setSpacing(self, x=None, y=None, z=None, spacing=None):
"""
Set the spacing between grid lines.
Arguments can be x,y,z or spacing=QVector3D().
"""
if spacing is not None:
x = spacing.x()
y = spacing.y()
z = spacing.z()
self.__spacing = [x,y,z]
self.update()
def spacing(self):
return self.__spacing[:]
def paint(self): def paint(self):
self.setupGLState() self.setupGLState()
@ -42,17 +59,20 @@ class GLGridItem(GLGraphicsItem):
glEnable(GL_LINE_SMOOTH) glEnable(GL_LINE_SMOOTH)
glEnable(GL_BLEND) glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); glHint(GL_LINE_SMOOTH_HINT, GL_NICEST)
glBegin( GL_LINES ) glBegin( GL_LINES )
x,y,z = self.size() x,y,z = self.size()
xs,ys,zs = self.spacing()
xvals = np.arange(-x/2., x/2. + xs*0.001, xs)
yvals = np.arange(-y/2., y/2. + ys*0.001, ys)
glColor4f(1, 1, 1, .3) glColor4f(1, 1, 1, .3)
for x in range(-10, 11): for x in xvals:
glVertex3f(x, -10, 0) glVertex3f(x, yvals[0], 0)
glVertex3f(x, 10, 0) glVertex3f(x, yvals[-1], 0)
for y in range(-10, 11): for y in yvals:
glVertex3f(-10, y, 0) glVertex3f(xvals[0], y, 0)
glVertex3f( 10, y, 0) glVertex3f(xvals[-1], y, 0)
glEnd() glEnd()

View File

@ -16,6 +16,7 @@ class GLLinePlotItem(GLGraphicsItem):
glopts = kwds.pop('glOptions', 'additive') glopts = kwds.pop('glOptions', 'additive')
self.setGLOptions(glopts) self.setGLOptions(glopts)
self.pos = None self.pos = None
self.mode = 'line_strip'
self.width = 1. self.width = 1.
self.color = (1.0,1.0,1.0,1.0) self.color = (1.0,1.0,1.0,1.0)
self.setData(**kwds) self.setData(**kwds)
@ -27,7 +28,7 @@ class GLLinePlotItem(GLGraphicsItem):
colors unchanged, etc. colors unchanged, etc.
==================== ================================================== ==================== ==================================================
Arguments: **Arguments:**
------------------------------------------------------------------------ ------------------------------------------------------------------------
pos (N,3) array of floats specifying point locations. pos (N,3) array of floats specifying point locations.
color (N,4) array of floats (0.0-1.0) or color (N,4) array of floats (0.0-1.0) or
@ -35,9 +36,13 @@ class GLLinePlotItem(GLGraphicsItem):
a single color for the entire item. a single color for the entire item.
width float specifying line width width float specifying line width
antialias enables smooth line drawing antialias enables smooth line drawing
mode 'lines': Each pair of vertexes draws a single line
segment.
'line_strip': All vertexes are drawn as a
continuous set of line segments.
==================== ================================================== ==================== ==================================================
""" """
args = ['pos', 'color', 'width', 'connected', 'antialias'] args = ['pos', 'color', 'width', 'mode', 'antialias']
for k in kwds.keys(): for k in kwds.keys():
if k not in args: if k not in args:
raise Exception('Invalid keyword argument: %s (allowed arguments are %s)' % (k, str(args))) raise Exception('Invalid keyword argument: %s (allowed arguments are %s)' % (k, str(args)))
@ -91,9 +96,15 @@ class GLLinePlotItem(GLGraphicsItem):
glEnable(GL_LINE_SMOOTH) glEnable(GL_LINE_SMOOTH)
glEnable(GL_BLEND) glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); glHint(GL_LINE_SMOOTH_HINT, GL_NICEST)
if self.mode == 'line_strip':
glDrawArrays(GL_LINE_STRIP, 0, int(self.pos.size / self.pos.shape[-1]))
elif self.mode == 'lines':
glDrawArrays(GL_LINES, 0, int(self.pos.size / self.pos.shape[-1]))
else:
raise Exception("Unknown line mode '%s'. (must be 'lines' or 'line_strip')" % self.mode)
glDrawArrays(GL_LINE_STRIP, 0, int(self.pos.size / self.pos.shape[-1]))
finally: finally:
glDisableClientState(GL_COLOR_ARRAY) glDisableClientState(GL_COLOR_ARRAY)
glDisableClientState(GL_VERTEX_ARRAY) glDisableClientState(GL_VERTEX_ARRAY)

View File

@ -19,7 +19,7 @@ class GLMeshItem(GLGraphicsItem):
def __init__(self, **kwds): def __init__(self, **kwds):
""" """
============== ===================================================== ============== =====================================================
Arguments **Arguments:**
meshdata MeshData object from which to determine geometry for meshdata MeshData object from which to determine geometry for
this item. this item.
color Default face color used if no vertex or face colors color Default face color used if no vertex or face colors
@ -153,8 +153,12 @@ class GLMeshItem(GLGraphicsItem):
self.colors = md.faceColors(indexed='faces') self.colors = md.faceColors(indexed='faces')
if self.opts['drawEdges']: if self.opts['drawEdges']:
self.edges = md.edges() if not md.hasFaceIndexedData():
self.edgeVerts = md.vertexes() self.edges = md.edges()
self.edgeVerts = md.vertexes()
else:
self.edges = md.edges()
self.edgeVerts = md.vertexes(indexed='faces')
return return
def paint(self): def paint(self):

View File

@ -28,8 +28,7 @@ class GLScatterPlotItem(GLGraphicsItem):
colors unchanged, etc. colors unchanged, etc.
==================== ================================================== ==================== ==================================================
Arguments: **Arguments:**
------------------------------------------------------------------------
pos (N,3) array of floats specifying point locations. pos (N,3) array of floats specifying point locations.
color (N,4) array of floats (0.0-1.0) specifying color (N,4) array of floats (0.0-1.0) specifying
spot colors OR a tuple of floats specifying spot colors OR a tuple of floats specifying

View File

@ -36,14 +36,14 @@ class GLSurfacePlotItem(GLMeshItem):
""" """
Update the data in this surface plot. Update the data in this surface plot.
========== ===================================================================== ============== =====================================================================
Arguments **Arguments:**
x,y 1D arrays of values specifying the x,y positions of vertexes in the x,y 1D arrays of values specifying the x,y positions of vertexes in the
grid. If these are omitted, then the values will be assumed to be grid. If these are omitted, then the values will be assumed to be
integers. integers.
z 2D array of height values for each grid vertex. z 2D array of height values for each grid vertex.
colors (width, height, 4) array of vertex colors. colors (width, height, 4) array of vertex colors.
========== ===================================================================== ============== =====================================================================
All arguments are optional. All arguments are optional.

View File

@ -1,127 +1,127 @@
# Copyright (c) 2009 Raymond Hettinger # Copyright (c) 2009 Raymond Hettinger
# #
# Permission is hereby granted, free of charge, to any person # Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files # obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction, # (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge, # including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software, # publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so, # and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions: # subject to the following conditions:
# #
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software. # included in all copies or substantial portions of the Software.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE. # OTHER DEALINGS IN THE SOFTWARE.
from UserDict import DictMixin from UserDict import DictMixin
class OrderedDict(dict, DictMixin): class OrderedDict(dict, DictMixin):
def __init__(self, *args, **kwds): def __init__(self, *args, **kwds):
if len(args) > 1: if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d' % len(args)) raise TypeError('expected at most 1 arguments, got %d' % len(args))
try: try:
self.__end self.__end
except AttributeError: except AttributeError:
self.clear() self.clear()
self.update(*args, **kwds) self.update(*args, **kwds)
def clear(self): def clear(self):
self.__end = end = [] self.__end = end = []
end += [None, end, end] # sentinel node for doubly linked list end += [None, end, end] # sentinel node for doubly linked list
self.__map = {} # key --> [key, prev, next] self.__map = {} # key --> [key, prev, next]
dict.clear(self) dict.clear(self)
def __setitem__(self, key, value): def __setitem__(self, key, value):
if key not in self: if key not in self:
end = self.__end end = self.__end
curr = end[1] curr = end[1]
curr[2] = end[1] = self.__map[key] = [key, curr, end] curr[2] = end[1] = self.__map[key] = [key, curr, end]
dict.__setitem__(self, key, value) dict.__setitem__(self, key, value)
def __delitem__(self, key): def __delitem__(self, key):
dict.__delitem__(self, key) dict.__delitem__(self, key)
key, prev, next = self.__map.pop(key) key, prev, next = self.__map.pop(key)
prev[2] = next prev[2] = next
next[1] = prev next[1] = prev
def __iter__(self): def __iter__(self):
end = self.__end end = self.__end
curr = end[2] curr = end[2]
while curr is not end: while curr is not end:
yield curr[0] yield curr[0]
curr = curr[2] curr = curr[2]
def __reversed__(self): def __reversed__(self):
end = self.__end end = self.__end
curr = end[1] curr = end[1]
while curr is not end: while curr is not end:
yield curr[0] yield curr[0]
curr = curr[1] curr = curr[1]
def popitem(self, last=True): def popitem(self, last=True):
if not self: if not self:
raise KeyError('dictionary is empty') raise KeyError('dictionary is empty')
if last: if last:
key = reversed(self).next() key = reversed(self).next()
else: else:
key = iter(self).next() key = iter(self).next()
value = self.pop(key) value = self.pop(key)
return key, value return key, value
def __reduce__(self): def __reduce__(self):
items = [[k, self[k]] for k in self] items = [[k, self[k]] for k in self]
tmp = self.__map, self.__end tmp = self.__map, self.__end
del self.__map, self.__end del self.__map, self.__end
inst_dict = vars(self).copy() inst_dict = vars(self).copy()
self.__map, self.__end = tmp self.__map, self.__end = tmp
if inst_dict: if inst_dict:
return (self.__class__, (items,), inst_dict) return (self.__class__, (items,), inst_dict)
return self.__class__, (items,) return self.__class__, (items,)
def keys(self): def keys(self):
return list(self) return list(self)
setdefault = DictMixin.setdefault setdefault = DictMixin.setdefault
update = DictMixin.update update = DictMixin.update
pop = DictMixin.pop pop = DictMixin.pop
values = DictMixin.values values = DictMixin.values
items = DictMixin.items items = DictMixin.items
iterkeys = DictMixin.iterkeys iterkeys = DictMixin.iterkeys
itervalues = DictMixin.itervalues itervalues = DictMixin.itervalues
iteritems = DictMixin.iteritems iteritems = DictMixin.iteritems
def __repr__(self): def __repr__(self):
if not self: if not self:
return '%s()' % (self.__class__.__name__,) return '%s()' % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, self.items()) return '%s(%r)' % (self.__class__.__name__, self.items())
def copy(self): def copy(self):
return self.__class__(self) return self.__class__(self)
@classmethod @classmethod
def fromkeys(cls, iterable, value=None): def fromkeys(cls, iterable, value=None):
d = cls() d = cls()
for key in iterable: for key in iterable:
d[key] = value d[key] = value
return d return d
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, OrderedDict): if isinstance(other, OrderedDict):
if len(self) != len(other): if len(self) != len(other):
return False return False
for p, q in zip(self.items(), other.items()): for p, q in zip(self.items(), other.items()):
if p != q: if p != q:
return False return False
return True return True
return dict.__eq__(self, other) return dict.__eq__(self, other)
def __ne__(self, other): def __ne__(self, other):
return not self == other return not self == other

View File

@ -107,33 +107,33 @@ class Parameter(QtCore.QObject):
Parameter instance, the options available to this method are also allowed Parameter instance, the options available to this method are also allowed
by most Parameter subclasses. by most Parameter subclasses.
================= ========================================================= ======================= =========================================================
Keyword Arguments **Keyword Arguments:**
name The name to give this Parameter. This is the name that name The name to give this Parameter. This is the name that
will appear in the left-most column of a ParameterTree will appear in the left-most column of a ParameterTree
for this Parameter. for this Parameter.
value The value to initially assign to this Parameter. value The value to initially assign to this Parameter.
default The default value for this Parameter (most Parameters default The default value for this Parameter (most Parameters
provide an option to 'reset to default'). provide an option to 'reset to default').
children A list of children for this Parameter. Children children A list of children for this Parameter. Children
may be given either as a Parameter instance or as a may be given either as a Parameter instance or as a
dictionary to pass to Parameter.create(). In this way, dictionary to pass to Parameter.create(). In this way,
it is possible to specify complex hierarchies of it is possible to specify complex hierarchies of
Parameters from a single nested data structure. Parameters from a single nested data structure.
readonly If True, the user will not be allowed to edit this readonly If True, the user will not be allowed to edit this
Parameter. (default=False) Parameter. (default=False)
enabled If False, any widget(s) for this parameter will appear enabled If False, any widget(s) for this parameter will appear
disabled. (default=True) disabled. (default=True)
visible If False, the Parameter will not appear when displayed visible If False, the Parameter will not appear when displayed
in a ParameterTree. (default=True) in a ParameterTree. (default=True)
renamable If True, the user may rename this Parameter. renamable If True, the user may rename this Parameter.
(default=False) (default=False)
removable If True, the user may remove this Parameter. removable If True, the user may remove this Parameter.
(default=False) (default=False)
expanded If True, the Parameter will appear expanded when expanded If True, the Parameter will appear expanded when
displayed in a ParameterTree (its children will be displayed in a ParameterTree (its children will be
visible). (default=True) visible). (default=True)
================= ========================================================= ======================= =========================================================
""" """
@ -516,7 +516,7 @@ class Parameter(QtCore.QObject):
self.sigChildRemoved.emit(self, child) self.sigChildRemoved.emit(self, child)
try: try:
child.sigTreeStateChanged.disconnect(self.treeStateChanged) child.sigTreeStateChanged.disconnect(self.treeStateChanged)
except TypeError: ## already disconnected except (TypeError, RuntimeError): ## already disconnected
pass pass
def clearChildren(self): def clearChildren(self):
@ -675,13 +675,13 @@ class Parameter(QtCore.QObject):
""" """
Called when the state of any sub-parameter has changed. Called when the state of any sub-parameter has changed.
========== ================================================================ ============== ================================================================
Arguments: **Arguments:**
param The immediate child whose tree state has changed. param The immediate child whose tree state has changed.
note that the change may have originated from a grandchild. note that the change may have originated from a grandchild.
changes List of tuples describing all changes that have been made changes List of tuples describing all changes that have been made
in this event: (param, changeDescr, data) in this event: (param, changeDescr, data)
========== ================================================================ ============== ================================================================
This function can be extended to react to tree state changes. This function can be extended to react to tree state changes.
""" """

View File

@ -18,16 +18,16 @@ class WidgetParameterItem(ParameterItem):
* simple widget for editing value (displayed instead of label when item is selected) * simple widget for editing value (displayed instead of label when item is selected)
* button that resets value to default * button that resets value to default
================= ============================================================= ========================== =============================================================
Registered Types: **Registered Types:**
int Displays a :class:`SpinBox <pyqtgraph.SpinBox>` in integer int Displays a :class:`SpinBox <pyqtgraph.SpinBox>` in integer
mode. mode.
float Displays a :class:`SpinBox <pyqtgraph.SpinBox>`. float Displays a :class:`SpinBox <pyqtgraph.SpinBox>`.
bool Displays a QCheckBox bool Displays a QCheckBox
str Displays a QLineEdit str Displays a QLineEdit
color Displays a :class:`ColorButton <pyqtgraph.ColorButton>` color Displays a :class:`ColorButton <pyqtgraph.ColorButton>`
colormap Displays a :class:`GradientWidget <pyqtgraph.GradientWidget>` colormap Displays a :class:`GradientWidget <pyqtgraph.GradientWidget>`
================= ============================================================= ========================== =============================================================
This class can be subclassed by overriding makeWidget() to provide a custom widget. This class can be subclassed by overriding makeWidget() to provide a custom widget.
""" """
@ -208,12 +208,14 @@ class WidgetParameterItem(ParameterItem):
val = self.widget.value() val = self.widget.value()
newVal = self.param.setValue(val) newVal = self.param.setValue(val)
def widgetValueChanging(self): def widgetValueChanging(self, *args):
""" """
Called when the widget's value is changing, but not finalized. Called when the widget's value is changing, but not finalized.
For example: editing text before pressing enter or changing focus. For example: editing text before pressing enter or changing focus.
""" """
pass # This is a bit sketchy: assume the last argument of each signal is
# the value..
self.param.sigValueChanging.emit(self.param, args[-1])
def selected(self, sel): def selected(self, sel):
"""Called when this item has been selected (sel=True) OR deselected (sel=False)""" """Called when this item has been selected (sel=True) OR deselected (sel=False)"""

View File

@ -1,26 +1,26 @@
""" """
Allows easy loading of pixmaps used in UI elements. Allows easy loading of pixmaps used in UI elements.
Provides support for frozen environments as well. Provides support for frozen environments as well.
""" """
import os, sys, pickle import os, sys, pickle
from ..functions import makeQImage from ..functions import makeQImage
from ..Qt import QtGui from ..Qt import QtGui
if sys.version_info[0] == 2: if sys.version_info[0] == 2:
from . import pixmapData_2 as pixmapData from . import pixmapData_2 as pixmapData
else: else:
from . import pixmapData_3 as pixmapData from . import pixmapData_3 as pixmapData
def getPixmap(name): def getPixmap(name):
""" """
Return a QPixmap corresponding to the image file with the given name. Return a QPixmap corresponding to the image file with the given name.
(eg. getPixmap('auto') loads pyqtgraph/pixmaps/auto.png) (eg. getPixmap('auto') loads pyqtgraph/pixmaps/auto.png)
""" """
key = name+'.png' key = name+'.png'
data = pixmapData.pixmapData[key] data = pixmapData.pixmapData[key]
if isinstance(data, basestring) or isinstance(data, bytes): if isinstance(data, basestring) or isinstance(data, bytes):
pixmapData.pixmapData[key] = pickle.loads(data) pixmapData.pixmapData[key] = pickle.loads(data)
arr = pixmapData.pixmapData[key] arr = pixmapData.pixmapData[key]
return QtGui.QPixmap(makeQImage(arr, alpha=True)) return QtGui.QPixmap(makeQImage(arr, alpha=True))

View File

@ -1,19 +1,19 @@
import numpy as np import numpy as np
from PyQt4 import QtGui from PyQt4 import QtGui
import os, pickle, sys import os, pickle, sys
path = os.path.abspath(os.path.split(__file__)[0]) path = os.path.abspath(os.path.split(__file__)[0])
pixmaps = {} pixmaps = {}
for f in os.listdir(path): for f in os.listdir(path):
if not f.endswith('.png'): if not f.endswith('.png'):
continue continue
print(f) print(f)
img = QtGui.QImage(os.path.join(path, f)) img = QtGui.QImage(os.path.join(path, f))
ptr = img.bits() ptr = img.bits()
ptr.setsize(img.byteCount()) ptr.setsize(img.byteCount())
arr = np.asarray(ptr).reshape(img.height(), img.width(), 4).transpose(1,0,2) arr = np.asarray(ptr).reshape(img.height(), img.width(), 4).transpose(1,0,2)
pixmaps[f] = pickle.dumps(arr) pixmaps[f] = pickle.dumps(arr)
ver = sys.version_info[0] ver = sys.version_info[0]
fh = open(os.path.join(path, 'pixmapData_%d.py' %ver), 'w') fh = open(os.path.join(path, 'pixmapData_%d.py' %ver), 'w')
fh.write("import numpy as np; pixmapData=%s" % repr(pixmaps)) fh.write("import numpy as np; pixmapData=%s" % repr(pixmaps))

View File

@ -1,5 +1,5 @@
""" """
Helper functions which smooth out the differences between python 2 and 3. Helper functions that smooth out the differences between python 2 and 3.
""" """
import sys import sys

68
tests/test_functions.py Normal file
View File

@ -0,0 +1,68 @@
import pyqtgraph as pg
import numpy as np
from numpy.testing import assert_array_almost_equal, assert_almost_equal
np.random.seed(12345)
def testSolve3D():
p1 = np.array([[0,0,0,1],
[1,0,0,1],
[0,1,0,1],
[0,0,1,1]], dtype=float)
# transform points through random matrix
tr = np.random.normal(size=(4, 4))
tr[3] = (0,0,0,1)
p2 = np.dot(tr, p1.T).T[:,:3]
# solve to see if we can recover the transformation matrix.
tr2 = pg.solve3DTransform(p1, p2)
assert_array_almost_equal(tr[:3], tr2[:3])
def test_interpolateArray():
data = np.array([[ 1., 2., 4. ],
[ 10., 20., 40. ],
[ 100., 200., 400.]])
x = np.array([[ 0.3, 0.6],
[ 1. , 1. ],
[ 0.5, 1. ],
[ 0.5, 2.5],
[ 10. , 10. ]])
result = pg.interpolateArray(data, x)
#import scipy.ndimage
#spresult = scipy.ndimage.map_coordinates(data, x.T, order=1)
spresult = np.array([ 5.92, 20. , 11. , 0. , 0. ]) # generated with the above line
assert_array_almost_equal(result, spresult)
# test mapping when x.shape[-1] < data.ndim
x = np.array([[ 0.3, 0],
[ 0.3, 1],
[ 0.3, 2]])
r1 = pg.interpolateArray(data, x)
r2 = pg.interpolateArray(data, x[0,:1])
assert_array_almost_equal(r1, r2)
# test mapping 2D array of locations
x = np.array([[[0.5, 0.5], [0.5, 1.0], [0.5, 1.5]],
[[1.5, 0.5], [1.5, 1.0], [1.5, 1.5]]])
r1 = pg.interpolateArray(data, x)
#r2 = scipy.ndimage.map_coordinates(data, x.transpose(2,0,1), order=1)
r2 = np.array([[ 8.25, 11. , 16.5 ], # generated with the above line
[ 82.5 , 110. , 165. ]])
assert_array_almost_equal(r1, r2)
if __name__ == '__main__':
test_interpolateArray()

10
tests/test_qt.py Normal file
View File

@ -0,0 +1,10 @@
import pyqtgraph as pg
import gc
def test_isQObjectAlive():
o1 = pg.QtCore.QObject()
o2 = pg.QtCore.QObject()
o2.setParent(o1)
del o1
gc.collect()
assert not pg.Qt.isQObjectAlive(o2)

View File

@ -0,0 +1,39 @@
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
from numpy.testing import assert_array_almost_equal, assert_almost_equal
testPoints = np.array([
[0, 0, 0],
[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
[-1, -1, 0],
[0, -1, -1]])
def testMatrix():
"""
SRTTransform3D => Transform3D => SRTTransform3D
"""
tr = pg.SRTTransform3D()
tr.setRotate(45, (0, 0, 1))
tr.setScale(0.2, 0.4, 1)
tr.setTranslate(10, 20, 40)
assert tr.getRotation() == (45, QtGui.QVector3D(0, 0, 1))
assert tr.getScale() == QtGui.QVector3D(0.2, 0.4, 1)
assert tr.getTranslation() == QtGui.QVector3D(10, 20, 40)
tr2 = pg.Transform3D(tr)
assert np.all(tr.matrix() == tr2.matrix())
# This is the most important test:
# The transition from Transform3D to SRTTransform3D is a tricky one.
tr3 = pg.SRTTransform3D(tr2)
assert_array_almost_equal(tr.matrix(), tr3.matrix())
assert_almost_equal(tr3.getRotation()[0], tr.getRotation()[0])
assert_array_almost_equal(tr3.getRotation()[1], tr.getRotation()[1])
assert_array_almost_equal(tr3.getScale(), tr.getScale())
assert_array_almost_equal(tr3.getTranslation(), tr.getTranslation())

0
util/__init__.py Normal file
View File

28
util/colorama/LICENSE.txt Normal file
View File

@ -0,0 +1,28 @@
Copyright (c) 2010 Jonathan Hartley
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holders, nor those of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

304
util/colorama/README.txt Normal file
View File

@ -0,0 +1,304 @@
Download and docs:
http://pypi.python.org/pypi/colorama
Development:
http://code.google.com/p/colorama
Discussion group:
https://groups.google.com/forum/#!forum/python-colorama
Description
===========
Makes ANSI escape character sequences for producing colored terminal text and
cursor positioning work under MS Windows.
ANSI escape character sequences have long been used to produce colored terminal
text and cursor positioning on Unix and Macs. Colorama makes this work on
Windows, too, by wrapping stdout, stripping ANSI sequences it finds (which
otherwise show up as gobbledygook in your output), and converting them into the
appropriate win32 calls to modify the state of the terminal. On other platforms,
Colorama does nothing.
Colorama also provides some shortcuts to help generate ANSI sequences
but works fine in conjunction with any other ANSI sequence generation library,
such as Termcolor (http://pypi.python.org/pypi/termcolor.)
This has the upshot of providing a simple cross-platform API for printing
colored terminal text from Python, and has the happy side-effect that existing
applications or libraries which use ANSI sequences to produce colored output on
Linux or Macs can now also work on Windows, simply by calling
``colorama.init()``.
An alternative approach is to install 'ansi.sys' on Windows machines, which
provides the same behaviour for all applications running in terminals. Colorama
is intended for situations where that isn't easy (e.g. maybe your app doesn't
have an installer.)
Demo scripts in the source code repository prints some colored text using
ANSI sequences. Compare their output under Gnome-terminal's built in ANSI
handling, versus on Windows Command-Prompt using Colorama:
.. image:: http://colorama.googlecode.com/hg/screenshots/ubuntu-demo.png
:width: 661
:height: 357
:alt: ANSI sequences on Ubuntu under gnome-terminal.
.. image:: http://colorama.googlecode.com/hg/screenshots/windows-demo.png
:width: 668
:height: 325
:alt: Same ANSI sequences on Windows, using Colorama.
These screengrabs show that Colorama on Windows does not support ANSI 'dim
text': it looks the same as 'normal text'.
License
=======
Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
Dependencies
============
None, other than Python. Tested on Python 2.5.5, 2.6.5, 2.7, 3.1.2, and 3.2
Usage
=====
Initialisation
--------------
Applications should initialise Colorama using::
from colorama import init
init()
If you are on Windows, the call to ``init()`` will start filtering ANSI escape
sequences out of any text sent to stdout or stderr, and will replace them with
equivalent Win32 calls.
Calling ``init()`` has no effect on other platforms (unless you request other
optional functionality, see keyword args below.) The intention is that
applications can call ``init()`` unconditionally on all platforms, after which
ANSI output should just work.
To stop using colorama before your program exits, simply call ``deinit()``.
This will restore stdout and stderr to their original values, so that Colorama
is disabled. To start using Colorama again, call ``reinit()``, which wraps
stdout and stderr again, but is cheaper to call than doing ``init()`` all over
again.
Colored Output
--------------
Cross-platform printing of colored text can then be done using Colorama's
constant shorthand for ANSI escape sequences::
from colorama import Fore, Back, Style
print(Fore.RED + 'some red text')
print(Back.GREEN + 'and with a green background')
print(Style.DIM + 'and in dim text')
print(Fore.RESET + Back.RESET + Style.RESET_ALL)
print('back to normal now')
or simply by manually printing ANSI sequences from your own code::
print('/033[31m' + 'some red text')
print('/033[30m' # and reset to default color)
or Colorama can be used happily in conjunction with existing ANSI libraries
such as Termcolor::
from colorama import init
from termcolor import colored
# use Colorama to make Termcolor work on Windows too
init()
# then use Termcolor for all colored text output
print(colored('Hello, World!', 'green', 'on_red'))
Available formatting constants are::
Fore: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET.
Back: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET.
Style: DIM, NORMAL, BRIGHT, RESET_ALL
Style.RESET_ALL resets foreground, background and brightness. Colorama will
perform this reset automatically on program exit.
Cursor Positioning
------------------
ANSI codes to reposition the cursor are supported. See demos/demo06.py for
an example of how to generate them.
Init Keyword Args
-----------------
``init()`` accepts some kwargs to override default behaviour.
init(autoreset=False):
If you find yourself repeatedly sending reset sequences to turn off color
changes at the end of every print, then ``init(autoreset=True)`` will
automate that::
from colorama import init
init(autoreset=True)
print(Fore.RED + 'some red text')
print('automatically back to default color again')
init(strip=None):
Pass ``True`` or ``False`` to override whether ansi codes should be
stripped from the output. The default behaviour is to strip if on Windows.
init(convert=None):
Pass ``True`` or ``False`` to override whether to convert ansi codes in the
output into win32 calls. The default behaviour is to convert if on Windows
and output is to a tty (terminal).
init(wrap=True):
On Windows, colorama works by replacing ``sys.stdout`` and ``sys.stderr``
with proxy objects, which override the .write() method to do their work. If
this wrapping causes you problems, then this can be disabled by passing
``init(wrap=False)``. The default behaviour is to wrap if autoreset or
strip or convert are True.
When wrapping is disabled, colored printing on non-Windows platforms will
continue to work as normal. To do cross-platform colored output, you can
use Colorama's ``AnsiToWin32`` proxy directly::
import sys
from colorama import init, AnsiToWin32
init(wrap=False)
stream = AnsiToWin32(sys.stderr).stream
# Python 2
print >>stream, Fore.BLUE + 'blue text on stderr'
# Python 3
print(Fore.BLUE + 'blue text on stderr', file=stream)
Status & Known Problems
=======================
I've personally only tested it on WinXP (CMD, Console2), Ubuntu
(gnome-terminal, xterm), and OSX.
Some presumably valid ANSI sequences aren't recognised (see details below)
but to my knowledge nobody has yet complained about this. Puzzling.
See outstanding issues and wishlist at:
http://code.google.com/p/colorama/issues/list
If anything doesn't work for you, or doesn't do what you expected or hoped for,
I'd love to hear about it on that issues list, would be delighted by patches,
and would be happy to grant commit access to anyone who submits a working patch
or two.
Recognised ANSI Sequences
=========================
ANSI sequences generally take the form:
ESC [ <param> ; <param> ... <command>
Where <param> is an integer, and <command> is a single letter. Zero or more
params are passed to a <command>. If no params are passed, it is generally
synonymous with passing a single zero. No spaces exist in the sequence, they
have just been inserted here to make it easy to read.
The only ANSI sequences that colorama converts into win32 calls are::
ESC [ 0 m # reset all (colors and brightness)
ESC [ 1 m # bright
ESC [ 2 m # dim (looks same as normal brightness)
ESC [ 22 m # normal brightness
# FOREGROUND:
ESC [ 30 m # black
ESC [ 31 m # red
ESC [ 32 m # green
ESC [ 33 m # yellow
ESC [ 34 m # blue
ESC [ 35 m # magenta
ESC [ 36 m # cyan
ESC [ 37 m # white
ESC [ 39 m # reset
# BACKGROUND
ESC [ 40 m # black
ESC [ 41 m # red
ESC [ 42 m # green
ESC [ 43 m # yellow
ESC [ 44 m # blue
ESC [ 45 m # magenta
ESC [ 46 m # cyan
ESC [ 47 m # white
ESC [ 49 m # reset
# cursor positioning
ESC [ y;x H # position cursor at x across, y down
# clear the screen
ESC [ mode J # clear the screen. Only mode 2 (clear entire screen)
# is supported. It should be easy to add other modes,
# let me know if that would be useful.
Multiple numeric params to the 'm' command can be combined into a single
sequence, eg::
ESC [ 36 ; 45 ; 1 m # bright cyan text on magenta background
All other ANSI sequences of the form ``ESC [ <param> ; <param> ... <command>``
are silently stripped from the output on Windows.
Any other form of ANSI sequence, such as single-character codes or alternative
initial characters, are not recognised nor stripped. It would be cool to add
them though. Let me know if it would be useful for you, via the issues on
google code.
Development
===========
Help and fixes welcome! Ask Jonathan for commit rights, you'll get them.
Running tests requires:
- Michael Foord's 'mock' module to be installed.
- Tests are written using the 2010 era updates to 'unittest', and require to
be run either using Python2.7 or greater, or else to have Michael Foord's
'unittest2' module installed.
unittest2 test discovery doesn't work for colorama, so I use 'nose'::
nosetests -s
The -s is required because 'nosetests' otherwise applies a proxy of its own to
stdout, which confuses the unit tests.
Contact
=======
Created by Jonathan Hartley, tartley@tartley.com
Thanks
======
| Ben Hoyt, for a magnificent fix under 64-bit Windows.
| Jesse@EmptySquare for submitting a fix for examples in the README.
| User 'jamessp', an observant documentation fix for cursor positioning.
| User 'vaal1239', Dave Mckee & Lackner Kristof for a tiny but much-needed Win7 fix.
| Julien Stuyck, for wisely suggesting Python3 compatible updates to README.
| Daniel Griffith for multiple fabulous patches.
| Oscar Lesta for valuable fix to stop ANSI chars being sent to non-tty output.
| Roger Binns, for many suggestions, valuable feedback, & bug reports.
| Tim Golden for thought and much appreciated feedback on the initial idea.

View File

137
util/colorama/win32.py Normal file
View File

@ -0,0 +1,137 @@
# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
# from winbase.h
STDOUT = -11
STDERR = -12
try:
from ctypes import windll
from ctypes import wintypes
except ImportError:
windll = None
SetConsoleTextAttribute = lambda *_: None
else:
from ctypes import (
byref, Structure, c_char, c_short, c_int, c_uint32, c_ushort, c_void_p, POINTER
)
class CONSOLE_SCREEN_BUFFER_INFO(Structure):
"""struct in wincon.h."""
_fields_ = [
("dwSize", wintypes._COORD),
("dwCursorPosition", wintypes._COORD),
("wAttributes", wintypes.WORD),
("srWindow", wintypes.SMALL_RECT),
("dwMaximumWindowSize", wintypes._COORD),
]
def __str__(self):
return '(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d)' % (
self.dwSize.Y, self.dwSize.X
, self.dwCursorPosition.Y, self.dwCursorPosition.X
, self.wAttributes
, self.srWindow.Top, self.srWindow.Left, self.srWindow.Bottom, self.srWindow.Right
, self.dwMaximumWindowSize.Y, self.dwMaximumWindowSize.X
)
_GetStdHandle = windll.kernel32.GetStdHandle
_GetStdHandle.argtypes = [
wintypes.DWORD,
]
_GetStdHandle.restype = wintypes.HANDLE
_GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo
_GetConsoleScreenBufferInfo.argtypes = [
wintypes.HANDLE,
c_void_p,
#POINTER(CONSOLE_SCREEN_BUFFER_INFO),
]
_GetConsoleScreenBufferInfo.restype = wintypes.BOOL
_SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute
_SetConsoleTextAttribute.argtypes = [
wintypes.HANDLE,
wintypes.WORD,
]
_SetConsoleTextAttribute.restype = wintypes.BOOL
_SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition
_SetConsoleCursorPosition.argtypes = [
wintypes.HANDLE,
c_int,
#wintypes._COORD,
]
_SetConsoleCursorPosition.restype = wintypes.BOOL
_FillConsoleOutputCharacterA = windll.kernel32.FillConsoleOutputCharacterA
_FillConsoleOutputCharacterA.argtypes = [
wintypes.HANDLE,
c_char,
wintypes.DWORD,
wintypes._COORD,
POINTER(wintypes.DWORD),
]
_FillConsoleOutputCharacterA.restype = wintypes.BOOL
_FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute
_FillConsoleOutputAttribute.argtypes = [
wintypes.HANDLE,
wintypes.WORD,
wintypes.DWORD,
c_int,
#wintypes._COORD,
POINTER(wintypes.DWORD),
]
_FillConsoleOutputAttribute.restype = wintypes.BOOL
handles = {
STDOUT: _GetStdHandle(STDOUT),
STDERR: _GetStdHandle(STDERR),
}
def GetConsoleScreenBufferInfo(stream_id=STDOUT):
handle = handles[stream_id]
csbi = CONSOLE_SCREEN_BUFFER_INFO()
success = _GetConsoleScreenBufferInfo(
handle, byref(csbi))
return csbi
def SetConsoleTextAttribute(stream_id, attrs):
handle = handles[stream_id]
return _SetConsoleTextAttribute(handle, attrs)
def SetConsoleCursorPosition(stream_id, position):
position = wintypes._COORD(*position)
# If the position is out of range, do nothing.
if position.Y <= 0 or position.X <= 0:
return
# Adjust for Windows' SetConsoleCursorPosition:
# 1. being 0-based, while ANSI is 1-based.
# 2. expecting (x,y), while ANSI uses (y,x).
adjusted_position = wintypes._COORD(position.Y - 1, position.X - 1)
# Adjust for viewport's scroll position
sr = GetConsoleScreenBufferInfo(STDOUT).srWindow
adjusted_position.Y += sr.Top
adjusted_position.X += sr.Left
# Resume normal processing
handle = handles[stream_id]
return _SetConsoleCursorPosition(handle, adjusted_position)
def FillConsoleOutputCharacter(stream_id, char, length, start):
handle = handles[stream_id]
char = c_char(char)
length = wintypes.DWORD(length)
num_written = wintypes.DWORD(0)
# Note that this is hard-coded for ANSI (vs wide) bytes.
success = _FillConsoleOutputCharacterA(
handle, char, length, start, byref(num_written))
return num_written.value
def FillConsoleOutputAttribute(stream_id, attr, length, start):
''' FillConsoleOutputAttribute( hConsole, csbi.wAttributes, dwConSize, coordScreen, &cCharsWritten )'''
handle = handles[stream_id]
attribute = wintypes.WORD(attr)
length = wintypes.DWORD(length)
num_written = wintypes.DWORD(0)
# Note that this is hard-coded for ANSI (vs wide) bytes.
return _FillConsoleOutputAttribute(
handle, attribute, length, start, byref(num_written))

120
util/colorama/winterm.py Normal file
View File

@ -0,0 +1,120 @@
# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
from . import win32
# from wincon.h
class WinColor(object):
BLACK = 0
BLUE = 1
GREEN = 2
CYAN = 3
RED = 4
MAGENTA = 5
YELLOW = 6
GREY = 7
# from wincon.h
class WinStyle(object):
NORMAL = 0x00 # dim text, dim background
BRIGHT = 0x08 # bright text, dim background
class WinTerm(object):
def __init__(self):
self._default = win32.GetConsoleScreenBufferInfo(win32.STDOUT).wAttributes
self.set_attrs(self._default)
self._default_fore = self._fore
self._default_back = self._back
self._default_style = self._style
def get_attrs(self):
return self._fore + self._back * 16 + self._style
def set_attrs(self, value):
self._fore = value & 7
self._back = (value >> 4) & 7
self._style = value & WinStyle.BRIGHT
def reset_all(self, on_stderr=None):
self.set_attrs(self._default)
self.set_console(attrs=self._default)
def fore(self, fore=None, on_stderr=False):
if fore is None:
fore = self._default_fore
self._fore = fore
self.set_console(on_stderr=on_stderr)
def back(self, back=None, on_stderr=False):
if back is None:
back = self._default_back
self._back = back
self.set_console(on_stderr=on_stderr)
def style(self, style=None, on_stderr=False):
if style is None:
style = self._default_style
self._style = style
self.set_console(on_stderr=on_stderr)
def set_console(self, attrs=None, on_stderr=False):
if attrs is None:
attrs = self.get_attrs()
handle = win32.STDOUT
if on_stderr:
handle = win32.STDERR
win32.SetConsoleTextAttribute(handle, attrs)
def get_position(self, handle):
position = win32.GetConsoleScreenBufferInfo(handle).dwCursorPosition
# Because Windows coordinates are 0-based,
# and win32.SetConsoleCursorPosition expects 1-based.
position.X += 1
position.Y += 1
return position
def set_cursor_position(self, position=None, on_stderr=False):
if position is None:
#I'm not currently tracking the position, so there is no default.
#position = self.get_position()
return
handle = win32.STDOUT
if on_stderr:
handle = win32.STDERR
win32.SetConsoleCursorPosition(handle, position)
def cursor_up(self, num_rows=0, on_stderr=False):
if num_rows == 0:
return
handle = win32.STDOUT
if on_stderr:
handle = win32.STDERR
position = self.get_position(handle)
adjusted_position = (position.Y - num_rows, position.X)
self.set_cursor_position(adjusted_position, on_stderr)
def erase_data(self, mode=0, on_stderr=False):
# 0 (or None) should clear from the cursor to the end of the screen.
# 1 should clear from the cursor to the beginning of the screen.
# 2 should clear the entire screen. (And maybe move cursor to (1,1)?)
#
# At the moment, I only support mode 2. From looking at the API, it
# should be possible to calculate a different number of bytes to clear,
# and to do so relative to the cursor position.
if mode[0] not in (2,):
return
handle = win32.STDOUT
if on_stderr:
handle = win32.STDERR
# here's where we'll home the cursor
coord_screen = win32.COORD(0,0)
csbi = win32.GetConsoleScreenBufferInfo(handle)
# get the number of character cells in the current buffer
dw_con_size = csbi.dwSize.X * csbi.dwSize.Y
# fill the entire screen with blanks
win32.FillConsoleOutputCharacter(handle, ' ', dw_con_size, coord_screen)
# now set the buffer's attributes accordingly
win32.FillConsoleOutputAttribute(handle, self.get_attrs(), dw_con_size, coord_screen )
# put the cursor at (0, 0)
win32.SetConsoleCursorPosition(handle, (coord_screen.X, coord_screen.Y))

101
util/cprint.py Normal file
View File

@ -0,0 +1,101 @@
"""
Cross-platform color text printing
Based on colorama (see pyqtgraph/util/colorama/README.txt)
"""
import sys, re
from .colorama.winterm import WinTerm, WinColor, WinStyle
from .colorama.win32 import windll
_WIN = sys.platform.startswith('win')
if windll is not None:
winterm = WinTerm()
else:
_WIN = False
def winset(reset=False, fore=None, back=None, style=None, stderr=False):
if reset:
winterm.reset_all()
if fore is not None:
winterm.fore(fore, stderr)
if back is not None:
winterm.back(back, stderr)
if style is not None:
winterm.style(style, stderr)
ANSI = {}
WIN = {}
for i,color in enumerate(['BLACK', 'RED', 'GREEN', 'YELLOW', 'BLUE', 'MAGENTA', 'CYAN', 'WHITE']):
globals()[color] = i
globals()['BR_' + color] = i + 8
globals()['BACK_' + color] = i + 40
ANSI[i] = "\033[%dm" % (30+i)
ANSI[i+8] = "\033[2;%dm" % (30+i)
ANSI[i+40] = "\033[%dm" % (40+i)
color = 'GREY' if color == 'WHITE' else color
WIN[i] = {'fore': getattr(WinColor, color), 'style': WinStyle.NORMAL}
WIN[i+8] = {'fore': getattr(WinColor, color), 'style': WinStyle.BRIGHT}
WIN[i+40] = {'back': getattr(WinColor, color)}
RESET = -1
ANSI[RESET] = "\033[0m"
WIN[RESET] = {'reset': True}
def cprint(stream, *args, **kwds):
"""
Print with color. Examples::
# colors are BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE
cprint('stdout', RED, 'This is in red. ', RESET, 'and this is normal\n')
# Adding BR_ before the color manes it bright
cprint('stdout', BR_GREEN, 'This is bright green.\n', RESET)
# Adding BACK_ changes background color
cprint('stderr', BACK_BLUE, WHITE, 'This is white-on-blue.', -1)
# Integers 0-7 for normal, 8-15 for bright, and 40-47 for background.
# -1 to reset.
cprint('stderr', 1, 'This is in red.', -1)
"""
if isinstance(stream, basestring):
stream = kwds.get('stream', 'stdout')
err = stream == 'stderr'
stream = getattr(sys, stream)
else:
err = kwds.get('stderr', False)
if hasattr(stream, 'isatty') and stream.isatty():
if _WIN:
# convert to win32 calls
for arg in args:
if isinstance(arg, basestring):
stream.write(arg)
else:
kwds = WIN[arg]
winset(stderr=err, **kwds)
else:
# convert to ANSI
for arg in args:
if isinstance(arg, basestring):
stream.write(arg)
else:
stream.write(ANSI[arg])
else:
# ignore colors
for arg in args:
if isinstance(arg, basestring):
stream.write(arg)
def cout(*args):
"""Shorthand for cprint('stdout', ...)"""
cprint('stdout', *args)
def cerr(*args):
"""Shorthand for cprint('stderr', ...)"""
cprint('stderr', *args)

121
util/lru_cache.py Normal file
View File

@ -0,0 +1,121 @@
import operator
import sys
import itertools
_IS_PY3 = sys.version_info[0] == 3
class LRUCache(object):
'''
This LRU cache should be reasonable for short collections (until around 100 items), as it does a
sort on the items if the collection would become too big (so, it is very fast for getting and
setting but when its size would become higher than the max size it does one sort based on the
internal time to decide which items should be removed -- which should be Ok if the resizeTo
isn't too close to the maxSize so that it becomes an operation that doesn't happen all the
time).
'''
def __init__(self, maxSize=100, resizeTo=70):
'''
============== =========================================================
**Arguments:**
maxSize (int) This is the maximum size of the cache. When some
item is added and the cache would become bigger than
this, it's resized to the value passed on resizeTo.
resizeTo (int) When a resize operation happens, this is the size
of the final cache.
============== =========================================================
'''
assert resizeTo < maxSize
self.maxSize = maxSize
self.resizeTo = resizeTo
self._counter = 0
self._dict = {}
if _IS_PY3:
self._nextTime = itertools.count(0).__next__
else:
self._nextTime = itertools.count(0).next
def __getitem__(self, key):
item = self._dict[key]
item[2] = self._nextTime()
return item[1]
def __len__(self):
return len(self._dict)
def __setitem__(self, key, value):
item = self._dict.get(key)
if item is None:
if len(self._dict) + 1 > self.maxSize:
self._resizeTo()
item = [key, value, self._nextTime()]
self._dict[key] = item
else:
item[1] = value
item[2] = self._nextTime()
def __delitem__(self, key):
del self._dict[key]
def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default
def clear(self):
self._dict.clear()
if _IS_PY3:
def values(self):
return [i[1] for i in self._dict.values()]
def keys(self):
return [x[0] for x in self._dict.values()]
def _resizeTo(self):
ordered = sorted(self._dict.values(), key=operator.itemgetter(2))[:self.resizeTo]
for i in ordered:
del self._dict[i[0]]
def iteritems(self, accessTime=False):
'''
:param bool accessTime:
If True sorts the returned items by the internal access time.
'''
if accessTime:
for x in sorted(self._dict.values(), key=operator.itemgetter(2)):
yield x[0], x[1]
else:
for x in self._dict.items():
yield x[0], x[1]
else:
def values(self):
return [i[1] for i in self._dict.itervalues()]
def keys(self):
return [x[0] for x in self._dict.itervalues()]
def _resizeTo(self):
ordered = sorted(self._dict.itervalues(), key=operator.itemgetter(2))[:self.resizeTo]
for i in ordered:
del self._dict[i[0]]
def iteritems(self, accessTime=False):
'''
============= ======================================================
**Arguments**
accessTime (bool) If True sorts the returned items by the
internal access time.
============= ======================================================
'''
if accessTime:
for x in sorted(self._dict.itervalues(), key=operator.itemgetter(2)):
yield x[0], x[1]
else:
for x in self._dict.iteritems():
yield x[0], x[1]

94
util/mutex.py Normal file
View File

@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
from ..Qt import QtCore
import traceback
class Mutex(QtCore.QMutex):
"""
Subclass of QMutex that provides useful debugging information during
deadlocks--tracebacks are printed for both the code location that is
attempting to lock the mutex as well as the location that has already
acquired the lock.
Also provides __enter__ and __exit__ methods for use in "with" statements.
"""
def __init__(self, *args, **kargs):
if kargs.get('recursive', False):
args = (QtCore.QMutex.Recursive,)
QtCore.QMutex.__init__(self, *args)
self.l = QtCore.QMutex() ## for serializing access to self.tb
self.tb = []
self.debug = True ## True to enable debugging functions
def tryLock(self, timeout=None, id=None):
if timeout is None:
locked = QtCore.QMutex.tryLock(self)
else:
locked = QtCore.QMutex.tryLock(self, timeout)
if self.debug and locked:
self.l.lock()
try:
if id is None:
self.tb.append(''.join(traceback.format_stack()[:-1]))
else:
self.tb.append(" " + str(id))
#print 'trylock', self, len(self.tb)
finally:
self.l.unlock()
return locked
def lock(self, id=None):
c = 0
waitTime = 5000 # in ms
while True:
if self.tryLock(waitTime, id):
break
c += 1
if self.debug:
self.l.lock()
try:
print("Waiting for mutex lock (%0.1f sec). Traceback follows:"
% (c*waitTime/1000.))
traceback.print_stack()
if len(self.tb) > 0:
print("Mutex is currently locked from:\n")
print(self.tb[-1])
else:
print("Mutex is currently locked from [???]")
finally:
self.l.unlock()
#print 'lock', self, len(self.tb)
def unlock(self):
QtCore.QMutex.unlock(self)
if self.debug:
self.l.lock()
try:
#print 'unlock', self, len(self.tb)
if len(self.tb) > 0:
self.tb.pop()
else:
raise Exception("Attempt to unlock mutex before it has been locked")
finally:
self.l.unlock()
def depth(self):
self.l.lock()
n = len(self.tb)
self.l.unlock()
return n
def traceback(self):
self.l.lock()
try:
ret = self.tb[:]
finally:
self.l.unlock()
return ret
def __exit__(self, *args):
self.unlock()
def __enter__(self):
self.lock()
return self

View File

@ -0,0 +1,50 @@
from pyqtgraph.util.lru_cache import LRUCache
def testLRU():
lru = LRUCache(2, 1)
# check twice
checkLru(lru)
checkLru(lru)
def checkLru(lru):
lru[1] = 1
lru[2] = 2
lru[3] = 3
assert len(lru) == 2
assert set([2, 3]) == set(lru.keys())
assert set([2, 3]) == set(lru.values())
lru[2] = 2
assert set([2, 3]) == set(lru.values())
lru[1] = 1
set([2, 1]) == set(lru.values())
#Iterates from the used in the last access to others based on access time.
assert [(2, 2), (1, 1)] == list(lru.iteritems(accessTime=True))
lru[2] = 2
assert [(1, 1), (2, 2)] == list(lru.iteritems(accessTime=True))
del lru[2]
assert [(1, 1), ] == list(lru.iteritems(accessTime=True))
lru[2] = 2
assert [(1, 1), (2, 2)] == list(lru.iteritems(accessTime=True))
_a = lru[1]
assert [(2, 2), (1, 1)] == list(lru.iteritems(accessTime=True))
_a = lru[2]
assert [(1, 1), (2, 2)] == list(lru.iteritems(accessTime=True))
assert lru.get(2) == 2
assert lru.get(3) == None
assert [(1, 1), (2, 2)] == list(lru.iteritems(accessTime=True))
lru.clear()
assert [] == list(lru.iteritems())
if __name__ == '__main__':
testLRU()

View File

@ -11,7 +11,7 @@ class ColorButton(QtGui.QPushButton):
Button displaying a color and allowing the user to select a new color. Button displaying a color and allowing the user to select a new color.
====================== ============================================================ ====================== ============================================================
**Signals**: **Signals:**
sigColorChanging(self) emitted whenever a new color is picked in the color dialog sigColorChanging(self) emitted whenever a new color is picked in the color dialog
sigColorChanged(self) emitted when the selected color is accepted (user clicks OK) sigColorChanged(self) emitted when the selected color is accepted (user clicks OK)
====================== ============================================================ ====================== ============================================================

View File

@ -86,14 +86,14 @@ class ColorMapParameter(ptree.types.GroupParameter):
""" """
Return an array of colors corresponding to *data*. Return an array of colors corresponding to *data*.
========= ================================================================= ============== =================================================================
Arguments **Arguments:**
data A numpy record array where the fields in data.dtype match those data A numpy record array where the fields in data.dtype match those
defined by a prior call to setFields(). defined by a prior call to setFields().
mode Either 'byte' or 'float'. For 'byte', the method returns an array mode Either 'byte' or 'float'. For 'byte', the method returns an array
of dtype ubyte with values scaled 0-255. For 'float', colors are of dtype ubyte with values scaled 0-255. For 'float', colors are
returned as 0.0-1.0 float values. returned as 0.0-1.0 float values.
========= ================================================================= ============== =================================================================
""" """
colors = np.zeros((len(data),4)) colors = np.zeros((len(data),4))
for item in self.children(): for item in self.children():

View File

@ -4,9 +4,27 @@ from .GraphicsView import GraphicsView
__all__ = ['GraphicsLayoutWidget'] __all__ = ['GraphicsLayoutWidget']
class GraphicsLayoutWidget(GraphicsView): class GraphicsLayoutWidget(GraphicsView):
"""
Convenience class consisting of a :class:`GraphicsView
<pyqtgraph.GraphicsView>` with a single :class:`GraphicsLayout
<pyqtgraph.GraphicsLayout>` as its central item.
This class wraps several methods from its internal GraphicsLayout:
:func:`nextRow <pyqtgraph.GraphicsLayout.nextRow>`
:func:`nextColumn <pyqtgraph.GraphicsLayout.nextColumn>`
:func:`addPlot <pyqtgraph.GraphicsLayout.addPlot>`
:func:`addViewBox <pyqtgraph.GraphicsLayout.addViewBox>`
:func:`addItem <pyqtgraph.GraphicsLayout.addItem>`
:func:`getItem <pyqtgraph.GraphicsLayout.getItem>`
:func:`addLabel <pyqtgraph.GraphicsLayout.addLabel>`
:func:`addLayout <pyqtgraph.GraphicsLayout.addLayout>`
:func:`removeItem <pyqtgraph.GraphicsLayout.removeItem>`
:func:`itemIndex <pyqtgraph.GraphicsLayout.itemIndex>`
:func:`clear <pyqtgraph.GraphicsLayout.clear>`
"""
def __init__(self, parent=None, **kargs): def __init__(self, parent=None, **kargs):
GraphicsView.__init__(self, parent) GraphicsView.__init__(self, parent)
self.ci = GraphicsLayout(**kargs) self.ci = GraphicsLayout(**kargs)
for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLabel', 'addLayout', 'addLabel', 'addViewBox', 'removeItem', 'itemIndex', 'clear']: for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLayout', 'addLabel', 'removeItem', 'itemIndex', 'clear']:
setattr(self, n, getattr(self.ci, n)) setattr(self, n, getattr(self.ci, n))
self.setCentralItem(self.ci) self.setCentralItem(self.ci)

View File

@ -40,8 +40,8 @@ class GraphicsView(QtGui.QGraphicsView):
The view can be panned using the middle mouse button and scaled using the right mouse button if The view can be panned using the middle mouse button and scaled using the right mouse button if
enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality).""" enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality)."""
sigRangeChanged = QtCore.Signal(object, object) sigDeviceRangeChanged = QtCore.Signal(object, object)
sigTransformChanged = QtCore.Signal(object) sigDeviceTransformChanged = QtCore.Signal(object)
sigMouseReleased = QtCore.Signal(object) sigMouseReleased = QtCore.Signal(object)
sigSceneMouseMoved = QtCore.Signal(object) sigSceneMouseMoved = QtCore.Signal(object)
#sigRegionChanged = QtCore.Signal(object) #sigRegionChanged = QtCore.Signal(object)
@ -50,21 +50,21 @@ class GraphicsView(QtGui.QGraphicsView):
def __init__(self, parent=None, useOpenGL=None, background='default'): def __init__(self, parent=None, useOpenGL=None, background='default'):
""" """
============ ============================================================ ============== ============================================================
Arguments: **Arguments:**
parent Optional parent widget parent Optional parent widget
useOpenGL If True, the GraphicsView will use OpenGL to do all of its useOpenGL If True, the GraphicsView will use OpenGL to do all of its
rendering. This can improve performance on some systems, rendering. This can improve performance on some systems,
but may also introduce bugs (the combination of but may also introduce bugs (the combination of
QGraphicsView and QGLWidget is still an 'experimental' QGraphicsView and QGLWidget is still an 'experimental'
feature of Qt) feature of Qt)
background Set the background color of the GraphicsView. Accepts any background Set the background color of the GraphicsView. Accepts any
single argument accepted by single argument accepted by
:func:`mkColor <pyqtgraph.mkColor>`. By :func:`mkColor <pyqtgraph.mkColor>`. By
default, the background color is determined using the default, the background color is determined using the
'backgroundColor' configuration option (see 'backgroundColor' configuration option (see
:func:`setConfigOption <pyqtgraph.setConfigOption>`. :func:`setConfigOption <pyqtgraph.setConfigOption>`.
============ ============================================================ ============== ============================================================
""" """
self.closed = False self.closed = False
@ -219,8 +219,8 @@ class GraphicsView(QtGui.QGraphicsView):
else: else:
self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio) self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio)
self.sigRangeChanged.emit(self, self.range) self.sigDeviceRangeChanged.emit(self, self.range)
self.sigTransformChanged.emit(self) self.sigDeviceTransformChanged.emit(self)
if propagate: if propagate:
for v in self.lockedViewports: for v in self.lockedViewports:
@ -287,7 +287,7 @@ class GraphicsView(QtGui.QGraphicsView):
image.setPxMode(True) image.setPxMode(True)
try: try:
self.sigScaleChanged.disconnect(image.setScaledMode) self.sigScaleChanged.disconnect(image.setScaledMode)
except TypeError: except (TypeError, RuntimeError):
pass pass
tl = image.sceneBoundingRect().topLeft() tl = image.sceneBoundingRect().topLeft()
w = self.size().width() * pxSize[0] w = self.size().width() * pxSize[0]
@ -368,14 +368,14 @@ class GraphicsView(QtGui.QGraphicsView):
delta = Point(np.clip(delta[0], -50, 50), np.clip(-delta[1], -50, 50)) delta = Point(np.clip(delta[0], -50, 50), np.clip(-delta[1], -50, 50))
scale = 1.01 ** delta scale = 1.01 ** delta
self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos)) self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos))
self.sigRangeChanged.emit(self, self.range) self.sigDeviceRangeChanged.emit(self, self.range)
elif ev.buttons() in [QtCore.Qt.MidButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button. elif ev.buttons() in [QtCore.Qt.MidButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button.
px = self.pixelSize() px = self.pixelSize()
tr = -delta * px tr = -delta * px
self.translate(tr[0], tr[1]) self.translate(tr[0], tr[1])
self.sigRangeChanged.emit(self, self.range) self.sigDeviceRangeChanged.emit(self, self.range)
def pixelSize(self): def pixelSize(self):
"""Return vector with the length and width of one view pixel in scene coordinates""" """Return vector with the length and width of one view pixel in scene coordinates"""

View File

@ -4,28 +4,43 @@ MultiPlotWidget.py - Convenience class--GraphicsView widget displaying a MultiP
Copyright 2010 Luke Campagnola Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation. Distributed under MIT/X11 license. See license.txt for more infomation.
""" """
from ..Qt import QtCore
from .GraphicsView import GraphicsView from .GraphicsView import GraphicsView
from ..graphicsItems import MultiPlotItem as MultiPlotItem from ..graphicsItems import MultiPlotItem as MultiPlotItem
__all__ = ['MultiPlotWidget'] __all__ = ['MultiPlotWidget']
class MultiPlotWidget(GraphicsView): class MultiPlotWidget(GraphicsView):
"""Widget implementing a graphicsView with a single PlotItem inside.""" """Widget implementing a graphicsView with a single MultiPlotItem inside."""
def __init__(self, parent=None): def __init__(self, parent=None):
self.minPlotHeight = 50
self.mPlotItem = MultiPlotItem.MultiPlotItem()
GraphicsView.__init__(self, parent) GraphicsView.__init__(self, parent)
self.enableMouse(False) self.enableMouse(False)
self.mPlotItem = MultiPlotItem.MultiPlotItem()
self.setCentralItem(self.mPlotItem) self.setCentralItem(self.mPlotItem)
## Explicitly wrap methods from mPlotItem ## Explicitly wrap methods from mPlotItem
#for m in ['setData']: #for m in ['setData']:
#setattr(self, m, getattr(self.mPlotItem, m)) #setattr(self, m, getattr(self.mPlotItem, m))
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
def __getattr__(self, attr): ## implicitly wrap methods from plotItem def __getattr__(self, attr): ## implicitly wrap methods from plotItem
if hasattr(self.mPlotItem, attr): if hasattr(self.mPlotItem, attr):
m = getattr(self.mPlotItem, attr) m = getattr(self.mPlotItem, attr)
if hasattr(m, '__call__'): if hasattr(m, '__call__'):
return m return m
raise NameError(attr) raise AttributeError(attr)
def setMinimumPlotHeight(self, min):
"""Set the minimum height for each sub-plot displayed.
If the total height of all plots is greater than the height of the
widget, then a scroll bar will appear to provide access to the entire
set of plots.
Added in version 0.9.9
"""
self.minPlotHeight = min
self.resizeEvent(None)
def widgetGroupInterface(self): def widgetGroupInterface(self):
return (None, MultiPlotWidget.saveState, MultiPlotWidget.restoreState) return (None, MultiPlotWidget.saveState, MultiPlotWidget.restoreState)
@ -43,3 +58,21 @@ class MultiPlotWidget(GraphicsView):
self.mPlotItem = None self.mPlotItem = None
self.setParent(None) self.setParent(None)
GraphicsView.close(self) GraphicsView.close(self)
def setRange(self, *args, **kwds):
GraphicsView.setRange(self, *args, **kwds)
if self.centralWidget is not None:
r = self.range
minHeight = len(self.mPlotItem.plots) * self.minPlotHeight
if r.height() < minHeight:
r.setHeight(minHeight)
r.setWidth(r.width() - self.verticalScrollBar().width())
self.centralWidget.setGeometry(r)
def resizeEvent(self, ev):
if self.closed:
return
if self.autoPixelRange:
self.range = QtCore.QRectF(0, 0, self.size().width(), self.size().height())
MultiPlotWidget.setRange(self, self.range, padding=0, disableAutoPixel=False) ## we do this because some subclasses like to redefine setRange in an incompatible way.
self.updateMatrix()

View File

@ -23,8 +23,8 @@ class PathButton(QtGui.QPushButton):
def setBrush(self, brush): def setBrush(self, brush):
self.brush = fn.mkBrush(brush) self.brush = fn.mkBrush(brush)
def setPen(self, pen): def setPen(self, *args, **kwargs):
self.pen = fn.mkPen(pen) self.pen = fn.mkPen(*args, **kwargs)
def setPath(self, path): def setPath(self, path):
self.path = path self.path = path
@ -46,6 +46,5 @@ class PathButton(QtGui.QPushButton):
p.setBrush(self.brush) p.setBrush(self.brush)
p.drawPath(self.path) p.drawPath(self.path)
p.end() p.end()

View File

@ -12,7 +12,9 @@ from ..graphicsItems.PlotItem import *
__all__ = ['PlotWidget'] __all__ = ['PlotWidget']
class PlotWidget(GraphicsView): class PlotWidget(GraphicsView):
#sigRangeChanged = QtCore.Signal(object, object) ## already defined in GraphicsView # signals wrapped from PlotItem / ViewBox
sigRangeChanged = QtCore.Signal(object, object)
sigTransformChanged = QtCore.Signal(object)
""" """
:class:`GraphicsView <pyqtgraph.GraphicsView>` widget with a single :class:`GraphicsView <pyqtgraph.GraphicsView>` widget with a single
@ -33,6 +35,7 @@ class PlotWidget(GraphicsView):
:func:`enableAutoRange <pyqtgraph.ViewBox.enableAutoRange>`, :func:`enableAutoRange <pyqtgraph.ViewBox.enableAutoRange>`,
:func:`disableAutoRange <pyqtgraph.ViewBox.disableAutoRange>`, :func:`disableAutoRange <pyqtgraph.ViewBox.disableAutoRange>`,
:func:`setAspectLocked <pyqtgraph.ViewBox.setAspectLocked>`, :func:`setAspectLocked <pyqtgraph.ViewBox.setAspectLocked>`,
:func:`setLimits <pyqtgraph.ViewBox.setLimits>`,
:func:`register <pyqtgraph.ViewBox.register>`, :func:`register <pyqtgraph.ViewBox.register>`,
:func:`unregister <pyqtgraph.ViewBox.unregister>` :func:`unregister <pyqtgraph.ViewBox.unregister>`
@ -52,7 +55,10 @@ class PlotWidget(GraphicsView):
self.setCentralItem(self.plotItem) self.setCentralItem(self.plotItem)
## Explicitly wrap methods from plotItem ## Explicitly wrap methods from plotItem
## NOTE: If you change this list, update the documentation above as well. ## NOTE: If you change this list, update the documentation above as well.
for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled', 'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange', 'register', 'unregister', 'viewRect']: for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange',
'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled',
'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange',
'setLimits', 'register', 'unregister', 'viewRect']:
setattr(self, m, getattr(self.plotItem, m)) setattr(self, m, getattr(self.plotItem, m))
#QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged) #QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged)
self.plotItem.sigRangeChanged.connect(self.viewRangeChanged) self.plotItem.sigRangeChanged.connect(self.viewRangeChanged)

View File

@ -3,6 +3,7 @@ if not USE_PYSIDE:
import sip import sip
from .. import multiprocess as mp from .. import multiprocess as mp
from .GraphicsView import GraphicsView from .GraphicsView import GraphicsView
from .. import CONFIG_OPTIONS
import numpy as np import numpy as np
import mmap, tempfile, ctypes, atexit, sys, random import mmap, tempfile, ctypes, atexit, sys, random
@ -35,7 +36,7 @@ class RemoteGraphicsView(QtGui.QWidget):
self._proc = mp.QtProcess(**kwds) self._proc = mp.QtProcess(**kwds)
self.pg = self._proc._import('pyqtgraph') self.pg = self._proc._import('pyqtgraph')
self.pg.setConfigOptions(**self.pg.CONFIG_OPTIONS) self.pg.setConfigOptions(**CONFIG_OPTIONS)
rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView') rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView')
self._view = rpgRemote.Renderer(*args, **remoteKwds) self._view = rpgRemote.Renderer(*args, **remoteKwds)
self._view._setProxyOptions(deferGetattr=True) self._view._setProxyOptions(deferGetattr=True)

View File

@ -9,7 +9,28 @@ try:
except ImportError: except ImportError:
HAVE_METAARRAY = False HAVE_METAARRAY = False
__all__ = ['TableWidget'] __all__ = ['TableWidget']
def _defersort(fn):
def defersort(self, *args, **kwds):
# may be called recursively; only the first call needs to block sorting
setSorting = False
if self._sorting is None:
self._sorting = self.isSortingEnabled()
setSorting = True
self.setSortingEnabled(False)
try:
return fn(self, *args, **kwds)
finally:
if setSorting:
self.setSortingEnabled(self._sorting)
self._sorting = None
return defersort
class TableWidget(QtGui.QTableWidget): class TableWidget(QtGui.QTableWidget):
"""Extends QTableWidget with some useful functions for automatic data handling """Extends QTableWidget with some useful functions for automatic data handling
and copy / export context menu. Can automatically format and display a variety and copy / export context menu. Can automatically format and display a variety
@ -18,14 +39,45 @@ class TableWidget(QtGui.QTableWidget):
""" """
def __init__(self, *args, **kwds): def __init__(self, *args, **kwds):
"""
All positional arguments are passed to QTableWidget.__init__().
===================== =================================================
**Keyword Arguments**
editable (bool) If True, cells in the table can be edited
by the user. Default is False.
sortable (bool) If True, the table may be soted by
clicking on column headers. Note that this also
causes rows to appear initially shuffled until
a sort column is selected. Default is True.
*(added in version 0.9.9)*
===================== =================================================
"""
QtGui.QTableWidget.__init__(self, *args) QtGui.QTableWidget.__init__(self, *args)
self.itemClass = TableWidgetItem
self.setVerticalScrollMode(self.ScrollPerPixel) self.setVerticalScrollMode(self.ScrollPerPixel)
self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection) self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection)
self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
self.setSortingEnabled(True)
self.clear() self.clear()
editable = kwds.get('editable', False)
self.setEditable(editable) kwds.setdefault('sortable', True)
kwds.setdefault('editable', False)
self.setEditable(kwds.pop('editable'))
self.setSortingEnabled(kwds.pop('sortable'))
if len(kwds) > 0:
raise TypeError("Invalid keyword arguments '%s'" % kwds.keys())
self._sorting = None # used when temporarily disabling sorting
self._formats = {None: None} # stores per-column formats and entire table format
self.sortModes = {} # stores per-column sort mode
self.itemChanged.connect(self.handleItemChanged)
self.contextMenu = QtGui.QMenu() self.contextMenu = QtGui.QMenu()
self.contextMenu.addAction('Copy Selection').triggered.connect(self.copySel) self.contextMenu.addAction('Copy Selection').triggered.connect(self.copySel)
self.contextMenu.addAction('Copy All').triggered.connect(self.copyAll) self.contextMenu.addAction('Copy All').triggered.connect(self.copyAll)
@ -40,6 +92,7 @@ class TableWidget(QtGui.QTableWidget):
self.items = [] self.items = []
self.setRowCount(0) self.setRowCount(0)
self.setColumnCount(0) self.setColumnCount(0)
self.sortModes = {}
def setData(self, data): def setData(self, data):
"""Set the data displayed in the table. """Set the data displayed in the table.
@ -56,12 +109,16 @@ class TableWidget(QtGui.QTableWidget):
self.appendData(data) self.appendData(data)
self.resizeColumnsToContents() self.resizeColumnsToContents()
@_defersort
def appendData(self, data): def appendData(self, data):
"""Types allowed:
1 or 2D numpy array or metaArray
1D numpy record array
list-of-lists, list-of-dicts or dict-of-lists
""" """
Add new rows to the table.
See :func:`setData() <pyqtgraph.TableWidget.setData>` for accepted
data types.
"""
startRow = self.rowCount()
fn0, header0 = self.iteratorFn(data) fn0, header0 = self.iteratorFn(data)
if fn0 is None: if fn0 is None:
self.clear() self.clear()
@ -80,42 +137,88 @@ class TableWidget(QtGui.QTableWidget):
self.setColumnCount(len(firstVals)) self.setColumnCount(len(firstVals))
if not self.verticalHeadersSet and header0 is not None: if not self.verticalHeadersSet and header0 is not None:
self.setRowCount(len(header0)) labels = [self.verticalHeaderItem(i).text() for i in range(self.rowCount())]
self.setVerticalHeaderLabels(header0) self.setRowCount(startRow + len(header0))
self.setVerticalHeaderLabels(labels + header0)
self.verticalHeadersSet = True self.verticalHeadersSet = True
if not self.horizontalHeadersSet and header1 is not None: if not self.horizontalHeadersSet and header1 is not None:
self.setHorizontalHeaderLabels(header1) self.setHorizontalHeaderLabels(header1)
self.horizontalHeadersSet = True self.horizontalHeadersSet = True
self.setRow(0, firstVals) i = startRow
i = 1 self.setRow(i, firstVals)
for row in it0: for row in it0:
self.setRow(i, [x for x in fn1(row)])
i += 1 i += 1
self.setRow(i, [x for x in fn1(row)])
if self._sorting and self.horizontalHeader().sortIndicatorSection() >= self.columnCount():
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
def setEditable(self, editable=True): def setEditable(self, editable=True):
self.editable = editable self.editable = editable
for item in self.items: for item in self.items:
item.setEditable(editable) item.setEditable(editable)
def setFormat(self, format, column=None):
"""
Specify the default text formatting for the entire table, or for a
single column if *column* is specified.
If a string is specified, it is used as a format string for converting
float values (and all other types are converted using str). If a
function is specified, it will be called with the item as its only
argument and must return a string. Setting format = None causes the
default formatter to be used instead.
Added in version 0.9.9.
"""
if format is not None and not isinstance(format, basestring) and not callable(format):
raise ValueError("Format argument must string, callable, or None. (got %s)" % format)
self._formats[column] = format
if column is None:
# update format of all items that do not have a column format
# specified
for c in range(self.columnCount()):
if self._formats.get(c, None) is None:
for r in range(self.rowCount()):
item = self.item(r, c)
if item is None:
continue
item.setFormat(format)
else:
# set all items in the column to use this format, or the default
# table format if None was specified.
if format is None:
format = self._formats[None]
for r in range(self.rowCount()):
item = self.item(r, column)
if item is None:
continue
item.setFormat(format)
def iteratorFn(self, data): def iteratorFn(self, data):
## Return 1) a function that will provide an iterator for data and 2) a list of header strings ## Return 1) a function that will provide an iterator for data and 2) a list of header strings
if isinstance(data, list) or isinstance(data, tuple): if isinstance(data, list) or isinstance(data, tuple):
return lambda d: d.__iter__(), None return lambda d: d.__iter__(), None
elif isinstance(data, dict): elif isinstance(data, dict):
return lambda d: iter(d.values()), list(map(str, data.keys())) return lambda d: iter(d.values()), list(map(asUnicode, data.keys()))
elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')):
if data.axisHasColumns(0): if data.axisHasColumns(0):
header = [str(data.columnName(0, i)) for i in range(data.shape[0])] header = [asUnicode(data.columnName(0, i)) for i in range(data.shape[0])]
elif data.axisHasValues(0): elif data.axisHasValues(0):
header = list(map(str, data.xvals(0))) header = list(map(asUnicode, data.xvals(0)))
else: else:
header = None header = None
return self.iterFirstAxis, header return self.iterFirstAxis, header
elif isinstance(data, np.ndarray): elif isinstance(data, np.ndarray):
return self.iterFirstAxis, None return self.iterFirstAxis, None
elif isinstance(data, np.void): elif isinstance(data, np.void):
return self.iterate, list(map(str, data.dtype.names)) return self.iterate, list(map(asUnicode, data.dtype.names))
elif data is None: elif data is None:
return (None,None) return (None,None)
else: else:
@ -135,21 +238,50 @@ class TableWidget(QtGui.QTableWidget):
def appendRow(self, data): def appendRow(self, data):
self.appendData([data]) self.appendData([data])
@_defersort
def addRow(self, vals): def addRow(self, vals):
row = self.rowCount() row = self.rowCount()
self.setRowCount(row + 1) self.setRowCount(row + 1)
self.setRow(row, vals) self.setRow(row, vals)
@_defersort
def setRow(self, row, vals): def setRow(self, row, vals):
if row > self.rowCount() - 1: if row > self.rowCount() - 1:
self.setRowCount(row + 1) self.setRowCount(row + 1)
for col in range(len(vals)): for col in range(len(vals)):
val = vals[col] val = vals[col]
item = TableWidgetItem(val) item = self.itemClass(val, row)
item.setEditable(self.editable) item.setEditable(self.editable)
sortMode = self.sortModes.get(col, None)
if sortMode is not None:
item.setSortMode(sortMode)
format = self._formats.get(col, self._formats[None])
item.setFormat(format)
self.items.append(item) self.items.append(item)
self.setItem(row, col, item) self.setItem(row, col, item)
item.setValue(val) # Required--the text-change callback is invoked
# when we call setItem.
def setSortMode(self, column, mode):
"""
Set the mode used to sort *column*.
============== ========================================================
**Sort Modes**
value Compares item.value if available; falls back to text
comparison.
text Compares item.text()
index Compares by the order in which items were inserted.
============== ========================================================
Added in version 0.9.9
"""
for r in range(self.rowCount()):
item = self.item(r, column)
if hasattr(item, 'setSortMode'):
item.setSortMode(mode)
self.sortModes[column] = mode
def sizeHint(self): def sizeHint(self):
# based on http://stackoverflow.com/a/7195443/54056 # based on http://stackoverflow.com/a/7195443/54056
width = sum(self.columnWidth(i) for i in range(self.columnCount())) width = sum(self.columnWidth(i) for i in range(self.columnCount()))
@ -173,7 +305,6 @@ class TableWidget(QtGui.QTableWidget):
rows = list(range(self.rowCount())) rows = list(range(self.rowCount()))
columns = list(range(self.columnCount())) columns = list(range(self.columnCount()))
data = [] data = []
if self.horizontalHeadersSet: if self.horizontalHeadersSet:
row = [] row = []
@ -222,7 +353,6 @@ class TableWidget(QtGui.QTableWidget):
if fileName == '': if fileName == '':
return return
open(fileName, 'w').write(data) open(fileName, 'w').write(data)
def contextMenuEvent(self, ev): def contextMenuEvent(self, ev):
self.contextMenu.popup(ev.globalPos()) self.contextMenu.popup(ev.globalPos())
@ -234,25 +364,102 @@ class TableWidget(QtGui.QTableWidget):
else: else:
ev.ignore() ev.ignore()
def handleItemChanged(self, item):
item.textChanged()
class TableWidgetItem(QtGui.QTableWidgetItem): class TableWidgetItem(QtGui.QTableWidgetItem):
def __init__(self, val): def __init__(self, val, index, format=None):
if isinstance(val, float) or isinstance(val, np.floating): QtGui.QTableWidgetItem.__init__(self, '')
s = "%0.3g" % val self._blockValueChange = False
else: self._format = None
s = str(val) self._defaultFormat = '%0.3g'
QtGui.QTableWidgetItem.__init__(self, s) self.sortMode = 'value'
self.value = val self.index = index
flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
self.setFlags(flags) self.setFlags(flags)
self.setValue(val)
self.setFormat(format)
def setEditable(self, editable): def setEditable(self, editable):
"""
Set whether this item is user-editable.
"""
if editable: if editable:
self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable) self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable)
else: else:
self.setFlags(self.flags() & ~QtCore.Qt.ItemIsEditable) self.setFlags(self.flags() & ~QtCore.Qt.ItemIsEditable)
def setSortMode(self, mode):
"""
Set the mode used to sort this item against others in its column.
============== ========================================================
**Sort Modes**
value Compares item.value if available; falls back to text
comparison.
text Compares item.text()
index Compares by the order in which items were inserted.
============== ========================================================
"""
modes = ('value', 'text', 'index', None)
if mode not in modes:
raise ValueError('Sort mode must be one of %s' % str(modes))
self.sortMode = mode
def setFormat(self, fmt):
"""Define the conversion from item value to displayed text.
If a string is specified, it is used as a format string for converting
float values (and all other types are converted using str). If a
function is specified, it will be called with the item as its only
argument and must return a string.
Added in version 0.9.9.
"""
if fmt is not None and not isinstance(fmt, basestring) and not callable(fmt):
raise ValueError("Format argument must string, callable, or None. (got %s)" % fmt)
self._format = fmt
self._updateText()
def _updateText(self):
self._blockValueChange = True
try:
self.setText(self.format())
finally:
self._blockValueChange = False
def setValue(self, value):
self.value = value
self._updateText()
def textChanged(self):
"""Called when this item's text has changed for any reason."""
if self._blockValueChange:
# text change was result of value or format change; do not
# propagate.
return
try:
self.value = type(self.value)(self.text())
except ValueError:
self.value = str(self.text())
def format(self):
if callable(self._format):
return self._format(self)
if isinstance(self.value, (float, np.floating)):
if self._format is None:
return self._defaultFormat % self.value
else:
return self._format % self.value
else:
return asUnicode(self.value)
def __lt__(self, other): def __lt__(self, other):
if hasattr(other, 'value'): if self.sortMode == 'index' and hasattr(other, 'index'):
return self.index < other.index
if self.sortMode == 'value' and hasattr(other, 'value'):
return self.value < other.value return self.value < other.value
else: else:
return self.text() < other.text() return self.text() < other.text()

View File

@ -16,18 +16,18 @@ class ValueLabel(QtGui.QLabel):
def __init__(self, parent=None, suffix='', siPrefix=False, averageTime=0, formatStr=None): def __init__(self, parent=None, suffix='', siPrefix=False, averageTime=0, formatStr=None):
""" """
============ ================================================================================== ============== ==================================================================================
Arguments **Arguments:**
suffix (str or None) The suffix to place after the value 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 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 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 is 0, then no averaging is performed. As this value increases
the display value will appear to change more slowly and smoothly. 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 formatStr (str) Optionally, provide a format string to use when displaying text. The text
will be generated by calling formatStr.format(value=, avgValue=, suffix=) will be generated by calling formatStr.format(value=, avgValue=, suffix=)
(see Python documentation on str.format) (see Python documentation on str.format)
This option is not compatible with siPrefix This option is not compatible with siPrefix
============ ================================================================================== ============== ==================================================================================
""" """
QtGui.QLabel.__init__(self, parent) QtGui.QLabel.__init__(self, parent)
self.values = [] self.values = []

View File

@ -0,0 +1,128 @@
import pyqtgraph as pg
import numpy as np
from pyqtgraph.pgcollections import OrderedDict
app = pg.mkQApp()
listOfTuples = [('text_%d' % i, i, i/9.) for i in range(12)]
listOfLists = [list(row) for row in listOfTuples]
plainArray = np.array(listOfLists, dtype=object)
recordArray = np.array(listOfTuples, dtype=[('string', object),
('integer', int),
('floating', float)])
dictOfLists = OrderedDict([(name, list(recordArray[name])) for name in recordArray.dtype.names])
listOfDicts = [OrderedDict([(name, rec[name]) for name in recordArray.dtype.names]) for rec in recordArray]
transposed = [[row[col] for row in listOfTuples] for col in range(len(listOfTuples[0]))]
def assertTableData(table, data):
assert len(data) == table.rowCount()
rows = list(range(table.rowCount()))
columns = list(range(table.columnCount()))
for r in rows:
assert len(data[r]) == table.columnCount()
row = []
for c in columns:
item = table.item(r, c)
if item is not None:
row.append(item.value)
else:
row.append(None)
assert row == list(data[r])
def test_TableWidget():
w = pg.TableWidget(sortable=False)
# Test all input data types
w.setData(listOfTuples)
assertTableData(w, listOfTuples)
w.setData(listOfLists)
assertTableData(w, listOfTuples)
w.setData(plainArray)
assertTableData(w, listOfTuples)
w.setData(recordArray)
assertTableData(w, listOfTuples)
w.setData(dictOfLists)
assertTableData(w, transposed)
w.appendData(dictOfLists)
assertTableData(w, transposed * 2)
w.setData(listOfDicts)
assertTableData(w, listOfTuples)
w.appendData(listOfDicts)
assertTableData(w, listOfTuples * 2)
# Test sorting
w.setData(listOfTuples)
w.sortByColumn(0, pg.QtCore.Qt.AscendingOrder)
assertTableData(w, sorted(listOfTuples, key=lambda a: a[0]))
w.sortByColumn(1, pg.QtCore.Qt.AscendingOrder)
assertTableData(w, sorted(listOfTuples, key=lambda a: a[1]))
w.sortByColumn(2, pg.QtCore.Qt.AscendingOrder)
assertTableData(w, sorted(listOfTuples, key=lambda a: a[2]))
w.setSortMode(1, 'text')
w.sortByColumn(1, pg.QtCore.Qt.AscendingOrder)
assertTableData(w, sorted(listOfTuples, key=lambda a: str(a[1])))
w.setSortMode(1, 'index')
w.sortByColumn(1, pg.QtCore.Qt.AscendingOrder)
assertTableData(w, listOfTuples)
# Test formatting
item = w.item(0, 2)
assert item.text() == ('%0.3g' % item.value)
w.setFormat('%0.6f')
assert item.text() == ('%0.6f' % item.value)
w.setFormat('X%0.7f', column=2)
assert isinstance(item.value, float)
assert item.text() == ('X%0.7f' % item.value)
# test setting items that do not exist yet
w.setFormat('X%0.7f', column=3)
# test append uses correct formatting
w.appendRow(('x', 10, 7.3))
item = w.item(w.rowCount()-1, 2)
assert isinstance(item.value, float)
assert item.text() == ('X%0.7f' % item.value)
# test reset back to defaults
w.setFormat(None, column=2)
assert isinstance(item.value, float)
assert item.text() == ('%0.6f' % item.value)
w.setFormat(None)
assert isinstance(item.value, float)
assert item.text() == ('%0.3g' % item.value)
# test function formatter
def fmt(item):
if isinstance(item.value, float):
return "%d %f" % (item.index, item.value)
else:
return pg.asUnicode(item.value)
w.setFormat(fmt)
assert isinstance(item.value, float)
assert isinstance(item.index, int)
assert item.text() == ("%d %f" % (item.index, item.value))
if __name__ == '__main__':
w = pg.TableWidget(editable=True)
w.setData(listOfTuples)
w.resize(600, 600)
w.show()