Merge branch 'develop' into core
This commit is contained in:
commit
c7f4a8fd39
@ -92,15 +92,11 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
|
||||
self.clickEvents = []
|
||||
self.dragButtons = []
|
||||
self.prepItems = weakref.WeakKeyDictionary() ## set of items with prepareForPaintMethods
|
||||
self.mouseGrabber = None
|
||||
self.dragItem = None
|
||||
self.lastDrag = None
|
||||
self.hoverItems = weakref.WeakKeyDictionary()
|
||||
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[0].triggered.connect(self.showExportDialog)
|
||||
@ -437,10 +433,10 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
for item in items:
|
||||
if hoverable and not hasattr(item, 'hoverEvent'):
|
||||
continue
|
||||
shape = item.shape()
|
||||
shape = item.shape() # Note: default shape() returns boundingRect()
|
||||
if shape is None:
|
||||
continue
|
||||
if item.mapToScene(shape).contains(point):
|
||||
if shape.contains(item.mapFromScene(point)):
|
||||
items2.append(item)
|
||||
|
||||
## Sort by descending Z-order (don't trust scene.itms() to do this either)
|
||||
|
@ -131,6 +131,10 @@ class MouseDragEvent(object):
|
||||
return self.finish
|
||||
|
||||
def __repr__(self):
|
||||
if self.currentItem is None:
|
||||
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()))
|
||||
@ -221,8 +225,14 @@ class MouseClickEvent(object):
|
||||
return self._modifiers
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
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):
|
||||
return self._time
|
||||
@ -345,6 +355,10 @@ class HoverEvent(object):
|
||||
return Point(self.currentItem.mapFromScene(self._lastScenePos))
|
||||
|
||||
def __repr__(self):
|
||||
if self.currentItem is None:
|
||||
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()))
|
||||
|
20
Qt.py
20
Qt.py
@ -32,6 +32,23 @@ else:
|
||||
if USE_PYSIDE:
|
||||
from PySide import QtGui, QtCore, QtOpenGL, QtSvg
|
||||
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__
|
||||
|
||||
# Make a loadUiType function like PyQt has
|
||||
@ -78,6 +95,9 @@ else:
|
||||
pass
|
||||
|
||||
|
||||
import sip
|
||||
def isQObjectAlive(obj):
|
||||
return not sip.isdeleted(obj)
|
||||
loadUiType = uic.loadUiType
|
||||
|
||||
QtCore.Signal = QtCore.pyqtSignal
|
||||
|
@ -4,7 +4,6 @@ from .Vector import Vector
|
||||
from .Transform3D import Transform3D
|
||||
from .Vector import Vector
|
||||
import numpy as np
|
||||
import scipy.linalg
|
||||
|
||||
class SRTTransform3D(Transform3D):
|
||||
"""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,
|
||||
otherwise the conversion will most likely fail.
|
||||
"""
|
||||
import numpy.linalg
|
||||
for i in range(4):
|
||||
self.setRow(i, m.row(i))
|
||||
m = self.matrix().reshape(4,4)
|
||||
## translation is 4th column
|
||||
self._state['pos'] = m[:3,3]
|
||||
|
||||
## scale is vector-length of first three columns
|
||||
scale = (m[:3,:3]**2).sum(axis=0)**0.5
|
||||
## see whether there is an inversion
|
||||
@ -132,9 +133,9 @@ class SRTTransform3D(Transform3D):
|
||||
self._state['scale'] = scale
|
||||
|
||||
## rotation axis is the eigenvector with eigenvalue=1
|
||||
r = m[:3, :3] / scale[:, np.newaxis]
|
||||
r = m[:3, :3] / scale[np.newaxis, :]
|
||||
try:
|
||||
evals, evecs = scipy.linalg.eig(r)
|
||||
evals, evecs = numpy.linalg.eig(r)
|
||||
except:
|
||||
print("Rotation matrix: %s" % str(r))
|
||||
print("Scale: %s" % str(scale))
|
||||
|
15
Vector.py
15
Vector.py
@ -68,3 +68,18 @@ class Vector(QtGui.QVector3D):
|
||||
yield(self.y())
|
||||
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
|
||||
|
||||
|
||||
|
12
__init__.py
12
__init__.py
@ -52,10 +52,11 @@ CONFIG_OPTIONS = {
|
||||
'background': 'k', ## default background for GraphicsWidget
|
||||
'antialias': False,
|
||||
'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
|
||||
'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)
|
||||
'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 .colormap import *
|
||||
from .ptime import time
|
||||
from pyqtgraph.Qt import isQObjectAlive
|
||||
|
||||
|
||||
##############################################################
|
||||
@ -284,7 +286,12 @@ def cleanup():
|
||||
s = QtGui.QGraphicsScene()
|
||||
for o in gc.get_objects():
|
||||
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)
|
||||
except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object
|
||||
continue
|
||||
@ -393,6 +400,7 @@ def dbg(*args, **kwds):
|
||||
consoles.append(c)
|
||||
except NameError:
|
||||
consoles = [c]
|
||||
return c
|
||||
|
||||
|
||||
def mkQApp():
|
||||
|
@ -431,9 +431,12 @@ class CanvasItem(QtCore.QObject):
|
||||
def selectionChanged(self, sel, multi):
|
||||
"""
|
||||
Inform the item that its selection state has changed.
|
||||
Arguments:
|
||||
sel: bool, whether the item is currently selected
|
||||
multi: bool, whether there are multiple items currently selected
|
||||
============== =========================================================
|
||||
**Arguments:**
|
||||
sel (bool) whether the item is currently selected
|
||||
multi (bool) whether there are multiple items currently
|
||||
selected
|
||||
============== =========================================================
|
||||
"""
|
||||
self.selectedAlone = sel and not multi
|
||||
self.showSelectBox()
|
||||
|
38
colormap.py
38
colormap.py
@ -1,5 +1,4 @@
|
||||
import numpy as np
|
||||
import scipy.interpolate
|
||||
from .Qt import QtGui, QtCore
|
||||
|
||||
class ColorMap(object):
|
||||
@ -52,8 +51,8 @@ class ColorMap(object):
|
||||
|
||||
def __init__(self, pos, color, mode=None):
|
||||
"""
|
||||
========= ==============================================================
|
||||
Arguments
|
||||
=============== ==============================================================
|
||||
**Arguments:**
|
||||
pos Array of positions where each color is defined
|
||||
color Array of RGBA colors.
|
||||
Integer data types are interpreted as 0-255; float data types
|
||||
@ -62,10 +61,10 @@ class ColorMap(object):
|
||||
indicating the color space that should be used when
|
||||
interpolating between stops. Note that the last mode value is
|
||||
ignored. By default, the mode is entirely RGB.
|
||||
========= ==============================================================
|
||||
=============== ==============================================================
|
||||
"""
|
||||
self.pos = pos
|
||||
self.color = color
|
||||
self.pos = np.array(pos)
|
||||
self.color = np.array(color)
|
||||
if mode is None:
|
||||
mode = np.ones(len(pos))
|
||||
self.mode = mode
|
||||
@ -92,15 +91,24 @@ class ColorMap(object):
|
||||
else:
|
||||
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):
|
||||
interp = scipy.interpolate.griddata(pos, color, np.array([data]))[0]
|
||||
# Interpolate
|
||||
# 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:
|
||||
interp = scipy.interpolate.griddata(pos, color, data)
|
||||
|
||||
if mode == self.QCOLOR:
|
||||
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)
|
||||
else:
|
||||
return [QtGui.QColor(*x) for x in interp]
|
||||
@ -193,8 +201,8 @@ class ColorMap(object):
|
||||
"""
|
||||
Return an RGB(A) lookup table (ndarray).
|
||||
|
||||
============= ============================================================================
|
||||
**Arguments**
|
||||
=============== =============================================================================
|
||||
**Arguments:**
|
||||
start The starting value in the lookup table (default=0.0)
|
||||
stop The final value in the lookup table (default=1.0)
|
||||
nPts The number of points in the returned lookup table.
|
||||
@ -202,7 +210,7 @@ class ColorMap(object):
|
||||
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'.
|
||||
See :func:`map() <pyqtgraph.ColorMap.map>`.
|
||||
============= ============================================================================
|
||||
=============== =============================================================================
|
||||
"""
|
||||
if isinstance(mode, basestring):
|
||||
mode = self.enumMap[mode.lower()]
|
||||
|
@ -31,8 +31,8 @@ class ConsoleWidget(QtGui.QWidget):
|
||||
|
||||
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
|
||||
historyFile optional file for storing command history
|
||||
text initial text to display in the console window
|
||||
@ -40,7 +40,7 @@ class ConsoleWidget(QtGui.QWidget):
|
||||
double-clicked). May contain {fileName} and {lineNum} format keys. Example::
|
||||
|
||||
editorCommand --loadfile {fileName} --gotoline {lineNum}
|
||||
============ =============================================================================
|
||||
============== =============================================================================
|
||||
"""
|
||||
QtGui.QWidget.__init__(self, parent)
|
||||
if namespace is None:
|
||||
|
123
debug.py
123
debug.py
@ -7,10 +7,12 @@ Distributed under MIT/X11 license. See license.txt for more infomation.
|
||||
|
||||
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 numpy import ndarray
|
||||
from .Qt import QtCore, QtGui
|
||||
from .util.mutex import Mutex
|
||||
from .util import cprint
|
||||
|
||||
__ftraceDepth = 0
|
||||
def ftrace(func):
|
||||
@ -238,7 +240,8 @@ def refPathString(chain):
|
||||
|
||||
def objectSize(obj, ignore=None, verbose=False, depth=0, recursive=False):
|
||||
"""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)')
|
||||
|
||||
|
||||
@ -399,7 +402,9 @@ class Profiler(object):
|
||||
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
|
||||
_msgs = []
|
||||
|
||||
@ -415,12 +420,10 @@ class Profiler(object):
|
||||
_disabledProfiler = DisabledProfiler()
|
||||
|
||||
|
||||
if _profilers:
|
||||
_profilers = _profilers.split(",")
|
||||
def __new__(cls, msg=None, disabled='env', delayed=True):
|
||||
"""Optionally create a new profiler based on caller's qualname.
|
||||
"""
|
||||
if disabled is True:
|
||||
if disabled is True or (disabled=='env' and len(cls._profilers) == 0):
|
||||
return cls._disabledProfiler
|
||||
|
||||
# determine the qualified name of the caller function
|
||||
@ -428,11 +431,11 @@ class Profiler(object):
|
||||
try:
|
||||
caller_object_type = type(caller_frame.f_locals["self"])
|
||||
except KeyError: # we are in a regular function
|
||||
qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1]
|
||||
qualifier = caller_frame.f_globals["__name__"].split(".", 1)[-1]
|
||||
else: # we are in a method
|
||||
qualifier = caller_object_type.__name__
|
||||
func_qualname = qualifier + "." + caller_frame.f_code.co_name
|
||||
if func_qualname not in cls._profilers: # don't do anything
|
||||
if disabled=='env' and func_qualname not in cls._profilers: # don't do anything
|
||||
return cls._disabledProfiler
|
||||
# create an actual profiling object
|
||||
cls._depth += 1
|
||||
@ -444,9 +447,9 @@ class Profiler(object):
|
||||
obj._firstTime = obj._lastTime = ptime.time()
|
||||
obj._newMsg("> Entering " + obj._name)
|
||||
return obj
|
||||
else:
|
||||
def __new__(cls, delayed=True):
|
||||
return lambda msg=None: None
|
||||
#else:
|
||||
#def __new__(cls, delayed=True):
|
||||
#return lambda msg=None: None
|
||||
|
||||
def __call__(self, msg=None):
|
||||
"""Register or print a new message with timing information.
|
||||
@ -467,6 +470,7 @@ class Profiler(object):
|
||||
if self._delayed:
|
||||
self._msgs.append((msg, args))
|
||||
else:
|
||||
self.flush()
|
||||
print(msg % args)
|
||||
|
||||
def __del__(self):
|
||||
@ -483,12 +487,15 @@ class Profiler(object):
|
||||
self._newMsg("< Exiting %s, total time: %0.4f ms",
|
||||
self._name, (ptime.time() - self._firstTime) * 1000)
|
||||
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]))
|
||||
type(self)._msgs = []
|
||||
|
||||
|
||||
|
||||
def profile(code, name='profile_run', sort='cumulative', num=30):
|
||||
"""Common-use for cProfile"""
|
||||
cProfile.run(code, name)
|
||||
@ -618,12 +625,12 @@ class ObjTracker(object):
|
||||
|
||||
## Which refs have disappeared since call to start() (these are only displayed once, then forgotten.)
|
||||
delRefs = {}
|
||||
for i in self.startRefs.keys():
|
||||
for i in list(self.startRefs.keys()):
|
||||
if i not in refs:
|
||||
delRefs[i] = self.startRefs[i]
|
||||
del self.startRefs[i]
|
||||
self.forgetRef(delRefs[i])
|
||||
for i in self.newRefs.keys():
|
||||
for i in list(self.newRefs.keys()):
|
||||
if i not in refs:
|
||||
delRefs[i] = self.newRefs[i]
|
||||
del self.newRefs[i]
|
||||
@ -661,7 +668,8 @@ class ObjTracker(object):
|
||||
for k in self.startCount:
|
||||
c1[k] = c1.get(k, 0) - self.startCount[k]
|
||||
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:
|
||||
if c1[t] == 0:
|
||||
continue
|
||||
@ -761,7 +769,8 @@ class ObjTracker(object):
|
||||
c = count.get(typ, [0,0])
|
||||
count[typ] = [c[0]+1, c[1]+objectSize(obj)]
|
||||
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:
|
||||
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):
|
||||
"""Create a more useful type string by making <instance> types report their class."""
|
||||
typ = type(obj)
|
||||
if typ == types.InstanceType:
|
||||
if typ == getattr(types, 'InstanceType', None):
|
||||
return "<instance of %s>" % obj.__class__.__name__
|
||||
else:
|
||||
return str(typ)
|
||||
|
||||
def searchRefs(obj, *args):
|
||||
"""Pseudo-interactive function for tracing references backward.
|
||||
Arguments:
|
||||
**Arguments:**
|
||||
|
||||
obj: The initial object from which to start searching
|
||||
args: A set of string or int arguments.
|
||||
each integer selects one of obj's referrers to be the new 'obj'
|
||||
@ -840,7 +850,8 @@ def searchRefs(obj, *args):
|
||||
ro: return obj
|
||||
rr: return list of obj's referrers
|
||||
|
||||
Examples:
|
||||
Examples::
|
||||
|
||||
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', 'l') ## ..also print lengths of the last set of referrers
|
||||
@ -989,3 +1000,75 @@ class PrintDetector(object):
|
||||
|
||||
def flush(self):
|
||||
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]
|
||||
|
@ -2,6 +2,7 @@ from ..Qt import QtCore, QtGui
|
||||
|
||||
from .DockDrop import *
|
||||
from ..widgets.VerticalLabel import VerticalLabel
|
||||
from ..python2_3 import asUnicode
|
||||
|
||||
class Dock(QtGui.QWidget, DockDrop):
|
||||
|
||||
@ -167,7 +168,7 @@ class Dock(QtGui.QWidget, DockDrop):
|
||||
self.resizeOverlay(self.size())
|
||||
|
||||
def name(self):
|
||||
return str(self.label.text())
|
||||
return asUnicode(self.label.text())
|
||||
|
||||
def container(self):
|
||||
return self._container
|
||||
|
@ -36,8 +36,8 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
|
||||
def addDock(self, dock=None, position='bottom', relativeTo=None, **kwds):
|
||||
"""Adds a dock to this area.
|
||||
|
||||
=========== =================================================================
|
||||
Arguments:
|
||||
============== =================================================================
|
||||
**Arguments:**
|
||||
dock The new Dock object to add. If None, then a new Dock will be
|
||||
created.
|
||||
position 'bottom', 'top', 'left', 'right', 'above', or 'below'
|
||||
@ -45,7 +45,7 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
|
||||
entire edge of the window. If relativeTo is another Dock, then
|
||||
the new Dock is placed adjacent to it (or in a tabbed
|
||||
configuration for 'above' and 'below').
|
||||
=========== =================================================================
|
||||
============== =================================================================
|
||||
|
||||
All extra keyword arguments are passed to Dock.__init__() if *dock* is
|
||||
None.
|
||||
|
16
dockarea/tests/test_dock.py
Normal file
16
dockarea/tests/test_dock.py
Normal 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)
|
@ -36,7 +36,7 @@ class PrintExporter(Exporter):
|
||||
dialog = QtGui.QPrintDialog(printer)
|
||||
dialog.setWindowTitle("Print Document")
|
||||
if dialog.exec_() != QtGui.QDialog.Accepted:
|
||||
return;
|
||||
return
|
||||
|
||||
#dpi = QtGui.QDesktopWidget().physicalDpiX()
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
from .Exporter import Exporter
|
||||
from ..python2_3 import asUnicode
|
||||
from ..parametertree import Parameter
|
||||
from ..Qt import QtGui, QtCore, QtSvg
|
||||
from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE
|
||||
from .. import debug
|
||||
from .. import functions as fn
|
||||
import re
|
||||
@ -219,6 +219,9 @@ def _generateItemSvg(item, nodes=None, root=None):
|
||||
#if hasattr(item, 'setExportMode'):
|
||||
#item.setExportMode(False)
|
||||
|
||||
if USE_PYSIDE:
|
||||
xmlStr = str(arr)
|
||||
else:
|
||||
xmlStr = bytes(arr).decode('utf-8')
|
||||
doc = xml.parseString(xmlStr)
|
||||
|
||||
|
67
exporters/tests/test_svg.py
Normal file
67
exporters/tests/test_svg.py
Normal 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')
|
||||
|
||||
|
@ -227,17 +227,10 @@ class Flowchart(Node):
|
||||
def nodeClosed(self, node):
|
||||
del self._nodes[node.name()]
|
||||
self.widget().removeNode(node)
|
||||
for signal in ['sigClosed', 'sigRenamed', 'sigOutputChanged']:
|
||||
try:
|
||||
node.sigClosed.disconnect(self.nodeClosed)
|
||||
except TypeError:
|
||||
pass
|
||||
try:
|
||||
node.sigRenamed.disconnect(self.nodeRenamed)
|
||||
except TypeError:
|
||||
pass
|
||||
try:
|
||||
node.sigOutputChanged.disconnect(self.nodeOutputChanged)
|
||||
except TypeError:
|
||||
getattr(node, signal).disconnect(self.nodeClosed)
|
||||
except (TypeError, RuntimeError):
|
||||
pass
|
||||
self.sigChartChanged.emit(self, 'remove', node)
|
||||
|
||||
@ -769,7 +762,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
|
||||
#self.disconnect(item.bypassBtn, QtCore.SIGNAL('clicked()'), self.bypassClicked)
|
||||
try:
|
||||
item.bypassBtn.clicked.disconnect(self.bypassClicked)
|
||||
except TypeError:
|
||||
except (TypeError, RuntimeError):
|
||||
pass
|
||||
self.ui.ctrlList.removeTopLevelItem(item)
|
||||
|
||||
|
@ -37,7 +37,7 @@ class Node(QtCore.QObject):
|
||||
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
|
||||
string, but must be unique within a flowchart. Usually,
|
||||
we simply let the flowchart decide on a name when calling
|
||||
@ -501,8 +501,8 @@ class NodeGraphicsItem(GraphicsObject):
|
||||
bounds = self.boundingRect()
|
||||
self.nameItem.setPos(bounds.width()/2. - self.nameItem.boundingRect().width()/2., 0)
|
||||
|
||||
def setPen(self, pen):
|
||||
self.pen = pen
|
||||
def setPen(self, *args, **kwargs):
|
||||
self.pen = fn.mkPen(*args, **kwargs)
|
||||
self.update()
|
||||
|
||||
def setBrush(self, brush):
|
||||
|
@ -26,12 +26,14 @@ class NodeLibrary:
|
||||
Register a new node type. If the type's name is already in use,
|
||||
an exception will be raised (unless override=True).
|
||||
|
||||
Arguments:
|
||||
============== =========================================================
|
||||
**Arguments:**
|
||||
|
||||
nodeClass - a subclass of Node (must have typ.nodeName)
|
||||
paths - list of tuples specifying the location(s) this
|
||||
nodeClass a subclass of Node (must have typ.nodeName)
|
||||
paths list of tuples specifying the location(s) this
|
||||
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):
|
||||
raise Exception("Object %s is not a Node subclass" % str(nodeClass))
|
||||
|
@ -1,10 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from ...Qt import QtCore, QtGui
|
||||
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 as pgfn
|
||||
from .common import *
|
||||
import numpy as np
|
||||
|
||||
@ -119,7 +117,11 @@ class Median(CtrlNode):
|
||||
|
||||
@metaArrayWrapper
|
||||
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):
|
||||
"""Filters data by taking the mode (histogram-based) of a sliding window"""
|
||||
@ -156,7 +158,11 @@ class Gaussian(CtrlNode):
|
||||
|
||||
@metaArrayWrapper
|
||||
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):
|
||||
@ -189,6 +195,10 @@ class Detrend(CtrlNode):
|
||||
|
||||
@metaArrayWrapper
|
||||
def processData(self, data):
|
||||
try:
|
||||
from scipy.signal import detrend
|
||||
except ImportError:
|
||||
raise Exception("DetrendFilter node requires the package scipy.signal.")
|
||||
return detrend(data)
|
||||
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import scipy
|
||||
import numpy as np
|
||||
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):
|
||||
"""Apply a linear filter with coefficients a, b. Optionally pad the data before filtering
|
||||
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)
|
||||
|
||||
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):
|
||||
"""return data passed through bessel filter"""
|
||||
try:
|
||||
import scipy.signal
|
||||
except ImportError:
|
||||
raise Exception("besselFilter() requires the package scipy.signal.")
|
||||
|
||||
if dt is None:
|
||||
try:
|
||||
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):
|
||||
"""return data passed through bessel filter"""
|
||||
try:
|
||||
import scipy.signal
|
||||
except ImportError:
|
||||
raise Exception("butterworthFilter() requires the package scipy.signal.")
|
||||
|
||||
if dt is None:
|
||||
try:
|
||||
tvals = data.xvals('Time')
|
||||
@ -175,6 +189,11 @@ def denoise(data, radius=2, threshold=4):
|
||||
|
||||
def adaptiveDetrend(data, x=None, threshold=3.0):
|
||||
"""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:
|
||||
x = data.xvals(0)
|
||||
|
||||
|
354
functions.py
354
functions.py
@ -34,17 +34,6 @@ import decimal, re
|
||||
import ctypes
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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>`
|
||||
|
||||
============== ====================================================================================================
|
||||
Arguments:
|
||||
**Arguments:**
|
||||
*data* (ndarray) the original dataset
|
||||
*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.
|
||||
@ -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))
|
||||
|
||||
"""
|
||||
if not HAVE_SCIPY:
|
||||
raise Exception("This function requires the scipy library, but it does not appear to be importable.")
|
||||
try:
|
||||
import scipy.ndimage
|
||||
have_scipy = True
|
||||
except ImportError:
|
||||
have_scipy = False
|
||||
have_scipy = False
|
||||
|
||||
# sanity check
|
||||
if len(shape) != len(vectors):
|
||||
@ -445,7 +438,6 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
|
||||
#print "tr1:", tr1
|
||||
## dims are now [(slice axes), (other axes)]
|
||||
|
||||
|
||||
## make sure vectors are arrays
|
||||
if not isinstance(vectors, np.ndarray):
|
||||
vectors = np.array(vectors)
|
||||
@ -461,12 +453,18 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
|
||||
#print "X values:"
|
||||
#print x
|
||||
## iterate manually over unused axes since map_coordinates won't do it for us
|
||||
if have_scipy:
|
||||
extraShape = data.shape[len(axes):]
|
||||
output = np.empty(tuple(shape) + extraShape, dtype=data.dtype)
|
||||
for inds in np.ndindex(*extraShape):
|
||||
ind = (Ellipsis,) + inds
|
||||
#print data[ind].shape, x.shape, output[ind].shape, output.shape
|
||||
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))
|
||||
trb = []
|
||||
@ -483,6 +481,117 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
|
||||
else:
|
||||
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):
|
||||
"""
|
||||
Given a QTransform, return a 3x3 numpy array.
|
||||
@ -577,17 +686,25 @@ def transformCoordinates(tr, coords, transpose=False):
|
||||
def solve3DTransform(points1, 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:
|
||||
raise Exception("This function depends on the scipy library, but it does not appear to be importable.")
|
||||
A = np.array([[points1[i].x(), points1[i].y(), points1[i].z(), 1] for i in range(4)])
|
||||
B = np.array([[points2[i].x(), points2[i].y(), points2[i].z(), 1] for i in range(4)])
|
||||
import numpy.linalg
|
||||
pts = []
|
||||
for inp in (points1, points2):
|
||||
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
|
||||
matrix = np.zeros((4,4))
|
||||
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
|
||||
|
||||
@ -600,8 +717,7 @@ def solveBilinearTransform(points1, points2):
|
||||
|
||||
mapped = np.dot(matrix, [x*y, x, y, 1])
|
||||
"""
|
||||
if not HAVE_SCIPY:
|
||||
raise Exception("This function depends on the scipy library, but it does not appear to be importable.")
|
||||
import numpy.linalg
|
||||
## A is 4 rows (points) x 4 columns (xy, x, y, 1)
|
||||
## 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)])
|
||||
@ -610,7 +726,7 @@ def solveBilinearTransform(points1, points2):
|
||||
## solve 2 sets of linear equations to determine transformation matrix elements
|
||||
matrix = np.zeros((2,4))
|
||||
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
|
||||
|
||||
@ -629,6 +745,10 @@ def rescaleData(data, scale, offset, dtype=None):
|
||||
try:
|
||||
if not getConfigOption('useWeave'):
|
||||
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
|
||||
if not data.dtype.isnative:
|
||||
@ -671,68 +791,13 @@ def applyLookupTable(data, lut):
|
||||
Uses values in *data* as indexes to select values from *lut*.
|
||||
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.
|
||||
"""
|
||||
if data.dtype.kind not in ('i', 'u'):
|
||||
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')
|
||||
|
||||
### 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):
|
||||
"""Equivalent to makeARGB(..., useRGBA=True)"""
|
||||
@ -751,8 +816,8 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||
|
||||
Both stages are optional.
|
||||
|
||||
============ ==================================================================================
|
||||
Arguments:
|
||||
============== ==================================================================================
|
||||
**Arguments:**
|
||||
data numpy array of int/float types. If
|
||||
levels List [min, max]; optionally rescale data before converting through the
|
||||
lookup table. The data is rescaled such that min->0 and max->*scale*::
|
||||
@ -780,7 +845,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||
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
|
||||
is BGRA).
|
||||
============ ==================================================================================
|
||||
============== ==================================================================================
|
||||
"""
|
||||
profile = debug.Profiler()
|
||||
|
||||
@ -887,8 +952,8 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
|
||||
pointing to the array which shares its data to prevent python
|
||||
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)
|
||||
and dtype=ubyte. The order of values in the 3rd axis must be
|
||||
(b, g, r, a).
|
||||
@ -903,7 +968,7 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
|
||||
creating the image. Note that Qt expects the axes to be in
|
||||
(height, width) order whereas pyqtgraph usually prefers the
|
||||
opposite.
|
||||
=========== ===================================================================
|
||||
============== ===================================================================
|
||||
"""
|
||||
## create QImage from buffer
|
||||
profile = debug.Profiler()
|
||||
@ -993,6 +1058,10 @@ def imageToArray(img, copy=False, transpose=True):
|
||||
else:
|
||||
ptr.setsize(img.byteCount())
|
||||
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:
|
||||
arr = arr.reshape(img.height(), img.width(), 3)
|
||||
@ -1052,6 +1121,85 @@ def colorToAlpha(data, color):
|
||||
#raise Exception()
|
||||
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'):
|
||||
@ -1113,6 +1261,8 @@ def arrayToQPath(x, y, connect='all'):
|
||||
# decide which points are connected by lines
|
||||
if connect == 'pairs':
|
||||
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[:,1] = 0
|
||||
connect = connect.flatten()
|
||||
@ -1240,8 +1390,8 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False):
|
||||
"""
|
||||
Generate isocurve from 2D data using marching squares algorithm.
|
||||
|
||||
============= =========================================================
|
||||
Arguments
|
||||
============== =========================================================
|
||||
**Arguments:**
|
||||
data 2D numpy array of scalar values
|
||||
level The level at which to generate an isosurface
|
||||
connected If False, return a single long list of point pairs
|
||||
@ -1252,7 +1402,7 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False):
|
||||
the data.
|
||||
path if True, return a QPainterPath rather than a list of
|
||||
vertex coordinates. This forces connected=True.
|
||||
============= =========================================================
|
||||
============== =========================================================
|
||||
|
||||
This function is SLOW; plenty of room for optimization here.
|
||||
"""
|
||||
@ -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)
|
||||
The parameter *smooth* is expressed in pixels.
|
||||
"""
|
||||
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:
|
||||
values = values.T
|
||||
values = values[np.newaxis, np.newaxis, ...].astype(float)
|
||||
@ -1441,7 +1595,7 @@ def traceImage(image, values, smooth=0.5):
|
||||
paths = []
|
||||
for i in range(diff.shape[-1]):
|
||||
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)
|
||||
path = QtGui.QPainterPath()
|
||||
for line in lines:
|
||||
@ -1512,7 +1666,8 @@ def isosurface(data, level):
|
||||
0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
|
||||
0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190,
|
||||
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.
|
||||
## Each set of three integers tells us which three edges to
|
||||
@ -1790,7 +1945,7 @@ def isosurface(data, level):
|
||||
[1, 1, 0, 2],
|
||||
[0, 1, 0, 2],
|
||||
#[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)
|
||||
faceShiftTables = [None]
|
||||
for i in range(1,6):
|
||||
@ -1889,7 +2044,6 @@ def isosurface(data, level):
|
||||
#profiler()
|
||||
if cells.shape[0] == 0:
|
||||
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
|
||||
#profiler()
|
||||
|
||||
@ -1901,9 +2055,7 @@ def isosurface(data, level):
|
||||
#profiler()
|
||||
|
||||
### expensive:
|
||||
#print verts.shape
|
||||
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]
|
||||
#profiler()
|
||||
nv = vertInds.shape[0]
|
||||
@ -1924,14 +2076,16 @@ def invertQTransform(tr):
|
||||
bugs in that method. (specifically, Qt has floating-point precision issues
|
||||
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()
|
||||
if inv[1] is False:
|
||||
raise Exception("Transform is not invertible.")
|
||||
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):
|
||||
|
@ -16,12 +16,14 @@ class ArrowItem(QtGui.QGraphicsPathItem):
|
||||
Arrows can be initialized with any keyword arguments accepted by
|
||||
the setStyle() method.
|
||||
"""
|
||||
self.opts = {}
|
||||
QtGui.QGraphicsPathItem.__init__(self, opts.get('parent', None))
|
||||
|
||||
if 'size' in opts:
|
||||
opts['headLen'] = opts['size']
|
||||
if 'width' in opts:
|
||||
opts['headWidth'] = opts['width']
|
||||
defOpts = {
|
||||
defaultOpts = {
|
||||
'pxMode': True,
|
||||
'angle': -150, ## If the angle is 0, the arrow points left
|
||||
'pos': (0,0),
|
||||
@ -33,12 +35,9 @@ class ArrowItem(QtGui.QGraphicsPathItem):
|
||||
'pen': (200,200,200),
|
||||
'brush': (50,50,200),
|
||||
}
|
||||
defOpts.update(opts)
|
||||
defaultOpts.update(opts)
|
||||
|
||||
self.setStyle(**defOpts)
|
||||
|
||||
self.setPen(fn.mkPen(defOpts['pen']))
|
||||
self.setBrush(fn.mkBrush(defOpts['brush']))
|
||||
self.setStyle(**defaultOpts)
|
||||
|
||||
self.rotate(self.opts['angle'])
|
||||
self.moveBy(*self.opts['pos'])
|
||||
@ -48,8 +47,8 @@ class ArrowItem(QtGui.QGraphicsPathItem):
|
||||
Changes the appearance of the arrow.
|
||||
All arguments are optional:
|
||||
|
||||
================= =================================================
|
||||
Keyword Arguments
|
||||
====================== =================================================
|
||||
**Keyword Arguments:**
|
||||
angle Orientation of the arrow in degrees. Default is
|
||||
0; arrow pointing to the left.
|
||||
headLen Length of the arrow head, from tip to base.
|
||||
@ -60,23 +59,26 @@ class ArrowItem(QtGui.QGraphicsPathItem):
|
||||
specified, ot overrides headWidth. default=25
|
||||
baseAngle Angle of the base of the arrow head. Default is
|
||||
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
|
||||
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.
|
||||
default=None
|
||||
tailWidth Width of the tail. default=3
|
||||
pen The pen used to draw the outline of 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']])
|
||||
self.path = fn.makeArrowPath(**opt)
|
||||
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)
|
||||
else:
|
||||
self.setFlags(self.flags() & ~self.ItemIgnoresTransformations)
|
||||
|
@ -33,7 +33,6 @@ class AxisItem(GraphicsWidget):
|
||||
|
||||
GraphicsWidget.__init__(self, parent)
|
||||
self.label = QtGui.QGraphicsTextItem(self)
|
||||
self.showValues = showValues
|
||||
self.picture = None
|
||||
self.orientation = orientation
|
||||
if orientation not in ['left', 'right', 'top', 'bottom']:
|
||||
@ -42,7 +41,7 @@ class AxisItem(GraphicsWidget):
|
||||
self.label.rotate(-90)
|
||||
|
||||
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
|
||||
'tickTextHeight': 18,
|
||||
'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
|
||||
(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': showValues,
|
||||
'tickLength': maxTickLength,
|
||||
}
|
||||
|
||||
self.textWidth = 30 ## Keeps track of maximum width / height of tick text
|
||||
@ -66,7 +67,6 @@ class AxisItem(GraphicsWidget):
|
||||
self.logMode = False
|
||||
self.tickFont = None
|
||||
|
||||
self.tickLength = maxTickLength
|
||||
self._tickLevels = None ## used to override the automatic ticking system with explicit ticks
|
||||
self.scale = 1.0
|
||||
self.autoSIPrefix = True
|
||||
@ -74,6 +74,9 @@ class AxisItem(GraphicsWidget):
|
||||
|
||||
self.setRange(0, 1)
|
||||
|
||||
if pen is None:
|
||||
self.setPen()
|
||||
else:
|
||||
self.setPen(pen)
|
||||
|
||||
self._linkedView = None
|
||||
@ -85,6 +88,73 @@ class AxisItem(GraphicsWidget):
|
||||
self.grid = False
|
||||
#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):
|
||||
self.scene().removeItem(self.label)
|
||||
self.label = None
|
||||
@ -125,20 +195,15 @@ class AxisItem(GraphicsWidget):
|
||||
if self.orientation == 'left':
|
||||
p.setY(int(self.size().height()/2 + br.width()/2))
|
||||
p.setX(-nudge)
|
||||
#s.setWidth(10)
|
||||
elif self.orientation == 'right':
|
||||
#s.setWidth(10)
|
||||
p.setY(int(self.size().height()/2 + br.width()/2))
|
||||
p.setX(int(self.size().width()-br.height()+nudge))
|
||||
elif self.orientation == 'top':
|
||||
#s.setHeight(10)
|
||||
p.setY(-nudge)
|
||||
p.setX(int(self.size().width()/2. - br.width()/2.))
|
||||
elif self.orientation == 'bottom':
|
||||
p.setX(int(self.size().width()/2. - br.width()/2.))
|
||||
#s.setHeight(10)
|
||||
p.setY(int(self.size().height()-br.height()+nudge))
|
||||
#self.label.resize(s)
|
||||
self.label.setPos(p)
|
||||
self.picture = None
|
||||
|
||||
@ -156,8 +221,8 @@ class AxisItem(GraphicsWidget):
|
||||
def setLabel(self, text=None, units=None, unitPrefix=None, **args):
|
||||
"""Set the text displayed adjacent to the axis.
|
||||
|
||||
============= =============================================================
|
||||
Arguments
|
||||
============== =============================================================
|
||||
**Arguments:**
|
||||
text The text (excluding units) to display on the label for this
|
||||
axis.
|
||||
units The units for this axis. Units should generally be given
|
||||
@ -166,7 +231,7 @@ class AxisItem(GraphicsWidget):
|
||||
range of data displayed.
|
||||
**args All extra keyword arguments become CSS style options for
|
||||
the <span> tag which will surround the axis label and units.
|
||||
============= =============================================================
|
||||
============== =============================================================
|
||||
|
||||
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.
|
||||
The height of the axis label is automatically added."""
|
||||
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
|
||||
else:
|
||||
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():
|
||||
h += self.label.boundingRect().height() * 0.8
|
||||
self.setMaximumHeight(h)
|
||||
self.setMinimumHeight(h)
|
||||
self.picture = None
|
||||
|
||||
|
||||
def setWidth(self, w=None):
|
||||
"""Set the width of this axis reserved for ticks and tick labels.
|
||||
The width of the axis label is automatically added."""
|
||||
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
|
||||
else:
|
||||
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():
|
||||
w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate
|
||||
self.setMaximumWidth(w)
|
||||
@ -271,16 +341,17 @@ class AxisItem(GraphicsWidget):
|
||||
return fn.mkPen(getConfigOption('foreground'))
|
||||
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.
|
||||
if pen == None, the default will be used (see :func:`setConfigOption
|
||||
<pyqtgraph.setConfigOption>`)
|
||||
If no arguments are given, the default foreground color will be used
|
||||
(see :func:`setConfigOption <pyqtgraph.setConfigOption>`).
|
||||
"""
|
||||
self.picture = None
|
||||
if pen is None:
|
||||
pen = getConfigOption('foreground')
|
||||
self._pen = fn.mkPen(pen)
|
||||
if args or kwargs:
|
||||
self._pen = fn.mkPen(*args, **kwargs)
|
||||
else:
|
||||
self._pen = fn.mkPen(getConfigOption('foreground'))
|
||||
self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6]
|
||||
self.setLabel()
|
||||
self.update()
|
||||
@ -391,14 +462,15 @@ class AxisItem(GraphicsWidget):
|
||||
rect = self.mapRectFromParent(self.geometry())
|
||||
## extend rect if ticks go in negative direction
|
||||
## also extend to account for text that flows past the edges
|
||||
tl = self.style['tickLength']
|
||||
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':
|
||||
rect = rect.adjusted(min(0,self.tickLength), -15, 0, 15)
|
||||
rect = rect.adjusted(min(0,tl), -15, 0, 15)
|
||||
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':
|
||||
rect = rect.adjusted(-15, min(0,self.tickLength), 15, 0)
|
||||
rect = rect.adjusted(-15, min(0,tl), 15, 0)
|
||||
return rect
|
||||
else:
|
||||
return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect())
|
||||
@ -618,7 +690,7 @@ class AxisItem(GraphicsWidget):
|
||||
|
||||
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
|
||||
interpreted by drawPicture().
|
||||
"""
|
||||
@ -667,6 +739,7 @@ class AxisItem(GraphicsWidget):
|
||||
if lengthInPixels == 0:
|
||||
return
|
||||
|
||||
# Determine major / minor / subminor axis ticks
|
||||
if self._tickLevels is None:
|
||||
tickLevels = self.tickValues(self.range[0], self.range[1], lengthInPixels)
|
||||
tickStrings = None
|
||||
@ -688,7 +761,7 @@ class AxisItem(GraphicsWidget):
|
||||
## determine mapping between tick values and local coordinates
|
||||
dif = self.range[1] - self.range[0]
|
||||
if dif == 0:
|
||||
xscale = 1
|
||||
xScale = 1
|
||||
offset = 0
|
||||
else:
|
||||
if axis == 0:
|
||||
@ -706,8 +779,7 @@ class AxisItem(GraphicsWidget):
|
||||
|
||||
tickPositions = [] # remembers positions of previously drawn ticks
|
||||
|
||||
## draw ticks
|
||||
## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching)
|
||||
## compute coordinates to draw ticks
|
||||
## draw three different intervals, long ticks first
|
||||
tickSpecs = []
|
||||
for i in range(len(tickLevels)):
|
||||
@ -715,7 +787,7 @@ class AxisItem(GraphicsWidget):
|
||||
ticks = tickLevels[i][1]
|
||||
|
||||
## length of tick
|
||||
tickLength = self.tickLength / ((i*0.5)+1.0)
|
||||
tickLength = self.style['tickLength'] / ((i*0.5)+1.0)
|
||||
|
||||
lineAlpha = 255 / (i+1)
|
||||
if self.grid is not False:
|
||||
@ -742,7 +814,6 @@ class AxisItem(GraphicsWidget):
|
||||
tickSpecs.append((tickPen, Point(p1), Point(p2)))
|
||||
profiler('compute ticks')
|
||||
|
||||
## This is where the long axis line should be drawn
|
||||
|
||||
if self.style['stopAxisAtTick'][0] is True:
|
||||
stop = max(span[0].y(), min(map(min, tickPositions)))
|
||||
@ -759,7 +830,6 @@ class AxisItem(GraphicsWidget):
|
||||
axisSpec = (self.pen(), span[0], span[1])
|
||||
|
||||
|
||||
|
||||
textOffset = self.style['tickTextOffset'][axis] ## spacing between axis and text
|
||||
#if self.style['autoExpandTextSpace'] is True:
|
||||
#textWidth = self.textWidth
|
||||
@ -771,7 +841,11 @@ class AxisItem(GraphicsWidget):
|
||||
textSize2 = 0
|
||||
textRects = []
|
||||
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)):
|
||||
## Get the list of strings to display for this level
|
||||
if tickStrings is None:
|
||||
@ -802,15 +876,15 @@ class AxisItem(GraphicsWidget):
|
||||
rects.append(br)
|
||||
textRects.append(rects[-1])
|
||||
|
||||
if i > 0: ## always draw top level
|
||||
## measure all text, make sure there's enough room
|
||||
if axis == 0:
|
||||
textSize = np.sum([r.height() for r in textRects])
|
||||
textSize2 = np.max([r.width() for r in textRects])
|
||||
textSize2 = np.max([r.width() for r in textRects]) if textRects else 0
|
||||
else:
|
||||
textSize = np.sum([r.width() for r in textRects])
|
||||
textSize2 = np.max([r.height() for r in textRects])
|
||||
textSize2 = np.max([r.height() for r in textRects]) if textRects else 0
|
||||
|
||||
if i > 0: ## always draw top level
|
||||
## If the strings are too crowded, stop drawing text now.
|
||||
## We use three different crowding limits based on the number
|
||||
## of texts drawn so far.
|
||||
@ -825,6 +899,7 @@ class AxisItem(GraphicsWidget):
|
||||
|
||||
#spacing, values = tickLevels[best]
|
||||
#strings = self.tickStrings(values, self.scale, spacing)
|
||||
# Determine exactly where tick text should be drawn
|
||||
for j in range(len(strings)):
|
||||
vstr = strings[j]
|
||||
if vstr is None: ## this tick was ignored because it is out of bounds
|
||||
@ -836,7 +911,7 @@ class AxisItem(GraphicsWidget):
|
||||
height = textRect.height()
|
||||
width = textRect.width()
|
||||
#self.textHeight = height
|
||||
offset = max(0,self.tickLength) + textOffset
|
||||
offset = max(0,self.style['tickLength']) + textOffset
|
||||
if self.orientation == 'left':
|
||||
textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter
|
||||
rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height)
|
||||
|
@ -47,16 +47,20 @@ class BarGraphItem(GraphicsObject):
|
||||
pens=None,
|
||||
brushes=None,
|
||||
)
|
||||
self._shape = None
|
||||
self.picture = None
|
||||
self.setOpts(**opts)
|
||||
|
||||
def setOpts(self, **opts):
|
||||
self.opts.update(opts)
|
||||
self.picture = None
|
||||
self._shape = None
|
||||
self.update()
|
||||
self.informViewBoundsChanged()
|
||||
|
||||
def drawPicture(self):
|
||||
self.picture = QtGui.QPicture()
|
||||
self._shape = QtGui.QPainterPath()
|
||||
p = QtGui.QPainter(self.picture)
|
||||
|
||||
pen = self.opts['pen']
|
||||
@ -122,6 +126,10 @@ class BarGraphItem(GraphicsObject):
|
||||
if brushes is not None:
|
||||
p.setBrush(fn.mkBrush(brushes[i]))
|
||||
|
||||
if np.isscalar(x0):
|
||||
x = x0
|
||||
else:
|
||||
x = x0[i]
|
||||
if np.isscalar(y0):
|
||||
y = y0
|
||||
else:
|
||||
@ -130,9 +138,15 @@ class BarGraphItem(GraphicsObject):
|
||||
w = width
|
||||
else:
|
||||
w = width[i]
|
||||
if np.isscalar(height):
|
||||
h = height
|
||||
else:
|
||||
h = height[i]
|
||||
|
||||
p.drawRect(QtCore.QRectF(x0[i], y, w, height[i]))
|
||||
|
||||
rect = QtCore.QRectF(x, y, w, h)
|
||||
p.drawRect(rect)
|
||||
self._shape.addRect(rect)
|
||||
|
||||
p.end()
|
||||
self.prepareGeometryChange()
|
||||
@ -148,4 +162,7 @@ class BarGraphItem(GraphicsObject):
|
||||
self.drawPicture()
|
||||
return QtCore.QRectF(self.picture.boundingRect())
|
||||
|
||||
|
||||
def shape(self):
|
||||
if self.picture is None:
|
||||
self.drawPicture()
|
||||
return self._shape
|
||||
|
@ -112,6 +112,6 @@ class CurveArrow(CurvePoint):
|
||||
self.arrow = ArrowItem.ArrowItem(**opts)
|
||||
self.arrow.setParentItem(self)
|
||||
|
||||
def setStyle(**opts):
|
||||
def setStyle(self, **opts):
|
||||
return self.arrow.setStyle(**opts)
|
||||
|
||||
|
@ -22,13 +22,16 @@ class FillBetweenItem(QtGui.QGraphicsPathItem):
|
||||
def setCurves(self, curve1, curve2):
|
||||
"""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:
|
||||
for c in self.curves:
|
||||
try:
|
||||
c.sigPlotChanged.disconnect(self.curveChanged)
|
||||
except TypeError:
|
||||
except (TypeError, RuntimeError):
|
||||
pass
|
||||
|
||||
curves = [curve1, curve2]
|
||||
|
@ -35,14 +35,14 @@ class TickSliderItem(GraphicsWidget):
|
||||
|
||||
def __init__(self, orientation='bottom', allowAdd=True, **kargs):
|
||||
"""
|
||||
============= =================================================================================
|
||||
**Arguments**
|
||||
============== =================================================================================
|
||||
**Arguments:**
|
||||
orientation Set the orientation of the gradient. Options are: 'left', 'right'
|
||||
'top', and 'bottom'.
|
||||
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.
|
||||
Can be any of the valid arguments for :func:`mkPen <pyqtgraph.mkPen>`
|
||||
============= =================================================================================
|
||||
============== =================================================================================
|
||||
"""
|
||||
## public
|
||||
GraphicsWidget.__init__(self)
|
||||
@ -103,13 +103,13 @@ class TickSliderItem(GraphicsWidget):
|
||||
## public
|
||||
"""Set the orientation of the TickSliderItem.
|
||||
|
||||
============= ===================================================================
|
||||
**Arguments**
|
||||
============== ===================================================================
|
||||
**Arguments:**
|
||||
orientation Options are: 'left', 'right', 'top', 'bottom'
|
||||
The orientation option specifies which side of the slider the
|
||||
ticks are on, as well as whether the slider is vertical ('right'
|
||||
and 'left') or horizontal ('top' and 'bottom').
|
||||
============= ===================================================================
|
||||
============== ===================================================================
|
||||
"""
|
||||
self.orientation = orientation
|
||||
self.setMaxDim()
|
||||
@ -136,13 +136,13 @@ class TickSliderItem(GraphicsWidget):
|
||||
"""
|
||||
Add a tick to the item.
|
||||
|
||||
============= ==================================================================
|
||||
**Arguments**
|
||||
============== ==================================================================
|
||||
**Arguments:**
|
||||
x Position where tick should be added.
|
||||
color Color of added tick. If color is not specified, the color will be
|
||||
white.
|
||||
movable Specifies whether the tick is movable with the mouse.
|
||||
============= ==================================================================
|
||||
============== ==================================================================
|
||||
"""
|
||||
|
||||
if color is None:
|
||||
@ -265,14 +265,14 @@ class TickSliderItem(GraphicsWidget):
|
||||
def setTickColor(self, tick, color):
|
||||
"""Set the color of the specified tick.
|
||||
|
||||
============= ==================================================================
|
||||
**Arguments**
|
||||
============== ==================================================================
|
||||
**Arguments:**
|
||||
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
|
||||
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
|
||||
:func:`mkBrush <pyqtgraph.mkBrush>`
|
||||
============= ==================================================================
|
||||
============== ==================================================================
|
||||
"""
|
||||
tick = self.getTick(tick)
|
||||
tick.color = color
|
||||
@ -284,14 +284,14 @@ class TickSliderItem(GraphicsWidget):
|
||||
"""
|
||||
Set the position (along the slider) of the tick.
|
||||
|
||||
============= ==================================================================
|
||||
**Arguments**
|
||||
============== ==================================================================
|
||||
**Arguments:**
|
||||
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
|
||||
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
|
||||
set to 0. If val is > 1, position will be set to 1.
|
||||
============= ==================================================================
|
||||
============== ==================================================================
|
||||
"""
|
||||
tick = self.getTick(tick)
|
||||
val = min(max(0.0, val), 1.0)
|
||||
@ -305,12 +305,12 @@ class TickSliderItem(GraphicsWidget):
|
||||
## public
|
||||
"""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
|
||||
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.
|
||||
============= ==================================================================
|
||||
============== ==================================================================
|
||||
"""
|
||||
tick = self.getTick(tick)
|
||||
return self.ticks[tick]
|
||||
@ -319,11 +319,11 @@ class TickSliderItem(GraphicsWidget):
|
||||
## public
|
||||
"""Return the Tick object at the specified index.
|
||||
|
||||
============= ==================================================================
|
||||
**Arguments**
|
||||
============== ==================================================================
|
||||
**Arguments:**
|
||||
tick An integer corresponding to the index of the desired tick. If the
|
||||
argument is not an integer it will be returned unchanged.
|
||||
============= ==================================================================
|
||||
============== ==================================================================
|
||||
"""
|
||||
if type(tick) is int:
|
||||
tick = self.listTicks()[tick][0]
|
||||
@ -349,7 +349,7 @@ class GradientEditorItem(TickSliderItem):
|
||||
with a GradientEditorItem that can be added to a GUI.
|
||||
|
||||
================================ ===========================================================
|
||||
**Signals**
|
||||
**Signals:**
|
||||
sigGradientChanged(self) Signal is emitted anytime the gradient changes. The signal
|
||||
is emitted in real time while ticks are being dragged or
|
||||
colors are being changed.
|
||||
@ -366,14 +366,14 @@ class GradientEditorItem(TickSliderItem):
|
||||
Create a new GradientEditorItem.
|
||||
All arguments are passed to :func:`TickSliderItem.__init__ <pyqtgraph.TickSliderItem.__init__>`
|
||||
|
||||
============= =================================================================================
|
||||
**Arguments**
|
||||
=============== =================================================================================
|
||||
**Arguments:**
|
||||
orientation Set the orientation of the gradient. Options are: 'left', 'right'
|
||||
'top', and 'bottom'.
|
||||
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.
|
||||
Can be any of the valid arguments for :func:`mkPen <pyqtgraph.mkPen>`
|
||||
============= =================================================================================
|
||||
=============== =================================================================================
|
||||
"""
|
||||
self.currentTick = None
|
||||
self.currentTickColor = None
|
||||
@ -445,13 +445,13 @@ class GradientEditorItem(TickSliderItem):
|
||||
"""
|
||||
Set the orientation of the GradientEditorItem.
|
||||
|
||||
============= ===================================================================
|
||||
**Arguments**
|
||||
============== ===================================================================
|
||||
**Arguments:**
|
||||
orientation Options are: 'left', 'right', 'top', 'bottom'
|
||||
The orientation option specifies which side of the gradient the
|
||||
ticks are on, as well as whether the gradient is vertical ('right'
|
||||
and 'left') or horizontal ('top' and 'bottom').
|
||||
============= ===================================================================
|
||||
============== ===================================================================
|
||||
"""
|
||||
TickSliderItem.setOrientation(self, orientation)
|
||||
self.translate(0, self.rectSize)
|
||||
@ -588,11 +588,11 @@ class GradientEditorItem(TickSliderItem):
|
||||
"""
|
||||
Return a color for a given value.
|
||||
|
||||
============= ==================================================================
|
||||
**Arguments**
|
||||
============== ==================================================================
|
||||
**Arguments:**
|
||||
x Value (position on gradient) of requested color.
|
||||
toQColor If true, returns a QColor object, else returns a (r,g,b,a) tuple.
|
||||
============= ==================================================================
|
||||
============== ==================================================================
|
||||
"""
|
||||
ticks = self.listTicks()
|
||||
if x <= ticks[0][1]:
|
||||
@ -648,12 +648,12 @@ class GradientEditorItem(TickSliderItem):
|
||||
"""
|
||||
Return an RGB(A) lookup table (ndarray).
|
||||
|
||||
============= ============================================================================
|
||||
**Arguments**
|
||||
============== ============================================================================
|
||||
**Arguments:**
|
||||
nPts The number of points in the returned lookup table.
|
||||
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.
|
||||
============= ============================================================================
|
||||
============== ============================================================================
|
||||
"""
|
||||
if alpha is None:
|
||||
alpha = self.usesAlpha()
|
||||
@ -702,13 +702,13 @@ class GradientEditorItem(TickSliderItem):
|
||||
"""
|
||||
Add a tick to the gradient. Return the tick.
|
||||
|
||||
============= ==================================================================
|
||||
**Arguments**
|
||||
============== ==================================================================
|
||||
**Arguments:**
|
||||
x Position where tick should be added.
|
||||
color Color of added tick. If color is not specified, the color will be
|
||||
the color of the gradient at the specified position.
|
||||
movable Specifies whether the tick is movable with the mouse.
|
||||
============= ==================================================================
|
||||
============== ==================================================================
|
||||
"""
|
||||
|
||||
|
||||
@ -748,8 +748,8 @@ class GradientEditorItem(TickSliderItem):
|
||||
"""
|
||||
Restore the gradient specified in state.
|
||||
|
||||
============= ====================================================================
|
||||
**Arguments**
|
||||
============== ====================================================================
|
||||
**Arguments:**
|
||||
state A dictionary with same structure as those returned by
|
||||
:func:`saveState <pyqtgraph.GradientEditorItem.saveState>`
|
||||
|
||||
@ -757,7 +757,7 @@ class GradientEditorItem(TickSliderItem):
|
||||
|
||||
- 'mode': hsv or rgb
|
||||
- 'ticks': a list of tuples (pos, (r,g,b,a))
|
||||
============= ====================================================================
|
||||
============== ====================================================================
|
||||
"""
|
||||
## public
|
||||
self.setColorMode(state['mode'])
|
||||
|
@ -28,8 +28,8 @@ class GraphItem(GraphicsObject):
|
||||
"""
|
||||
Change the data displayed by the graph.
|
||||
|
||||
============ =========================================================
|
||||
Arguments
|
||||
============== =======================================================================
|
||||
**Arguments:**
|
||||
pos (N,2) array of the positions of each node in the graph.
|
||||
adj (M,2) array of connection data. Each row contains indexes
|
||||
of two nodes that are connected.
|
||||
@ -45,31 +45,55 @@ class GraphItem(GraphicsObject):
|
||||
* None (to disable connection drawing)
|
||||
* 'default' to use the default foreground color.
|
||||
|
||||
symbolPen The pen used for drawing nodes.
|
||||
symbolPen The pen(s) used for drawing nodes.
|
||||
symbolBrush The brush(es) used for drawing nodes.
|
||||
``**opts`` All other keyword arguments are given to
|
||||
:func:`ScatterPlotItem.setData() <pyqtgraph.ScatterPlotItem.setData>`
|
||||
to affect the appearance of nodes (symbol, size, brush,
|
||||
etc.)
|
||||
============ =========================================================
|
||||
============== =======================================================================
|
||||
"""
|
||||
if 'adj' in kwds:
|
||||
self.adjacency = kwds.pop('adj')
|
||||
assert self.adjacency.dtype.kind in 'iu'
|
||||
self.picture = None
|
||||
if self.adjacency.dtype.kind not in 'iu':
|
||||
raise Exception("adjacency array must have int or unsigned type.")
|
||||
self._update()
|
||||
if 'pos' in kwds:
|
||||
self.pos = kwds['pos']
|
||||
self.picture = None
|
||||
self._update()
|
||||
if 'pen' in kwds:
|
||||
self.setPen(kwds.pop('pen'))
|
||||
self.picture = None
|
||||
self._update()
|
||||
|
||||
if 'symbolPen' in kwds:
|
||||
kwds['pen'] = kwds.pop('symbolPen')
|
||||
if 'symbolBrush' in kwds:
|
||||
kwds['brush'] = kwds.pop('symbolBrush')
|
||||
self.scatter.setData(**kwds)
|
||||
self.informViewBoundsChanged()
|
||||
|
||||
def setPen(self, pen):
|
||||
self.pen = pen
|
||||
def _update(self):
|
||||
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):
|
||||
self.picture = QtGui.QPicture()
|
||||
|
@ -1,30 +1,10 @@
|
||||
from ..Qt import QtGui, QtCore
|
||||
from ..Qt import QtGui, QtCore, isQObjectAlive
|
||||
from ..GraphicsScene import GraphicsScene
|
||||
from ..Point import Point
|
||||
from .. import functions as fn
|
||||
import weakref
|
||||
from ..pgcollections import OrderedDict
|
||||
import operator, sys
|
||||
|
||||
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
|
||||
|
||||
import operator
|
||||
from ..util.lru_cache import LRUCache
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
_pixelVectorGlobalCache = FiniteCache(100)
|
||||
_pixelVectorGlobalCache = LRUCache(100, 70)
|
||||
|
||||
def __init__(self, register=True):
|
||||
if not hasattr(self, '_qtBaseClass'):
|
||||
@ -62,8 +42,11 @@ class GraphicsItem(object):
|
||||
|
||||
def getViewWidget(self):
|
||||
"""
|
||||
Return the view widget for this item. If the scene has multiple views, only the first view is returned.
|
||||
The return value is cached; clear the cached value with forgetViewWidget()
|
||||
Return the view widget for this item.
|
||||
|
||||
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:
|
||||
scene = self.scene()
|
||||
@ -73,7 +56,12 @@ class GraphicsItem(object):
|
||||
if len(views) < 1:
|
||||
return None
|
||||
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):
|
||||
self._viewWidget = None
|
||||
@ -479,15 +467,14 @@ class GraphicsItem(object):
|
||||
|
||||
## disconnect from previous view
|
||||
if oldView is not None:
|
||||
#print "disconnect:", self, oldView
|
||||
for signal, slot in [('sigRangeChanged', self.viewRangeChanged),
|
||||
('sigDeviceRangeChanged', self.viewRangeChanged),
|
||||
('sigTransformChanged', self.viewTransformChanged),
|
||||
('sigDeviceTransformChanged', self.viewTransformChanged)]:
|
||||
try:
|
||||
oldView.sigRangeChanged.disconnect(self.viewRangeChanged)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
oldView.sigTransformChanged.disconnect(self.viewTransformChanged)
|
||||
except TypeError:
|
||||
getattr(oldView, signal).disconnect(slot)
|
||||
except (TypeError, AttributeError, RuntimeError):
|
||||
# TypeError and RuntimeError are from pyqt and pyside, respectively
|
||||
pass
|
||||
|
||||
self._connectedView = None
|
||||
@ -495,6 +482,12 @@ class GraphicsItem(object):
|
||||
## connect to new view
|
||||
if view is not None:
|
||||
#print "connect:", self, view
|
||||
if hasattr(view, 'sigDeviceRangeChanged'):
|
||||
# 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)
|
||||
|
@ -32,6 +32,15 @@ class GraphicsLayout(GraphicsWidget):
|
||||
#print self.pos(), self.mapToDevice(self.rect().topLeft())
|
||||
#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):
|
||||
"""Advance to next row for automatic item placement"""
|
||||
self.currentRow += 1
|
||||
|
@ -21,7 +21,14 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
|
||||
ret = QtGui.QGraphicsObject.itemChange(self, change, value)
|
||||
if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
|
||||
self.parentChanged()
|
||||
if self.__inform_view_on_changes and change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
|
||||
try:
|
||||
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:
|
||||
|
@ -58,7 +58,7 @@ class HistogramLUTItem(GraphicsWidget):
|
||||
self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal)
|
||||
self.region.setZValue(1000)
|
||||
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.vb, 0, 1)
|
||||
self.layout.addItem(self.gradient, 0, 2)
|
||||
|
@ -6,6 +6,7 @@ import collections
|
||||
from .. import functions as fn
|
||||
from .. import debug as debug
|
||||
from .GraphicsObject import GraphicsObject
|
||||
from ..Point import Point
|
||||
|
||||
__all__ = ['ImageItem']
|
||||
class ImageItem(GraphicsObject):
|
||||
@ -34,20 +35,16 @@ class ImageItem(GraphicsObject):
|
||||
See :func:`setImage <pyqtgraph.ImageItem.setImage>` for all allowed initialization arguments.
|
||||
"""
|
||||
GraphicsObject.__init__(self)
|
||||
#self.pixmapItem = QtGui.QGraphicsPixmapItem(self)
|
||||
#self.qimage = QtGui.QImage()
|
||||
#self._pixmap = None
|
||||
self.menu = None
|
||||
self.image = None ## original image data
|
||||
self.qimage = None ## rendered image for display
|
||||
#self.clipMask = None
|
||||
|
||||
self.paintMode = None
|
||||
|
||||
self.levels = None ## [min, max] or [[redMin, redMax], ...]
|
||||
self.lut = None
|
||||
self.autoDownsample = False
|
||||
|
||||
#self.clipLevel = None
|
||||
self.drawKernel = None
|
||||
self.border = None
|
||||
self.removable = False
|
||||
@ -142,7 +139,18 @@ class ImageItem(GraphicsObject):
|
||||
if update:
|
||||
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):
|
||||
|
||||
if 'lut' in kargs:
|
||||
self.setLookupTable(kargs['lut'], update=update)
|
||||
if 'levels' in kargs:
|
||||
@ -158,6 +166,10 @@ class ImageItem(GraphicsObject):
|
||||
if 'removable' in kargs:
|
||||
self.removable = kargs['removable']
|
||||
self.menu = None
|
||||
if 'autoDownsample' in kargs:
|
||||
self.setAutoDownsample(kargs['autoDownsample'])
|
||||
if update:
|
||||
self.update()
|
||||
|
||||
def setRect(self, rect):
|
||||
"""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)
|
||||
compositionMode see :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>`
|
||||
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()
|
||||
@ -200,6 +215,9 @@ class ImageItem(GraphicsObject):
|
||||
gotNewData = True
|
||||
shapeChanged = (self.image is None or image.shape != self.image.shape)
|
||||
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:
|
||||
self.prepareGeometryChange()
|
||||
self.informViewBoundsChanged()
|
||||
@ -247,10 +265,9 @@ class ImageItem(GraphicsObject):
|
||||
defaults.update(kargs)
|
||||
return self.setImage(*args, **defaults)
|
||||
|
||||
|
||||
|
||||
|
||||
def render(self):
|
||||
# Convert data to QImage for display.
|
||||
|
||||
profile = debug.Profiler()
|
||||
if self.image is None or self.image.size == 0:
|
||||
return
|
||||
@ -258,10 +275,22 @@ class ImageItem(GraphicsObject):
|
||||
lut = self.lut(self.image)
|
||||
else:
|
||||
lut = self.lut
|
||||
#print lut.shape
|
||||
#print self.lut
|
||||
|
||||
argb, alpha = fn.makeARGB(self.image.transpose((1, 0, 2)[:self.image.ndim]), lut=lut, levels=self.levels)
|
||||
if self.autoDownsample:
|
||||
# reduce dimensions of image based on screen resolution
|
||||
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)
|
||||
|
||||
def paint(self, p, *args):
|
||||
@ -277,7 +306,7 @@ class ImageItem(GraphicsObject):
|
||||
p.setCompositionMode(self.paintMode)
|
||||
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')
|
||||
if self.border is not None:
|
||||
p.setPen(self.border)
|
||||
@ -322,6 +351,8 @@ class ImageItem(GraphicsObject):
|
||||
mx = stepData.max()
|
||||
step = np.ceil((mx-mn) / 500.)
|
||||
bins = np.arange(mn, mx+1.01*step, step, dtype=np.int)
|
||||
if len(bins) == 0:
|
||||
bins = [mn, mx]
|
||||
else:
|
||||
bins = 500
|
||||
|
||||
@ -356,6 +387,11 @@ class ImageItem(GraphicsObject):
|
||||
return 1,1
|
||||
return br.width()/self.width(), br.height()/self.height()
|
||||
|
||||
def viewTransformChanged(self):
|
||||
if self.autoDownsample:
|
||||
self.qimage = None
|
||||
self.update()
|
||||
|
||||
#def mousePressEvent(self, ev):
|
||||
#if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton:
|
||||
#self.drawAt(ev.pos(), ev)
|
||||
|
@ -15,7 +15,7 @@ class InfiniteLine(GraphicsObject):
|
||||
This line may be dragged to indicate a position in data coordinates.
|
||||
|
||||
=============================== ===================================================
|
||||
**Signals**
|
||||
**Signals:**
|
||||
sigDragged(self)
|
||||
sigPositionChangeFinished(self)
|
||||
sigPositionChanged(self)
|
||||
@ -28,8 +28,8 @@ class InfiniteLine(GraphicsObject):
|
||||
|
||||
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
|
||||
vertical/horizontal lines.
|
||||
angle Angle of line in degrees. 0 is horizontal, 90 is vertical.
|
||||
@ -39,7 +39,7 @@ class InfiniteLine(GraphicsObject):
|
||||
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
|
||||
line is vertical or horizontal.
|
||||
============= ==================================================================
|
||||
=============== ==================================================================
|
||||
"""
|
||||
|
||||
GraphicsObject.__init__(self)
|
||||
@ -73,10 +73,10 @@ class InfiniteLine(GraphicsObject):
|
||||
self.maxRange = bounds
|
||||
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
|
||||
for :func:`mkPen <pyqtgraph.mkPen>`."""
|
||||
self.pen = fn.mkPen(pen)
|
||||
self.pen = fn.mkPen(*args, **kwargs)
|
||||
self.currentPen = self.pen
|
||||
self.update()
|
||||
|
||||
|
@ -18,14 +18,14 @@ class IsocurveItem(GraphicsObject):
|
||||
"""
|
||||
Create a new isocurve item.
|
||||
|
||||
============= ===============================================================
|
||||
**Arguments**
|
||||
============== ===============================================================
|
||||
**Arguments:**
|
||||
data A 2-dimensional ndarray. Can be initialized as None, and set
|
||||
later using :func:`setData <pyqtgraph.IsocurveItem.setData>`
|
||||
level The cutoff value at which to draw the isocurve.
|
||||
pen The color of the curve item. Can be anything valid for
|
||||
:func:`mkPen <pyqtgraph.mkPen>`
|
||||
============= ===============================================================
|
||||
============== ===============================================================
|
||||
"""
|
||||
GraphicsObject.__init__(self)
|
||||
|
||||
@ -45,12 +45,12 @@ class IsocurveItem(GraphicsObject):
|
||||
"""
|
||||
Set the data/image to draw isocurves for.
|
||||
|
||||
============= ========================================================================
|
||||
**Arguments**
|
||||
============== ========================================================================
|
||||
**Arguments:**
|
||||
data A 2-dimensional ndarray.
|
||||
level The cutoff value at which to draw the curve. If level is not specified,
|
||||
the previously set level is used.
|
||||
============= ========================================================================
|
||||
============== ========================================================================
|
||||
"""
|
||||
if level is None:
|
||||
level = self.level
|
||||
|
@ -21,8 +21,8 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
|
||||
"""
|
||||
def __init__(self, size=None, offset=None):
|
||||
"""
|
||||
========== ===============================================================
|
||||
Arguments
|
||||
============== ===============================================================
|
||||
**Arguments:**
|
||||
size Specifies the fixed size (width, height) of the legend. If
|
||||
this argument is omitted, the legend will autimatically resize
|
||||
to fit its contents.
|
||||
@ -31,7 +31,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
|
||||
offset from the right or bottom. If offset is None, the
|
||||
legend must be anchored manually by calling anchor() or
|
||||
positioned by calling setPos().
|
||||
========== ===============================================================
|
||||
============== ===============================================================
|
||||
|
||||
"""
|
||||
|
||||
@ -61,14 +61,14 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
|
||||
"""
|
||||
Add a new entry to the legend.
|
||||
|
||||
=========== ========================================================
|
||||
Arguments
|
||||
============== ========================================================
|
||||
**Arguments:**
|
||||
item A PlotDataItem from which the line and point style
|
||||
of the item will be determined or an instance of
|
||||
ItemSample (or a subclass), allowing the item display
|
||||
to be customized.
|
||||
title The title to display for this item. Simple HTML allowed.
|
||||
=========== ========================================================
|
||||
============== ========================================================
|
||||
"""
|
||||
label = LabelItem(name)
|
||||
if isinstance(item, ItemSample):
|
||||
@ -85,10 +85,10 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
|
||||
"""
|
||||
Removes one item from the legend.
|
||||
|
||||
=========== ========================================================
|
||||
Arguments
|
||||
============== ========================================================
|
||||
**Arguments:**
|
||||
title The title displayed for this item.
|
||||
=========== ========================================================
|
||||
============== ========================================================
|
||||
"""
|
||||
# Thanks, Ulrich!
|
||||
# cycle for a match
|
||||
|
@ -30,8 +30,8 @@ class LinearRegionItem(UIGraphicsItem):
|
||||
def __init__(self, values=[0,1], orientation=None, brush=None, movable=True, bounds=None):
|
||||
"""Create a new LinearRegionItem.
|
||||
|
||||
============= =====================================================================
|
||||
**Arguments**
|
||||
============== =====================================================================
|
||||
**Arguments:**
|
||||
values A list of the positions of the lines in the region. These are not
|
||||
limits; limits can be set by specifying bounds.
|
||||
orientation Options are LinearRegionItem.Vertical or LinearRegionItem.Horizontal.
|
||||
@ -42,7 +42,7 @@ class LinearRegionItem(UIGraphicsItem):
|
||||
movable If True, the region and individual lines are movable by the user; if
|
||||
False, they are static.
|
||||
bounds Optional [min, max] bounding values for the region
|
||||
============= =====================================================================
|
||||
============== =====================================================================
|
||||
"""
|
||||
|
||||
UIGraphicsItem.__init__(self)
|
||||
@ -89,10 +89,10 @@ class LinearRegionItem(UIGraphicsItem):
|
||||
def setRegion(self, rgn):
|
||||
"""Set the values for the edges of the region.
|
||||
|
||||
============= ==============================================
|
||||
**Arguments**
|
||||
============== ==============================================
|
||||
**Arguments:**
|
||||
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]:
|
||||
return
|
||||
|
@ -7,26 +7,23 @@ Distributed under MIT/X11 license. See license.txt for more infomation.
|
||||
|
||||
from numpy import ndarray
|
||||
from . import GraphicsLayout
|
||||
|
||||
try:
|
||||
from metaarray import *
|
||||
HAVE_METAARRAY = True
|
||||
except:
|
||||
#raise
|
||||
HAVE_METAARRAY = False
|
||||
from ..metaarray import *
|
||||
|
||||
|
||||
__all__ = ['MultiPlotItem']
|
||||
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):
|
||||
#self.layout.clear()
|
||||
self.plots = []
|
||||
|
||||
if HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||
if hasattr(data, 'implements') and data.implements('MetaArray'):
|
||||
if data.ndim != 2:
|
||||
raise Exception("MultiPlot currently only accepts 2D MetaArray.")
|
||||
ic = data.infoCopy()
|
||||
@ -44,18 +41,14 @@ class MultiPlotItem(GraphicsLayout.GraphicsLayout):
|
||||
pi.plot(data[tuple(sl)])
|
||||
#self.layout.addItem(pi, i, 0)
|
||||
self.plots.append((pi, i, 0))
|
||||
title = None
|
||||
units = None
|
||||
info = ic[ax]['cols'][i]
|
||||
if 'title' in info:
|
||||
title = info['title']
|
||||
elif 'name' in info:
|
||||
title = info['name']
|
||||
if 'units' in info:
|
||||
units = info['units']
|
||||
|
||||
title = info.get('title', info.get('name', None))
|
||||
units = info.get('units', None)
|
||||
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:
|
||||
raise Exception("Data type %s not (yet?) supported for MultiPlot." % type(data))
|
||||
|
||||
|
@ -173,8 +173,14 @@ class PlotCurveItem(GraphicsObject):
|
||||
if pxPad > 0:
|
||||
# determine length of pixel in local x, y directions
|
||||
px, py = self.pixelVectors()
|
||||
try:
|
||||
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
|
||||
px *= pxPad
|
||||
@ -486,7 +492,7 @@ class PlotCurveItem(GraphicsObject):
|
||||
gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP)
|
||||
|
||||
## draw stencil pattern
|
||||
gl.glStencilMask(0xFF);
|
||||
gl.glStencilMask(0xFF)
|
||||
gl.glClear(gl.GL_STENCIL_BUFFER_BIT)
|
||||
gl.glBegin(gl.GL_TRIANGLES)
|
||||
gl.glVertex2f(rect.x(), rect.y())
|
||||
@ -520,7 +526,7 @@ class PlotCurveItem(GraphicsObject):
|
||||
gl.glEnable(gl.GL_LINE_SMOOTH)
|
||||
gl.glEnable(gl.GL_BLEND)
|
||||
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])
|
||||
finally:
|
||||
gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
|
||||
|
@ -56,9 +56,10 @@ class PlotDataItem(GraphicsObject):
|
||||
=========================== =========================================
|
||||
|
||||
**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
|
||||
: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.
|
||||
May be any single argument accepted by :func:`mkPen() <pyqtgraph.mkPen>`
|
||||
@ -67,21 +68,29 @@ class PlotDataItem(GraphicsObject):
|
||||
fillLevel Fill the area between the curve and fillLevel
|
||||
fillBrush Fill to use when fillLevel is specified.
|
||||
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)
|
||||
|
||||
============ ================================================
|
||||
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
|
||||
symbolPen Outline pen for drawing points OR list of pens, one per point.
|
||||
May be any single argument accepted by :func:`mkPen() <pyqtgraph.mkPen>`
|
||||
symbolBrush Brush for filling points OR list of brushes, one per point.
|
||||
May be any single argument accepted by :func:`mkBrush() <pyqtgraph.mkBrush>`
|
||||
symbolPen Outline pen for drawing points OR list of pens, one
|
||||
per point. May be any single argument accepted by
|
||||
:func:`mkPen() <pyqtgraph.mkPen>`
|
||||
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.
|
||||
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.
|
||||
============ ================================================
|
||||
============ =====================================================
|
||||
|
||||
**Optimization keyword arguments:**
|
||||
|
||||
@ -145,6 +154,7 @@ class PlotDataItem(GraphicsObject):
|
||||
'shadowPen': None,
|
||||
'fillLevel': None,
|
||||
'fillBrush': None,
|
||||
'stepMode': None,
|
||||
|
||||
'symbol': None,
|
||||
'symbolSize': 10,
|
||||
@ -290,8 +300,8 @@ class PlotDataItem(GraphicsObject):
|
||||
Set the downsampling mode of this item. Downsampling reduces the number
|
||||
of samples drawn to increase performance.
|
||||
|
||||
=========== =================================================================
|
||||
Arguments
|
||||
============== =================================================================
|
||||
**Arguments:**
|
||||
ds (int) Reduce visible plot samples by this factor. To disable,
|
||||
set ds=1.
|
||||
auto (bool) If True, automatically pick *ds* based on visible range
|
||||
@ -301,7 +311,7 @@ class PlotDataItem(GraphicsObject):
|
||||
'peak': Downsample by drawing a saw wave that follows the min
|
||||
and max of the original data. This method produces the best
|
||||
visual representation of the data but is slower.
|
||||
=========== =================================================================
|
||||
============== =================================================================
|
||||
"""
|
||||
changed = False
|
||||
if ds is not None:
|
||||
@ -451,7 +461,7 @@ class PlotDataItem(GraphicsObject):
|
||||
def updateItems(self):
|
||||
|
||||
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]
|
||||
|
||||
scatterArgs = {}
|
||||
@ -527,6 +537,7 @@ class PlotDataItem(GraphicsObject):
|
||||
x0 = (range.left()-x[0]) / dx
|
||||
x1 = (range.right()-x[0]) / dx
|
||||
width = self.getViewBox().width()
|
||||
if width != 0.0:
|
||||
ds = int(max(1, int(0.2 * (x1-x0) / width)))
|
||||
## downsampling is expensive; delay until after clipping.
|
||||
|
||||
@ -646,13 +657,12 @@ class PlotDataItem(GraphicsObject):
|
||||
|
||||
def _fourierTransform(self, x, y):
|
||||
## 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)
|
||||
uniform = not np.any(np.abs(dx-dx[0]) > (abs(dx[0]) / 1000.))
|
||||
if not uniform:
|
||||
import scipy.interpolate as interp
|
||||
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
|
||||
f = np.fft.fft(y) / len(y)
|
||||
y = abs(f[1:len(f)/2])
|
||||
|
@ -18,6 +18,7 @@ This class is very heavily featured:
|
||||
"""
|
||||
from ...Qt import QtGui, QtCore, QtSvg, USE_PYSIDE
|
||||
from ... import pixmaps
|
||||
import sys
|
||||
|
||||
if USE_PYSIDE:
|
||||
from .plotConfigTemplate_pyside import *
|
||||
@ -69,6 +70,7 @@ class PlotItem(GraphicsWidget):
|
||||
:func:`setYLink <pyqtgraph.ViewBox.setYLink>`,
|
||||
:func:`setAutoPan <pyqtgraph.ViewBox.setAutoPan>`,
|
||||
:func:`setAutoVisible <pyqtgraph.ViewBox.setAutoVisible>`,
|
||||
:func:`setLimits <pyqtgraph.ViewBox.setLimits>`,
|
||||
:func:`viewRect <pyqtgraph.ViewBox.viewRect>`,
|
||||
:func:`viewRange <pyqtgraph.ViewBox.viewRange>`,
|
||||
: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>`
|
||||
|
||||
==================== =======================================================================
|
||||
**Signals**
|
||||
**Signals:**
|
||||
sigYRangeChanged wrapped from :class:`ViewBox <pyqtgraph.ViewBox>`
|
||||
sigXRangeChanged 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().
|
||||
|
||||
============== ==========================================================================================
|
||||
**Arguments**
|
||||
**Arguments:**
|
||||
*title* Title to display at the top of the item. Html is allowed.
|
||||
*labels* A dictionary specifying the axis labels to display::
|
||||
|
||||
@ -192,14 +194,6 @@ class PlotItem(GraphicsWidget):
|
||||
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.curves = []
|
||||
self.itemMeta = weakref.WeakKeyDictionary()
|
||||
@ -298,6 +292,25 @@ class PlotItem(GraphicsWidget):
|
||||
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):
|
||||
"""
|
||||
@ -355,10 +368,8 @@ class PlotItem(GraphicsWidget):
|
||||
self.ctrlMenu.setParent(None)
|
||||
self.ctrlMenu = None
|
||||
|
||||
#self.ctrlBtn.setParent(None)
|
||||
#self.ctrlBtn = None
|
||||
#self.autoBtn.setParent(None)
|
||||
#self.autoBtn = None
|
||||
self.autoBtn.setParent(None)
|
||||
self.autoBtn = None
|
||||
|
||||
for k in self.axes:
|
||||
i = self.axes[k]['item']
|
||||
@ -930,8 +941,8 @@ class PlotItem(GraphicsWidget):
|
||||
def setDownsampling(self, ds=None, auto=None, mode=None):
|
||||
"""Change the default downsampling mode for all PlotDataItems managed by this plot.
|
||||
|
||||
=========== =================================================================
|
||||
Arguments
|
||||
=============== =================================================================
|
||||
**Arguments:**
|
||||
ds (int) Reduce visible plot samples by this factor, or
|
||||
(bool) To enable/disable downsampling without changing the value.
|
||||
auto (bool) If True, automatically pick *ds* based on visible range
|
||||
@ -941,7 +952,7 @@ class PlotItem(GraphicsWidget):
|
||||
'peak': Downsample by drawing a saw wave that follows the min
|
||||
and max of the original data. This method produces the best
|
||||
visual representation of the data but is slower.
|
||||
=========== =================================================================
|
||||
=============== =================================================================
|
||||
"""
|
||||
if ds is not None:
|
||||
if ds is False:
|
||||
@ -1112,15 +1123,15 @@ class PlotItem(GraphicsWidget):
|
||||
"""
|
||||
Set the label for an axis. Basic HTML formatting is allowed.
|
||||
|
||||
============= =================================================================
|
||||
**Arguments**
|
||||
============== =================================================================
|
||||
**Arguments:**
|
||||
axis must be one of 'left', 'bottom', 'right', or 'top'
|
||||
text text to display along the axis. HTML allowed.
|
||||
units units to display after the title. If units are given,
|
||||
then an SI prefix will be automatically appended
|
||||
and the axis values will be scaled accordingly.
|
||||
(ie, use 'V' instead of 'mV'; 'm' will be added automatically)
|
||||
============= =================================================================
|
||||
============== =================================================================
|
||||
"""
|
||||
self.getAxis(axis).setLabel(text=text, units=units, **args)
|
||||
self.showAxis(axis)
|
||||
|
@ -13,11 +13,8 @@ of how to build an ROI at the bottom of the file.
|
||||
"""
|
||||
|
||||
from ..Qt import QtCore, QtGui
|
||||
#if not hasattr(QtCore, 'Signal'):
|
||||
#QtCore.Signal = QtCore.pyqtSignal
|
||||
import numpy as np
|
||||
from numpy.linalg import norm
|
||||
import scipy.ndimage as ndimage
|
||||
#from numpy.linalg import norm
|
||||
from ..Point import *
|
||||
from ..SRTTransform import SRTTransform
|
||||
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())
|
||||
|
||||
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
|
||||
one of its handles) or if the ROI is changed
|
||||
programatically.
|
||||
@ -58,7 +100,7 @@ class ROI(GraphicsObject):
|
||||
details.
|
||||
sigRemoveRequested Emitted when the user selects 'remove' from the
|
||||
ROI's context menu (if available).
|
||||
----------------------- ----------------------------------------------------
|
||||
======================= ====================================================
|
||||
"""
|
||||
|
||||
sigRegionChangeFinished = QtCore.Signal(object)
|
||||
@ -117,7 +159,11 @@ class ROI(GraphicsObject):
|
||||
return sc
|
||||
|
||||
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['pos'] = tuple(self.state['pos'])
|
||||
state['size'] = tuple(self.state['size'])
|
||||
@ -125,6 +171,10 @@ class ROI(GraphicsObject):
|
||||
return state
|
||||
|
||||
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.setSize(state['size'], update=False)
|
||||
self.setAngle(state['angle'], update=update)
|
||||
@ -135,20 +185,32 @@ class ROI(GraphicsObject):
|
||||
h['item'].setZValue(z+1)
|
||||
|
||||
def parentBounds(self):
|
||||
"""
|
||||
Return the bounding rectangle of this ROI in the coordinate system
|
||||
of its parent.
|
||||
"""
|
||||
return self.mapToParent(self.boundingRect()).boundingRect()
|
||||
|
||||
def setPen(self, pen):
|
||||
self.pen = fn.mkPen(pen)
|
||||
def setPen(self, *args, **kwargs):
|
||||
"""
|
||||
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.update()
|
||||
|
||||
def size(self):
|
||||
"""Return the size (w,h) of the ROI."""
|
||||
return self.getState()['size']
|
||||
|
||||
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']
|
||||
|
||||
def angle(self):
|
||||
"""Return the angle of the ROI in degrees."""
|
||||
return self.getState()['angle']
|
||||
|
||||
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
|
||||
is moved to the nearest acceptable position instead.
|
||||
|
||||
snap can be:
|
||||
None (default): use self.translateSnap and self.snapSize to determine whether/how to snap
|
||||
False: do not snap
|
||||
*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)
|
||||
True: snap using self.snapSize (and ignoring self.translateSnap)
|
||||
True snap using self.snapSize (and ignoring self.translateSnap)
|
||||
=============== ==========================================================================
|
||||
|
||||
Also accepts *update* and *finish* arguments (see setPos() for a description of these).
|
||||
"""
|
||||
@ -264,21 +329,86 @@ class ROI(GraphicsObject):
|
||||
#self.stateChanged()
|
||||
|
||||
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)
|
||||
|
||||
def handleMoveStarted(self):
|
||||
self.preMoveState = self.getState()
|
||||
|
||||
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)
|
||||
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):
|
||||
"""
|
||||
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:
|
||||
pos = Point(pos)
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
center = Point(center)
|
||||
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)
|
||||
|
||||
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)
|
||||
center = Point(center)
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
center = Point(center)
|
||||
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)
|
||||
|
||||
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)
|
||||
center = Point(center)
|
||||
return self.addHandle({'name': name, 'type': 'rf', 'center': center, 'pos': pos, 'item': item}, index=index)
|
||||
@ -329,6 +520,9 @@ class ROI(GraphicsObject):
|
||||
return h
|
||||
|
||||
def indexOfHandle(self, handle):
|
||||
"""
|
||||
Return the index of *handle* in the list of this ROI's handles.
|
||||
"""
|
||||
if isinstance(handle, Handle):
|
||||
index = [i for i, info in enumerate(self.handles) if info['item'] is handle]
|
||||
if len(index) == 0:
|
||||
@ -338,7 +532,8 @@ class ROI(GraphicsObject):
|
||||
return 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)
|
||||
|
||||
handle = self.handles[index]['item']
|
||||
@ -349,20 +544,17 @@ class ROI(GraphicsObject):
|
||||
self.stateChanged()
|
||||
|
||||
def replaceHandle(self, oldHandle, newHandle):
|
||||
"""Replace one handle in the ROI for another. This is useful when connecting multiple ROIs together.
|
||||
*oldHandle* may be a Handle instance or the index of a handle."""
|
||||
#print "========================="
|
||||
#print "replace", oldHandle, newHandle
|
||||
#print self
|
||||
#print self.handles
|
||||
#print "-----------------"
|
||||
"""Replace one handle in the ROI for another. This is useful when
|
||||
connecting multiple ROIs together.
|
||||
|
||||
*oldHandle* may be a Handle instance or the index of a handle to be
|
||||
replaced."""
|
||||
index = self.indexOfHandle(oldHandle)
|
||||
info = self.handles[index]
|
||||
self.removeHandle(index)
|
||||
info['item'] = newHandle
|
||||
info['pos'] = newHandle.pos()
|
||||
self.addHandle(info, index=index)
|
||||
#print self.handles
|
||||
|
||||
def checkRemoveHandle(self, handle):
|
||||
## This is used when displaying a Handle's context menu to determine
|
||||
@ -373,7 +565,10 @@ class ROI(GraphicsObject):
|
||||
|
||||
|
||||
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:
|
||||
positions = []
|
||||
for h in self.handles:
|
||||
@ -383,6 +578,10 @@ class ROI(GraphicsObject):
|
||||
return (self.handles[index]['name'], self.handles[index]['pos'])
|
||||
|
||||
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:
|
||||
positions = []
|
||||
for h in self.handles:
|
||||
@ -392,6 +591,9 @@ class ROI(GraphicsObject):
|
||||
return (self.handles[index]['name'], self.handles[index]['item'].scenePos())
|
||||
|
||||
def getHandles(self):
|
||||
"""
|
||||
Return a list of this ROI's Handles.
|
||||
"""
|
||||
return [h['item'] for h in self.handles]
|
||||
|
||||
def mapSceneToParent(self, pt):
|
||||
@ -463,11 +665,7 @@ class ROI(GraphicsObject):
|
||||
|
||||
def removeClicked(self):
|
||||
## Send remove event only after we have exited the menu event handler
|
||||
self.removeTimer = QtCore.QTimer()
|
||||
self.removeTimer.timeout.connect(lambda: self.sigRemoveRequested.emit(self))
|
||||
self.removeTimer.start(0)
|
||||
|
||||
|
||||
QtCore.QTimer.singleShot(0, lambda: self.sigRemoveRequested.emit(self))
|
||||
|
||||
def mouseDragEvent(self, ev):
|
||||
if ev.isStart():
|
||||
@ -511,56 +709,16 @@ class ROI(GraphicsObject):
|
||||
else:
|
||||
ev.ignore()
|
||||
|
||||
|
||||
|
||||
|
||||
def cancelMove(self):
|
||||
self.isMoving = False
|
||||
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):
|
||||
"""When handles move, they must ask the ROI if the move is acceptable.
|
||||
By default, this always returns True. Subclasses may wish override.
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True, coords='parent'):
|
||||
## called by Handles when they are moved.
|
||||
## pos is the new position of the handle in scene coords, as requested by the handle.
|
||||
@ -664,8 +822,11 @@ class ROI(GraphicsObject):
|
||||
if not self.rotateAllowed:
|
||||
return
|
||||
## If the handle is directly over its center point, we can't compute an angle.
|
||||
try:
|
||||
if lp1.length() == 0 or lp0.length() == 0:
|
||||
return
|
||||
except OverflowError:
|
||||
return
|
||||
|
||||
## determine new rotation angle, constrained if necessary
|
||||
ang = newState['angle'] - lp0.angle(lp1)
|
||||
@ -704,8 +865,11 @@ class ROI(GraphicsObject):
|
||||
else:
|
||||
scaleAxis = 0
|
||||
|
||||
try:
|
||||
if lp1.length() == 0 or lp0.length() == 0:
|
||||
return
|
||||
except OverflowError:
|
||||
return
|
||||
|
||||
ang = newState['angle'] - lp0.angle(lp1)
|
||||
if ang is None:
|
||||
@ -804,7 +968,6 @@ class ROI(GraphicsObject):
|
||||
round(pos[1] / snap[1]) * snap[1]
|
||||
)
|
||||
|
||||
|
||||
def boundingRect(self):
|
||||
return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized()
|
||||
|
||||
@ -871,7 +1034,25 @@ class ROI(GraphicsObject):
|
||||
return bounds, tr
|
||||
|
||||
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
|
||||
the slice from *data* and uses :func:`getAffineSliceParams <pyqtgraph.ROI.getAffineSliceParams>` to determine the parameters to
|
||||
@ -906,105 +1087,6 @@ class ROI(GraphicsObject):
|
||||
mapped = fn.transformCoordinates(img.transform(), coords)
|
||||
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)):
|
||||
"""
|
||||
Returns the parameters needed to use :func:`affineSlice <pyqtgraph.affineSlice>` to
|
||||
@ -1088,7 +1170,18 @@ class ROI(GraphicsObject):
|
||||
|
||||
|
||||
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
|
||||
't': (4, np.pi/4),
|
||||
'f': (4, np.pi/4),
|
||||
@ -1360,6 +1453,22 @@ class TestROI(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):
|
||||
#QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1])
|
||||
ROI.__init__(self, pos, size, **args)
|
||||
@ -1375,6 +1484,22 @@ class RectROI(ROI):
|
||||
self.addScaleHandle([0.5, 1], [0.5, center[1]])
|
||||
|
||||
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):
|
||||
pos1 = Point(pos1)
|
||||
pos2 = Point(pos2)
|
||||
@ -1399,6 +1524,13 @@ class MultiRectROI(QtGui.QGraphicsObject):
|
||||
This is generally used to mark a curved path through
|
||||
an image similarly to PolyLineROI. It differs in that each segment
|
||||
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)
|
||||
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)")
|
||||
|
||||
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):
|
||||
#QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1])
|
||||
ROI.__init__(self, pos, size, **args)
|
||||
@ -1540,6 +1684,10 @@ class EllipseROI(ROI):
|
||||
p.drawEllipse(r)
|
||||
|
||||
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)
|
||||
if arr is None or arr.shape[0] == 0 or arr.shape[1] == 0:
|
||||
return None
|
||||
@ -1557,12 +1705,25 @@ class EllipseROI(ROI):
|
||||
|
||||
|
||||
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):
|
||||
ROI.__init__(self, pos, size, **args)
|
||||
self.aspectLocked = True
|
||||
#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])
|
||||
|
||||
|
||||
class PolygonROI(ROI):
|
||||
## deprecated. Use PloyLineROI instead.
|
||||
|
||||
@ -1616,8 +1777,24 @@ class PolygonROI(ROI):
|
||||
return sc
|
||||
|
||||
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):
|
||||
|
||||
if pos is None:
|
||||
@ -1730,6 +1907,10 @@ class PolyLineROI(ROI):
|
||||
return p
|
||||
|
||||
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))
|
||||
if sl is None:
|
||||
return None
|
||||
@ -1758,6 +1939,16 @@ class PolyLineROI(ROI):
|
||||
class LineSegmentROI(ROI):
|
||||
"""
|
||||
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):
|
||||
@ -1810,8 +2001,13 @@ class LineSegmentROI(ROI):
|
||||
|
||||
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.
|
||||
Since this pulls 1D data from a 2D coordinate system, the return value will have ndim = data.ndim-1
|
||||
Use the position of this ROI relative to an imageItem to pull a slice
|
||||
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]
|
||||
|
@ -664,8 +664,14 @@ class ScatterPlotItem(GraphicsObject):
|
||||
if pxPad > 0:
|
||||
# determine length of pixel in local x, y directions
|
||||
px, py = self.pixelVectors()
|
||||
try:
|
||||
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
|
||||
px *= pxPad
|
||||
|
@ -9,8 +9,8 @@ class TextItem(UIGraphicsItem):
|
||||
"""
|
||||
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
|
||||
*color* The color of the text (any format accepted by pg.mkColor)
|
||||
*html* If specified, this overrides both *text* and *color*
|
||||
@ -20,7 +20,7 @@ class TextItem(UIGraphicsItem):
|
||||
sets the lower-right corner.
|
||||
*border* A pen to use when drawing the border
|
||||
*fill* A brush to use when filling within the border
|
||||
=========== =================================================================================
|
||||
============== =================================================================================
|
||||
"""
|
||||
|
||||
## not working yet
|
||||
|
@ -19,15 +19,15 @@ class VTickGroup(UIGraphicsItem):
|
||||
"""
|
||||
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.
|
||||
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
|
||||
fifth of the view.
|
||||
pen The pen to use for drawing ticks. Default is grey. Can be specified
|
||||
as any argument valid for :func:`mkPen<pyqtgraph.mkPen>`
|
||||
============= ===================================================================
|
||||
============== ===================================================================
|
||||
"""
|
||||
if yrange is None:
|
||||
yrange = [0, 1]
|
||||
@ -56,10 +56,10 @@ class VTickGroup(UIGraphicsItem):
|
||||
def setXVals(self, vals):
|
||||
"""Set the x values for the ticks.
|
||||
|
||||
============= =====================================================================
|
||||
**Arguments**
|
||||
============== =====================================================================
|
||||
**Arguments:**
|
||||
vals A list of x values (in data/plot coordinates) at which to draw ticks.
|
||||
============= =====================================================================
|
||||
============== =====================================================================
|
||||
"""
|
||||
self.xvals = vals
|
||||
self.rebuildTicks()
|
||||
|
@ -5,28 +5,63 @@ from ...Point import Point
|
||||
from ... import functions as fn
|
||||
from .. ItemGroup import ItemGroup
|
||||
from .. GraphicsWidget import GraphicsWidget
|
||||
from ...GraphicsScene import GraphicsScene
|
||||
import weakref
|
||||
from copy import deepcopy
|
||||
from ... import debug as debug
|
||||
from ... import getConfigOption
|
||||
import sys
|
||||
from pyqtgraph.Qt import isQObjectAlive
|
||||
|
||||
__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):
|
||||
|
||||
sigItemsChanged = QtCore.Signal()
|
||||
def __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
|
||||
self._GraphicsObject__inform_view_on_change = False
|
||||
|
||||
def itemChange(self, change, value):
|
||||
ret = ItemGroup.itemChange(self, change, value)
|
||||
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
|
||||
|
||||
|
||||
@ -71,8 +106,8 @@ class ViewBox(GraphicsWidget):
|
||||
|
||||
def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None):
|
||||
"""
|
||||
============= =============================================================
|
||||
**Arguments**
|
||||
============== =============================================================
|
||||
**Arguments:**
|
||||
*parent* (QGraphicsWidget) Optional parent widget
|
||||
*border* (QPen) Do draw a border around the view, give any
|
||||
single argument accepted by :func:`mkPen <pyqtgraph.mkPen>`
|
||||
@ -80,7 +115,7 @@ class ViewBox(GraphicsWidget):
|
||||
coorinates to. (or False to allow the ratio to change)
|
||||
*enableMouse* (bool) Whether mouse can be used to scale/pan the view
|
||||
*invertY* (bool) See :func:`invertY <pyqtgraph.ViewBox.invertY>`
|
||||
============= =============================================================
|
||||
============== =============================================================
|
||||
"""
|
||||
|
||||
|
||||
@ -118,6 +153,15 @@ class ViewBox(GraphicsWidget):
|
||||
'wheelScaleFactor': -1.0 / 8.0,
|
||||
|
||||
'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._itemBoundsCache = weakref.WeakKeyDictionary()
|
||||
@ -131,7 +175,7 @@ class ViewBox(GraphicsWidget):
|
||||
## this is a workaround for a Qt + OpenGL bug that causes improper clipping
|
||||
## https://bugreports.qt.nokia.com/browse/QTBUG-23723
|
||||
self.childGroup = ChildGroup(self)
|
||||
self.childGroup.sigItemsChanged.connect(self.itemsChanged)
|
||||
self.childGroup.itemsChangedListeners.append(self)
|
||||
|
||||
self.background = QtGui.QGraphicsRectItem(self.rect())
|
||||
self.background.setParentItem(self)
|
||||
@ -197,6 +241,7 @@ class ViewBox(GraphicsWidget):
|
||||
del ViewBox.NamedViews[self.name]
|
||||
|
||||
def close(self):
|
||||
self.clear()
|
||||
self.unregister()
|
||||
|
||||
def implements(self, interface):
|
||||
@ -276,6 +321,17 @@ class ViewBox(GraphicsWidget):
|
||||
self.updateViewRange()
|
||||
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):
|
||||
"""
|
||||
@ -398,13 +454,20 @@ class ViewBox(GraphicsWidget):
|
||||
print("make qrectf failed:", self.state['targetRange'])
|
||||
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):
|
||||
"""
|
||||
Set the visible range of the ViewBox.
|
||||
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.
|
||||
*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.
|
||||
@ -546,14 +609,14 @@ class ViewBox(GraphicsWidget):
|
||||
Note that this is not the same as enableAutoRange, which causes the view to
|
||||
automatically auto-range whenever its contents are changed.
|
||||
|
||||
=========== ============================================================
|
||||
Arguments
|
||||
============== ============================================================
|
||||
**Arguments:**
|
||||
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
|
||||
and 0.1 depending on the size of the ViewBox.
|
||||
items If specified, this is a list of items to consider when
|
||||
determining the visible range.
|
||||
=========== ============================================================
|
||||
============== ============================================================
|
||||
"""
|
||||
if item is None:
|
||||
bounds = self.childrenBoundingRect(items=items)
|
||||
@ -572,6 +635,57 @@ class ViewBox(GraphicsWidget):
|
||||
padding = 0.02
|
||||
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):
|
||||
"""
|
||||
Scale by *s* around given center point (or center of view).
|
||||
@ -818,7 +932,7 @@ class ViewBox(GraphicsWidget):
|
||||
try:
|
||||
getattr(oldLink, signal).disconnect(slot)
|
||||
oldLink.sigResized.disconnect(slot)
|
||||
except TypeError:
|
||||
except (TypeError, RuntimeError):
|
||||
## This can occur if the view has been deleted already
|
||||
pass
|
||||
|
||||
@ -1056,6 +1170,7 @@ class ViewBox(GraphicsWidget):
|
||||
center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos()))
|
||||
#center = ev.pos()
|
||||
|
||||
self._resetTarget()
|
||||
self.scaleBy(s, center)
|
||||
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
|
||||
ev.accept()
|
||||
@ -1113,6 +1228,8 @@ class ViewBox(GraphicsWidget):
|
||||
x = tr.x() if mask[0] == 1 else None
|
||||
y = tr.y() if mask[1] == 1 else None
|
||||
|
||||
self._resetTarget()
|
||||
if x is not None or y is not None:
|
||||
self.translateBy(x=x, y=y)
|
||||
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
|
||||
elif ev.button() & QtCore.Qt.RightButton:
|
||||
@ -1132,6 +1249,7 @@ class ViewBox(GraphicsWidget):
|
||||
y = s[1] if mouseEnabled[1] == 1 else None
|
||||
|
||||
center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton)))
|
||||
self._resetTarget()
|
||||
self.scaleBy(x=x, y=y, center=center)
|
||||
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
|
||||
|
||||
@ -1327,9 +1445,9 @@ class ViewBox(GraphicsWidget):
|
||||
viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]]
|
||||
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
|
||||
tr = self.targetRect()
|
||||
bounds = self.rect()
|
||||
@ -1351,7 +1469,6 @@ class ViewBox(GraphicsWidget):
|
||||
# then make the entire target range visible
|
||||
ax = 0 if targetRatio > viewRatio else 1
|
||||
|
||||
#### these should affect viewRange, not targetRange!
|
||||
if ax == 0:
|
||||
## view range needs to be taller than target
|
||||
dy = 0.5 * (tr.width() / viewRatio - tr.height())
|
||||
@ -1365,7 +1482,58 @@ class ViewBox(GraphicsWidget):
|
||||
changed[0] = True
|
||||
viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx]
|
||||
|
||||
changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) and (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)]
|
||||
# ----------- 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
|
||||
|
||||
# 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
|
||||
|
||||
# emit range change signals
|
||||
@ -1493,6 +1661,9 @@ class ViewBox(GraphicsWidget):
|
||||
## called when the application is about to exit.
|
||||
## this disables all callbacks, which might otherwise generate errors if invoked during exit.
|
||||
for k in ViewBox.AllViews:
|
||||
if isQObjectAlive(k) and getConfigOption('crashWarning'):
|
||||
sys.stderr.write('Warning: ViewBox should be closed before application exit.\n')
|
||||
|
||||
try:
|
||||
k.destroyed.disconnect()
|
||||
except RuntimeError: ## signal is already disconnected.
|
||||
|
47
graphicsItems/tests/test_GraphicsItem.py
Normal file
47
graphicsItems/tests/test_GraphicsItem.py
Normal 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()
|
||||
|
||||
|
@ -19,11 +19,14 @@ def mkQApp():
|
||||
|
||||
|
||||
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):
|
||||
mkQApp()
|
||||
#self.win = QtGui.QMainWindow()
|
||||
GraphicsLayoutWidget.__init__(self, **kargs)
|
||||
#self.win.setCentralWidget(self)
|
||||
self.resize(*size)
|
||||
if title is not None:
|
||||
self.setWindowTitle(title)
|
||||
|
@ -33,6 +33,11 @@ from .. import debug as debug
|
||||
|
||||
from ..SignalProxy import SignalProxy
|
||||
|
||||
try:
|
||||
from bottleneck import nanmin, nanmax
|
||||
except ImportError:
|
||||
from numpy import nanmin, nanmax
|
||||
|
||||
#try:
|
||||
#from .. import metaarray as metaarray
|
||||
#HAVE_METAARRAY = True
|
||||
@ -196,7 +201,12 @@ class ImageView(QtGui.QWidget):
|
||||
img = img.asarray()
|
||||
|
||||
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.imageDisp = None
|
||||
|
||||
@ -319,11 +329,10 @@ class ImageView(QtGui.QWidget):
|
||||
if self.imageDisp is None:
|
||||
image = self.normalize(self.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
|
||||
|
||||
|
||||
def close(self):
|
||||
"""Closes the widget nicely, making sure to clear the graphics scene and release memory."""
|
||||
self.ui.roiPlot.close()
|
||||
@ -375,7 +384,6 @@ class ImageView(QtGui.QWidget):
|
||||
else:
|
||||
QtGui.QWidget.keyReleaseEvent(self, ev)
|
||||
|
||||
|
||||
def evalKeyState(self):
|
||||
if len(self.keysPressed) == 1:
|
||||
key = list(self.keysPressed.keys())[0]
|
||||
@ -399,16 +407,13 @@ class ImageView(QtGui.QWidget):
|
||||
else:
|
||||
self.play(0)
|
||||
|
||||
|
||||
def timeout(self):
|
||||
now = ptime.time()
|
||||
dt = now - self.lastPlayTime
|
||||
if dt < 0:
|
||||
return
|
||||
n = int(self.playRate * dt)
|
||||
#print n, dt
|
||||
if n != 0:
|
||||
#print n, dt, self.lastPlayTime
|
||||
self.lastPlayTime += (float(n)/self.playRate)
|
||||
if self.currentIndex+n > self.image.shape[0]:
|
||||
self.play(0)
|
||||
@ -434,16 +439,13 @@ class ImageView(QtGui.QWidget):
|
||||
self.roiChanged()
|
||||
self.sigProcessingChanged.emit(self)
|
||||
|
||||
|
||||
def updateNorm(self):
|
||||
if self.ui.normTimeRangeCheck.isChecked():
|
||||
#print "show!"
|
||||
self.normRgn.show()
|
||||
else:
|
||||
self.normRgn.hide()
|
||||
|
||||
if self.ui.normROICheck.isChecked():
|
||||
#print "show!"
|
||||
self.normRoi.show()
|
||||
else:
|
||||
self.normRoi.hide()
|
||||
@ -520,20 +522,24 @@ class ImageView(QtGui.QWidget):
|
||||
xvals = (coords**2).sum(axis=0) ** 0.5
|
||||
self.roiCurve.setData(y=data, x=xvals)
|
||||
|
||||
#self.ui.roiPlot.replot()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def quickMinMax(data):
|
||||
def quickMinMax(self, data):
|
||||
"""
|
||||
Estimate the min/max values of *data* by subsampling.
|
||||
"""
|
||||
while data.size > 1e6:
|
||||
ax = np.argmax(data.shape)
|
||||
sl = [slice(None)] * data.ndim
|
||||
sl[ax] = slice(None, None, 2)
|
||||
data = data[sl]
|
||||
return data.min(), data.max()
|
||||
return nanmin(data), nanmax(data)
|
||||
|
||||
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():
|
||||
return image
|
||||
|
||||
|
11
imageview/tests/test_imageview.py
Normal file
11
imageview/tests/test_imageview.py
Normal 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()
|
@ -40,7 +40,7 @@ class Parallelize(object):
|
||||
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
|
||||
distribute the tasks). If unspecified, then each worker will receive
|
||||
a single task with a unique id number.
|
||||
|
@ -1,13 +1,15 @@
|
||||
from .remoteproxy import RemoteEventHandler, ClosedError, NoResultError, LocalObjectProxy, ObjectProxy
|
||||
import subprocess, atexit, os, sys, time, random, socket, signal
|
||||
import multiprocessing.connection
|
||||
from ..Qt import USE_PYSIDE
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError:
|
||||
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']
|
||||
|
||||
class Process(RemoteEventHandler):
|
||||
@ -35,11 +37,12 @@ class Process(RemoteEventHandler):
|
||||
return objects either by proxy or by value (if they are picklable). See
|
||||
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):
|
||||
"""
|
||||
============ =============================================================
|
||||
Arguments:
|
||||
============== =============================================================
|
||||
**Arguments:**
|
||||
name Optional name for this process used when printing messages
|
||||
from the remote process.
|
||||
target Optional function to call after starting remote process.
|
||||
@ -56,7 +59,7 @@ class Process(RemoteEventHandler):
|
||||
for a python bug: http://bugs.python.org/issue3905
|
||||
but has the side effect that child output is significantly
|
||||
delayed relative to the parent output.
|
||||
============ =============================================================
|
||||
============== =============================================================
|
||||
"""
|
||||
if target is None:
|
||||
target = startEventLoop
|
||||
@ -64,7 +67,7 @@ class Process(RemoteEventHandler):
|
||||
name = str(self)
|
||||
if executable is None:
|
||||
executable = sys.executable
|
||||
self.debug = debug
|
||||
self.debug = 7 if debug is True else False # 7 causes printing in white
|
||||
|
||||
## random authentication key
|
||||
authkey = os.urandom(20)
|
||||
@ -75,22 +78,21 @@ class Process(RemoteEventHandler):
|
||||
|
||||
#print "key:", ' '.join([str(ord(x)) for x in authkey])
|
||||
## Listen for connection from remote process (and find free port number)
|
||||
port = 10000
|
||||
while True:
|
||||
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
|
||||
|
||||
l = multiprocessing.connection.Listener(('localhost', 0), authkey=authkey)
|
||||
port = l.address[1]
|
||||
|
||||
## start remote process, instruct it to run target function
|
||||
sysPath = sys.path if copySysPath else None
|
||||
bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py'))
|
||||
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:
|
||||
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)
|
||||
## to circumvent the bug and still make the output visible, we use
|
||||
## background threads to pass data from pipes to stdout/stderr
|
||||
self._stdoutForwarder = FileForwarder(self.proc.stdout, "stdout")
|
||||
self._stderrForwarder = FileForwarder(self.proc.stderr, "stderr")
|
||||
self._stdoutForwarder = FileForwarder(self.proc.stdout, "stdout", procDebug)
|
||||
self._stderrForwarder = FileForwarder(self.proc.stderr, "stderr", procDebug)
|
||||
else:
|
||||
self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE)
|
||||
|
||||
@ -120,7 +122,7 @@ class Process(RemoteEventHandler):
|
||||
targetStr=targetStr,
|
||||
path=sysPath,
|
||||
pyside=USE_PYSIDE,
|
||||
debug=debug
|
||||
debug=procDebug
|
||||
)
|
||||
pickle.dump(data, self.proc.stdin)
|
||||
self.proc.stdin.close()
|
||||
@ -137,7 +139,7 @@ class Process(RemoteEventHandler):
|
||||
else:
|
||||
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.')
|
||||
|
||||
atexit.register(self.join)
|
||||
@ -167,10 +169,11 @@ class Process(RemoteEventHandler):
|
||||
def startEventLoop(name, port, authkey, ppid, debug=False):
|
||||
if debug:
|
||||
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)
|
||||
if debug:
|
||||
print('[%d] connected; starting remote proxy.' % os.getpid())
|
||||
cprint.cout(debug, '[%d] connected; starting remote proxy.\n' % os.getpid(), -1)
|
||||
global HANDLER
|
||||
#ppid = 0 if not hasattr(os, 'getppid') else os.getppid()
|
||||
HANDLER = RemoteEventHandler(conn, name, ppid, debug=debug)
|
||||
@ -380,17 +383,17 @@ class QtProcess(Process):
|
||||
def __init__(self, **kwds):
|
||||
if 'target' not in kwds:
|
||||
kwds['target'] = startQtEventLoop
|
||||
from ..Qt import QtGui ## avoid module-level import to keep bootstrap snappy.
|
||||
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)
|
||||
self.startEventTimer()
|
||||
|
||||
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()
|
||||
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()
|
||||
|
||||
def startRequestProcessing(self, interval=0.01):
|
||||
@ -412,10 +415,10 @@ class QtProcess(Process):
|
||||
def startQtEventLoop(name, port, authkey, ppid, debug=False):
|
||||
if debug:
|
||||
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)
|
||||
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 PyQt4 import QtGui, QtCore
|
||||
app = QtGui.QApplication.instance()
|
||||
@ -445,11 +448,13 @@ class FileForwarder(threading.Thread):
|
||||
which ensures that the correct behavior is achieved even if
|
||||
sys.stdout/stderr are replaced at runtime.
|
||||
"""
|
||||
def __init__(self, input, output):
|
||||
def __init__(self, input, output, color):
|
||||
threading.Thread.__init__(self)
|
||||
self.input = input
|
||||
self.output = output
|
||||
self.lock = threading.Lock()
|
||||
self.daemon = True
|
||||
self.color = color
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
@ -457,12 +462,12 @@ class FileForwarder(threading.Thread):
|
||||
while True:
|
||||
line = self.input.readline()
|
||||
with self.lock:
|
||||
sys.stdout.write(line)
|
||||
cprint.cout(self.color, line, -1)
|
||||
elif self.output == 'stderr':
|
||||
while True:
|
||||
line = self.input.readline()
|
||||
with self.lock:
|
||||
sys.stderr.write(line)
|
||||
cprint.cerr(self.color, line, -1)
|
||||
else:
|
||||
while True:
|
||||
line = self.input.readline()
|
||||
|
@ -7,6 +7,9 @@ except ImportError:
|
||||
import builtins
|
||||
import pickle
|
||||
|
||||
# color printing for debugging
|
||||
from ..util import cprint
|
||||
|
||||
class ClosedError(Exception):
|
||||
"""Raised when an event handler receives a request to close the connection
|
||||
or discovers that the connection has been closed."""
|
||||
@ -80,7 +83,7 @@ class RemoteEventHandler(object):
|
||||
def debugMsg(self, msg):
|
||||
if not self.debug:
|
||||
return
|
||||
print("[%d] %s" % (os.getpid(), str(msg)))
|
||||
cprint.cout(self.debug, "[%d] %s\n" % (os.getpid(), str(msg)), -1)
|
||||
|
||||
def getProxyOption(self, opt):
|
||||
return self.proxyOptions[opt]
|
||||
@ -299,8 +302,8 @@ class RemoteEventHandler(object):
|
||||
(The docstring has information that is nevertheless useful to the programmer
|
||||
as it describes the internal protocol used to communicate between processes)
|
||||
|
||||
========== ====================================================================
|
||||
Arguments:
|
||||
============== ====================================================================
|
||||
**Arguments:**
|
||||
request String describing the type of request being sent (see below)
|
||||
reqId Integer uniquely linking a result back to the request that generated
|
||||
it. (most requests leave this blank)
|
||||
@ -315,7 +318,7 @@ class RemoteEventHandler(object):
|
||||
byteData If specified, this is a list of objects to be sent as byte messages
|
||||
to the remote process.
|
||||
This is used to send large arrays without the cost of pickling.
|
||||
========== ====================================================================
|
||||
============== ====================================================================
|
||||
|
||||
Description of request strings and options allowed for each:
|
||||
|
||||
|
@ -36,6 +36,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||
## (rotation around z-axis 0 points along x-axis)
|
||||
'viewport': None, ## glViewport params; None == whole widget
|
||||
}
|
||||
self.setBackgroundColor('k')
|
||||
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.keysPressed = {}
|
||||
@ -64,9 +65,16 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||
|
||||
|
||||
def initializeGL(self):
|
||||
glClearColor(0.0, 0.0, 0.0, 0.0)
|
||||
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):
|
||||
vp = self.opts['viewport']
|
||||
if vp is None:
|
||||
@ -129,6 +137,12 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||
return tr
|
||||
|
||||
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 = glSelectBuffer(100000)
|
||||
try:
|
||||
@ -158,6 +172,8 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||
glViewport(*viewport)
|
||||
self.setProjection(region=region)
|
||||
self.setModelview()
|
||||
bgcolor = self.opts['bgcolor']
|
||||
glClearColor(bgcolor.red(), bgcolor.green(), bgcolor.blue(), 1.0)
|
||||
glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT )
|
||||
self.drawItemTree(useItemNames=useItemNames)
|
||||
|
||||
@ -180,7 +196,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||
i.paint()
|
||||
except:
|
||||
from .. import debug
|
||||
pyqtgraph.debug.printExc()
|
||||
debug.printExc()
|
||||
msg = "Error while drawing item %s." % str(item)
|
||||
ver = glGetString(GL_VERSION)
|
||||
if ver is not None:
|
||||
@ -294,6 +310,17 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||
|
||||
def mouseReleaseEvent(self, ev):
|
||||
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):
|
||||
if (ev.modifiers() & QtCore.Qt.ControlModifier):
|
||||
|
@ -1,5 +1,5 @@
|
||||
from ..Qt import QtGui
|
||||
from .. import functions as fn
|
||||
from pyqtgraph.Qt import QtGui
|
||||
import pyqtgraph.functions as fn
|
||||
import numpy as np
|
||||
|
||||
class MeshData(object):
|
||||
@ -23,8 +23,8 @@ class MeshData(object):
|
||||
|
||||
def __init__(self, vertexes=None, faces=None, edges=None, vertexColors=None, faceColors=None):
|
||||
"""
|
||||
============= =====================================================
|
||||
Arguments
|
||||
============== =====================================================
|
||||
**Arguments:**
|
||||
vertexes (Nv, 3) array of vertex coordinates.
|
||||
If faces is not specified, then this will instead be
|
||||
interpreted as (Nf, 3, 3) array of coordinates.
|
||||
@ -34,7 +34,7 @@ class MeshData(object):
|
||||
If faces is not specified, then this will instead be
|
||||
interpreted as (Nf, 3, 4) array of colors.
|
||||
faceColors (Nf, 4) array of face colors.
|
||||
============= =====================================================
|
||||
============== =====================================================
|
||||
|
||||
All arguments are optional.
|
||||
"""
|
||||
@ -84,64 +84,11 @@ class MeshData(object):
|
||||
if faceColors is not None:
|
||||
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):
|
||||
"""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
|
||||
|
||||
def edges(self):
|
||||
@ -162,8 +109,6 @@ class MeshData(object):
|
||||
self._vertexColorsIndexedByFaces = None
|
||||
self._faceColorsIndexedByFaces = None
|
||||
|
||||
|
||||
|
||||
def vertexes(self, indexed=None):
|
||||
"""Return an array (N,3) of the positions of vertexes in the mesh.
|
||||
By default, each unique vertex appears only once in the array.
|
||||
@ -208,7 +153,6 @@ class MeshData(object):
|
||||
self._faceNormals = None
|
||||
self._faceNormalsIndexedByFaces = None
|
||||
|
||||
|
||||
def hasFaceIndexedData(self):
|
||||
"""Return True if this object already has vertex positions indexed by face"""
|
||||
return self._vertexesIndexedByFaces is not None
|
||||
@ -230,7 +174,6 @@ class MeshData(object):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def faceNormals(self, indexed=None):
|
||||
"""
|
||||
Return an array (Nf, 3) of normal vectors for each face.
|
||||
@ -242,7 +185,6 @@ class MeshData(object):
|
||||
v = self.vertexes(indexed='faces')
|
||||
self._faceNormals = np.cross(v[:,1]-v[:,0], v[:,2]-v[:,0])
|
||||
|
||||
|
||||
if indexed is None:
|
||||
return self._faceNormals
|
||||
elif indexed == 'faces':
|
||||
@ -266,7 +208,11 @@ class MeshData(object):
|
||||
vertFaces = self.vertexFaces()
|
||||
self._vertexNormals = np.empty(self._vertexes.shape, dtype=float)
|
||||
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 /= (norm**2).sum()**0.5 ## and re-normalize
|
||||
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)
|
||||
|
||||
## I think generally this should be discouraged..
|
||||
|
||||
faces = self._vertexesIndexedByFaces
|
||||
verts = {} ## used to remember the index of each vertex position
|
||||
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.
|
||||
"""
|
||||
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]):
|
||||
face = self._faces[i]
|
||||
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)
|
||||
return self._vertexFaces
|
||||
|
||||
@ -426,8 +369,8 @@ class MeshData(object):
|
||||
#pass
|
||||
|
||||
def _computeEdges(self):
|
||||
if not self.hasFaceIndexedData:
|
||||
## generate self._edges from self._faces
|
||||
#print self._faces
|
||||
nf = len(self._faces)
|
||||
edges = np.empty(nf*3, dtype=[('i', np.uint, 2)])
|
||||
edges['i'][0:nf] = self._faces[:,:2]
|
||||
@ -442,6 +385,19 @@ class MeshData(object):
|
||||
# remove duplicate entries
|
||||
self._edges = np.unique(edges)['i']
|
||||
#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):
|
||||
@ -516,4 +472,33 @@ class MeshData(object):
|
||||
|
||||
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)
|
||||
|
@ -45,7 +45,7 @@ class GLAxisItem(GLGraphicsItem):
|
||||
|
||||
if self.antialias:
|
||||
glEnable(GL_LINE_SMOOTH)
|
||||
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
|
||||
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST)
|
||||
|
||||
glBegin( GL_LINES )
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import numpy as np
|
||||
|
||||
from OpenGL.GL import *
|
||||
from .. GLGraphicsItem import GLGraphicsItem
|
||||
from ... import QtGui
|
||||
@ -16,8 +18,9 @@ class GLGridItem(GLGraphicsItem):
|
||||
self.setGLOptions(glOptions)
|
||||
self.antialias = antialias
|
||||
if size is None:
|
||||
size = QtGui.QVector3D(1,1,1)
|
||||
size = QtGui.QVector3D(20,20,1)
|
||||
self.setSize(size=size)
|
||||
self.setSpacing(1, 1, 1)
|
||||
|
||||
def setSize(self, x=None, y=None, z=None, size=None):
|
||||
"""
|
||||
@ -34,6 +37,20 @@ class GLGridItem(GLGraphicsItem):
|
||||
def size(self):
|
||||
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):
|
||||
self.setupGLState()
|
||||
@ -42,17 +59,20 @@ class GLGridItem(GLGraphicsItem):
|
||||
glEnable(GL_LINE_SMOOTH)
|
||||
glEnable(GL_BLEND)
|
||||
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 )
|
||||
|
||||
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)
|
||||
for x in range(-10, 11):
|
||||
glVertex3f(x, -10, 0)
|
||||
glVertex3f(x, 10, 0)
|
||||
for y in range(-10, 11):
|
||||
glVertex3f(-10, y, 0)
|
||||
glVertex3f( 10, y, 0)
|
||||
for x in xvals:
|
||||
glVertex3f(x, yvals[0], 0)
|
||||
glVertex3f(x, yvals[-1], 0)
|
||||
for y in yvals:
|
||||
glVertex3f(xvals[0], y, 0)
|
||||
glVertex3f(xvals[-1], y, 0)
|
||||
|
||||
glEnd()
|
||||
|
@ -16,6 +16,7 @@ class GLLinePlotItem(GLGraphicsItem):
|
||||
glopts = kwds.pop('glOptions', 'additive')
|
||||
self.setGLOptions(glopts)
|
||||
self.pos = None
|
||||
self.mode = 'line_strip'
|
||||
self.width = 1.
|
||||
self.color = (1.0,1.0,1.0,1.0)
|
||||
self.setData(**kwds)
|
||||
@ -27,7 +28,7 @@ class GLLinePlotItem(GLGraphicsItem):
|
||||
colors unchanged, etc.
|
||||
|
||||
==================== ==================================================
|
||||
Arguments:
|
||||
**Arguments:**
|
||||
------------------------------------------------------------------------
|
||||
pos (N,3) array of floats specifying point locations.
|
||||
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.
|
||||
width float specifying line width
|
||||
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():
|
||||
if k not in 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_BLEND)
|
||||
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)
|
||||
|
||||
finally:
|
||||
glDisableClientState(GL_COLOR_ARRAY)
|
||||
glDisableClientState(GL_VERTEX_ARRAY)
|
||||
|
@ -19,7 +19,7 @@ class GLMeshItem(GLGraphicsItem):
|
||||
def __init__(self, **kwds):
|
||||
"""
|
||||
============== =====================================================
|
||||
Arguments
|
||||
**Arguments:**
|
||||
meshdata MeshData object from which to determine geometry for
|
||||
this item.
|
||||
color Default face color used if no vertex or face colors
|
||||
@ -153,8 +153,12 @@ class GLMeshItem(GLGraphicsItem):
|
||||
self.colors = md.faceColors(indexed='faces')
|
||||
|
||||
if self.opts['drawEdges']:
|
||||
if not md.hasFaceIndexedData():
|
||||
self.edges = md.edges()
|
||||
self.edgeVerts = md.vertexes()
|
||||
else:
|
||||
self.edges = md.edges()
|
||||
self.edgeVerts = md.vertexes(indexed='faces')
|
||||
return
|
||||
|
||||
def paint(self):
|
||||
|
@ -28,8 +28,7 @@ class GLScatterPlotItem(GLGraphicsItem):
|
||||
colors unchanged, etc.
|
||||
|
||||
==================== ==================================================
|
||||
Arguments:
|
||||
------------------------------------------------------------------------
|
||||
**Arguments:**
|
||||
pos (N,3) array of floats specifying point locations.
|
||||
color (N,4) array of floats (0.0-1.0) specifying
|
||||
spot colors OR a tuple of floats specifying
|
||||
|
@ -36,14 +36,14 @@ class GLSurfacePlotItem(GLMeshItem):
|
||||
"""
|
||||
Update the data in this surface plot.
|
||||
|
||||
========== =====================================================================
|
||||
Arguments
|
||||
============== =====================================================================
|
||||
**Arguments:**
|
||||
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
|
||||
integers.
|
||||
z 2D array of height values for each grid vertex.
|
||||
colors (width, height, 4) array of vertex colors.
|
||||
========== =====================================================================
|
||||
============== =====================================================================
|
||||
|
||||
All arguments are optional.
|
||||
|
||||
|
@ -107,8 +107,8 @@ class Parameter(QtCore.QObject):
|
||||
Parameter instance, the options available to this method are also allowed
|
||||
by most Parameter subclasses.
|
||||
|
||||
================= =========================================================
|
||||
Keyword Arguments
|
||||
======================= =========================================================
|
||||
**Keyword Arguments:**
|
||||
name The name to give this Parameter. This is the name that
|
||||
will appear in the left-most column of a ParameterTree
|
||||
for this Parameter.
|
||||
@ -133,7 +133,7 @@ class Parameter(QtCore.QObject):
|
||||
expanded If True, the Parameter will appear expanded when
|
||||
displayed in a ParameterTree (its children will be
|
||||
visible). (default=True)
|
||||
================= =========================================================
|
||||
======================= =========================================================
|
||||
"""
|
||||
|
||||
|
||||
@ -516,7 +516,7 @@ class Parameter(QtCore.QObject):
|
||||
self.sigChildRemoved.emit(self, child)
|
||||
try:
|
||||
child.sigTreeStateChanged.disconnect(self.treeStateChanged)
|
||||
except TypeError: ## already disconnected
|
||||
except (TypeError, RuntimeError): ## already disconnected
|
||||
pass
|
||||
|
||||
def clearChildren(self):
|
||||
@ -675,13 +675,13 @@ class Parameter(QtCore.QObject):
|
||||
"""
|
||||
Called when the state of any sub-parameter has changed.
|
||||
|
||||
========== ================================================================
|
||||
Arguments:
|
||||
============== ================================================================
|
||||
**Arguments:**
|
||||
param The immediate child whose tree state has changed.
|
||||
note that the change may have originated from a grandchild.
|
||||
changes List of tuples describing all changes that have been made
|
||||
in this event: (param, changeDescr, data)
|
||||
========== ================================================================
|
||||
============== ================================================================
|
||||
|
||||
This function can be extended to react to tree state changes.
|
||||
"""
|
||||
|
@ -18,8 +18,8 @@ class WidgetParameterItem(ParameterItem):
|
||||
* simple widget for editing value (displayed instead of label when item is selected)
|
||||
* button that resets value to default
|
||||
|
||||
================= =============================================================
|
||||
Registered Types:
|
||||
========================== =============================================================
|
||||
**Registered Types:**
|
||||
int Displays a :class:`SpinBox <pyqtgraph.SpinBox>` in integer
|
||||
mode.
|
||||
float Displays a :class:`SpinBox <pyqtgraph.SpinBox>`.
|
||||
@ -27,7 +27,7 @@ class WidgetParameterItem(ParameterItem):
|
||||
str Displays a QLineEdit
|
||||
color Displays a :class:`ColorButton <pyqtgraph.ColorButton>`
|
||||
colormap Displays a :class:`GradientWidget <pyqtgraph.GradientWidget>`
|
||||
================= =============================================================
|
||||
========================== =============================================================
|
||||
|
||||
This class can be subclassed by overriding makeWidget() to provide a custom widget.
|
||||
"""
|
||||
@ -208,12 +208,14 @@ class WidgetParameterItem(ParameterItem):
|
||||
val = self.widget.value()
|
||||
newVal = self.param.setValue(val)
|
||||
|
||||
def widgetValueChanging(self):
|
||||
def widgetValueChanging(self, *args):
|
||||
"""
|
||||
Called when the widget's value is changing, but not finalized.
|
||||
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):
|
||||
"""Called when this item has been selected (sel=True) OR deselected (sel=False)"""
|
||||
|
@ -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
|
||||
|
||||
|
68
tests/test_functions.py
Normal file
68
tests/test_functions.py
Normal 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
10
tests/test_qt.py
Normal 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)
|
39
tests/test_srttransform3d.py
Normal file
39
tests/test_srttransform3d.py
Normal 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
0
util/__init__.py
Normal file
28
util/colorama/LICENSE.txt
Normal file
28
util/colorama/LICENSE.txt
Normal 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
304
util/colorama/README.txt
Normal 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.
|
||||
|
0
util/colorama/__init__.py
Normal file
0
util/colorama/__init__.py
Normal file
137
util/colorama/win32.py
Normal file
137
util/colorama/win32.py
Normal 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
120
util/colorama/winterm.py
Normal 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
101
util/cprint.py
Normal 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
121
util/lru_cache.py
Normal 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
94
util/mutex.py
Normal 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
|
50
util/tests/test_lru_cache.py
Normal file
50
util/tests/test_lru_cache.py
Normal 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()
|
@ -11,7 +11,7 @@ class ColorButton(QtGui.QPushButton):
|
||||
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
|
||||
sigColorChanged(self) emitted when the selected color is accepted (user clicks OK)
|
||||
====================== ============================================================
|
||||
|
@ -86,14 +86,14 @@ class ColorMapParameter(ptree.types.GroupParameter):
|
||||
"""
|
||||
Return an array of colors corresponding to *data*.
|
||||
|
||||
========= =================================================================
|
||||
Arguments
|
||||
============== =================================================================
|
||||
**Arguments:**
|
||||
data A numpy record array where the fields in data.dtype match those
|
||||
defined by a prior call to setFields().
|
||||
mode Either 'byte' or 'float'. For 'byte', the method returns an array
|
||||
of dtype ubyte with values scaled 0-255. For 'float', colors are
|
||||
returned as 0.0-1.0 float values.
|
||||
========= =================================================================
|
||||
============== =================================================================
|
||||
"""
|
||||
colors = np.zeros((len(data),4))
|
||||
for item in self.children():
|
||||
|
@ -4,9 +4,27 @@ from .GraphicsView import GraphicsView
|
||||
|
||||
__all__ = ['GraphicsLayoutWidget']
|
||||
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):
|
||||
GraphicsView.__init__(self, parent)
|
||||
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))
|
||||
self.setCentralItem(self.ci)
|
||||
|
@ -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
|
||||
enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality)."""
|
||||
|
||||
sigRangeChanged = QtCore.Signal(object, object)
|
||||
sigTransformChanged = QtCore.Signal(object)
|
||||
sigDeviceRangeChanged = QtCore.Signal(object, object)
|
||||
sigDeviceTransformChanged = QtCore.Signal(object)
|
||||
sigMouseReleased = QtCore.Signal(object)
|
||||
sigSceneMouseMoved = QtCore.Signal(object)
|
||||
#sigRegionChanged = QtCore.Signal(object)
|
||||
@ -50,8 +50,8 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
|
||||
def __init__(self, parent=None, useOpenGL=None, background='default'):
|
||||
"""
|
||||
============ ============================================================
|
||||
Arguments:
|
||||
============== ============================================================
|
||||
**Arguments:**
|
||||
parent Optional parent widget
|
||||
useOpenGL If True, the GraphicsView will use OpenGL to do all of its
|
||||
rendering. This can improve performance on some systems,
|
||||
@ -64,7 +64,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
default, the background color is determined using the
|
||||
'backgroundColor' configuration option (see
|
||||
:func:`setConfigOption <pyqtgraph.setConfigOption>`.
|
||||
============ ============================================================
|
||||
============== ============================================================
|
||||
"""
|
||||
|
||||
self.closed = False
|
||||
@ -219,8 +219,8 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
else:
|
||||
self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio)
|
||||
|
||||
self.sigRangeChanged.emit(self, self.range)
|
||||
self.sigTransformChanged.emit(self)
|
||||
self.sigDeviceRangeChanged.emit(self, self.range)
|
||||
self.sigDeviceTransformChanged.emit(self)
|
||||
|
||||
if propagate:
|
||||
for v in self.lockedViewports:
|
||||
@ -287,7 +287,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
image.setPxMode(True)
|
||||
try:
|
||||
self.sigScaleChanged.disconnect(image.setScaledMode)
|
||||
except TypeError:
|
||||
except (TypeError, RuntimeError):
|
||||
pass
|
||||
tl = image.sceneBoundingRect().topLeft()
|
||||
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))
|
||||
scale = 1.01 ** delta
|
||||
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.
|
||||
px = self.pixelSize()
|
||||
tr = -delta * px
|
||||
|
||||
self.translate(tr[0], tr[1])
|
||||
self.sigRangeChanged.emit(self, self.range)
|
||||
self.sigDeviceRangeChanged.emit(self, self.range)
|
||||
|
||||
def pixelSize(self):
|
||||
"""Return vector with the length and width of one view pixel in scene coordinates"""
|
||||
|
@ -4,28 +4,43 @@ MultiPlotWidget.py - Convenience class--GraphicsView widget displaying a MultiP
|
||||
Copyright 2010 Luke Campagnola
|
||||
Distributed under MIT/X11 license. See license.txt for more infomation.
|
||||
"""
|
||||
|
||||
from ..Qt import QtCore
|
||||
from .GraphicsView import GraphicsView
|
||||
from ..graphicsItems import MultiPlotItem as MultiPlotItem
|
||||
|
||||
__all__ = ['MultiPlotWidget']
|
||||
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):
|
||||
self.minPlotHeight = 50
|
||||
self.mPlotItem = MultiPlotItem.MultiPlotItem()
|
||||
GraphicsView.__init__(self, parent)
|
||||
self.enableMouse(False)
|
||||
self.mPlotItem = MultiPlotItem.MultiPlotItem()
|
||||
self.setCentralItem(self.mPlotItem)
|
||||
## Explicitly wrap methods from mPlotItem
|
||||
#for m in ['setData']:
|
||||
#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
|
||||
if hasattr(self.mPlotItem, attr):
|
||||
m = getattr(self.mPlotItem, attr)
|
||||
if hasattr(m, '__call__'):
|
||||
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):
|
||||
return (None, MultiPlotWidget.saveState, MultiPlotWidget.restoreState)
|
||||
@ -43,3 +58,21 @@ class MultiPlotWidget(GraphicsView):
|
||||
self.mPlotItem = None
|
||||
self.setParent(None)
|
||||
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()
|
||||
|
@ -23,8 +23,8 @@ class PathButton(QtGui.QPushButton):
|
||||
def setBrush(self, brush):
|
||||
self.brush = fn.mkBrush(brush)
|
||||
|
||||
def setPen(self, pen):
|
||||
self.pen = fn.mkPen(pen)
|
||||
def setPen(self, *args, **kwargs):
|
||||
self.pen = fn.mkPen(*args, **kwargs)
|
||||
|
||||
def setPath(self, path):
|
||||
self.path = path
|
||||
@ -48,4 +48,3 @@ class PathButton(QtGui.QPushButton):
|
||||
p.end()
|
||||
|
||||
|
||||
|
@ -12,7 +12,9 @@ from ..graphicsItems.PlotItem import *
|
||||
__all__ = ['PlotWidget']
|
||||
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
|
||||
@ -33,6 +35,7 @@ class PlotWidget(GraphicsView):
|
||||
:func:`enableAutoRange <pyqtgraph.ViewBox.enableAutoRange>`,
|
||||
:func:`disableAutoRange <pyqtgraph.ViewBox.disableAutoRange>`,
|
||||
:func:`setAspectLocked <pyqtgraph.ViewBox.setAspectLocked>`,
|
||||
:func:`setLimits <pyqtgraph.ViewBox.setLimits>`,
|
||||
:func:`register <pyqtgraph.ViewBox.register>`,
|
||||
:func:`unregister <pyqtgraph.ViewBox.unregister>`
|
||||
|
||||
@ -52,7 +55,10 @@ class PlotWidget(GraphicsView):
|
||||
self.setCentralItem(self.plotItem)
|
||||
## Explicitly wrap methods from plotItem
|
||||
## 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))
|
||||
#QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged)
|
||||
self.plotItem.sigRangeChanged.connect(self.viewRangeChanged)
|
||||
|
@ -3,6 +3,7 @@ if not USE_PYSIDE:
|
||||
import sip
|
||||
from .. import multiprocess as mp
|
||||
from .GraphicsView import GraphicsView
|
||||
from .. import CONFIG_OPTIONS
|
||||
import numpy as np
|
||||
import mmap, tempfile, ctypes, atexit, sys, random
|
||||
|
||||
@ -35,7 +36,7 @@ class RemoteGraphicsView(QtGui.QWidget):
|
||||
|
||||
self._proc = mp.QtProcess(**kwds)
|
||||
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')
|
||||
self._view = rpgRemote.Renderer(*args, **remoteKwds)
|
||||
self._view._setProxyOptions(deferGetattr=True)
|
||||
|
@ -9,7 +9,28 @@ try:
|
||||
except ImportError:
|
||||
HAVE_METAARRAY = False
|
||||
|
||||
|
||||
__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):
|
||||
"""Extends QTableWidget with some useful functions for automatic data handling
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
|
||||
self.itemClass = TableWidgetItem
|
||||
|
||||
self.setVerticalScrollMode(self.ScrollPerPixel)
|
||||
self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection)
|
||||
self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
|
||||
self.setSortingEnabled(True)
|
||||
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.addAction('Copy Selection').triggered.connect(self.copySel)
|
||||
self.contextMenu.addAction('Copy All').triggered.connect(self.copyAll)
|
||||
@ -40,6 +92,7 @@ class TableWidget(QtGui.QTableWidget):
|
||||
self.items = []
|
||||
self.setRowCount(0)
|
||||
self.setColumnCount(0)
|
||||
self.sortModes = {}
|
||||
|
||||
def setData(self, data):
|
||||
"""Set the data displayed in the table.
|
||||
@ -56,12 +109,16 @@ class TableWidget(QtGui.QTableWidget):
|
||||
self.appendData(data)
|
||||
self.resizeColumnsToContents()
|
||||
|
||||
@_defersort
|
||||
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)
|
||||
if fn0 is None:
|
||||
self.clear()
|
||||
@ -80,42 +137,88 @@ class TableWidget(QtGui.QTableWidget):
|
||||
self.setColumnCount(len(firstVals))
|
||||
|
||||
if not self.verticalHeadersSet and header0 is not None:
|
||||
self.setRowCount(len(header0))
|
||||
self.setVerticalHeaderLabels(header0)
|
||||
labels = [self.verticalHeaderItem(i).text() for i in range(self.rowCount())]
|
||||
self.setRowCount(startRow + len(header0))
|
||||
self.setVerticalHeaderLabels(labels + header0)
|
||||
self.verticalHeadersSet = True
|
||||
if not self.horizontalHeadersSet and header1 is not None:
|
||||
self.setHorizontalHeaderLabels(header1)
|
||||
self.horizontalHeadersSet = True
|
||||
|
||||
self.setRow(0, firstVals)
|
||||
i = 1
|
||||
i = startRow
|
||||
self.setRow(i, firstVals)
|
||||
for row in it0:
|
||||
self.setRow(i, [x for x in fn1(row)])
|
||||
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):
|
||||
self.editable = editable
|
||||
for item in self.items:
|
||||
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):
|
||||
## 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):
|
||||
return lambda d: d.__iter__(), None
|
||||
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')):
|
||||
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):
|
||||
header = list(map(str, data.xvals(0)))
|
||||
header = list(map(asUnicode, data.xvals(0)))
|
||||
else:
|
||||
header = None
|
||||
return self.iterFirstAxis, header
|
||||
elif isinstance(data, np.ndarray):
|
||||
return self.iterFirstAxis, None
|
||||
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:
|
||||
return (None,None)
|
||||
else:
|
||||
@ -135,20 +238,49 @@ class TableWidget(QtGui.QTableWidget):
|
||||
def appendRow(self, data):
|
||||
self.appendData([data])
|
||||
|
||||
@_defersort
|
||||
def addRow(self, vals):
|
||||
row = self.rowCount()
|
||||
self.setRowCount(row + 1)
|
||||
self.setRow(row, vals)
|
||||
|
||||
@_defersort
|
||||
def setRow(self, row, vals):
|
||||
if row > self.rowCount() - 1:
|
||||
self.setRowCount(row + 1)
|
||||
for col in range(len(vals)):
|
||||
val = vals[col]
|
||||
item = TableWidgetItem(val)
|
||||
item = self.itemClass(val, row)
|
||||
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.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):
|
||||
# based on http://stackoverflow.com/a/7195443/54056
|
||||
@ -173,7 +305,6 @@ class TableWidget(QtGui.QTableWidget):
|
||||
rows = list(range(self.rowCount()))
|
||||
columns = list(range(self.columnCount()))
|
||||
|
||||
|
||||
data = []
|
||||
if self.horizontalHeadersSet:
|
||||
row = []
|
||||
@ -223,7 +354,6 @@ class TableWidget(QtGui.QTableWidget):
|
||||
return
|
||||
open(fileName, 'w').write(data)
|
||||
|
||||
|
||||
def contextMenuEvent(self, ev):
|
||||
self.contextMenu.popup(ev.globalPos())
|
||||
|
||||
@ -234,25 +364,102 @@ class TableWidget(QtGui.QTableWidget):
|
||||
else:
|
||||
ev.ignore()
|
||||
|
||||
def handleItemChanged(self, item):
|
||||
item.textChanged()
|
||||
|
||||
|
||||
class TableWidgetItem(QtGui.QTableWidgetItem):
|
||||
def __init__(self, val):
|
||||
if isinstance(val, float) or isinstance(val, np.floating):
|
||||
s = "%0.3g" % val
|
||||
else:
|
||||
s = str(val)
|
||||
QtGui.QTableWidgetItem.__init__(self, s)
|
||||
self.value = val
|
||||
def __init__(self, val, index, format=None):
|
||||
QtGui.QTableWidgetItem.__init__(self, '')
|
||||
self._blockValueChange = False
|
||||
self._format = None
|
||||
self._defaultFormat = '%0.3g'
|
||||
self.sortMode = 'value'
|
||||
self.index = index
|
||||
flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
|
||||
self.setFlags(flags)
|
||||
self.setValue(val)
|
||||
self.setFormat(format)
|
||||
|
||||
def setEditable(self, editable):
|
||||
"""
|
||||
Set whether this item is user-editable.
|
||||
"""
|
||||
if editable:
|
||||
self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable)
|
||||
else:
|
||||
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):
|
||||
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
|
||||
else:
|
||||
return self.text() < other.text()
|
||||
|
@ -16,8 +16,8 @@ class ValueLabel(QtGui.QLabel):
|
||||
|
||||
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
|
||||
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
|
||||
@ -27,7 +27,7 @@ class ValueLabel(QtGui.QLabel):
|
||||
will be generated by calling formatStr.format(value=, avgValue=, suffix=)
|
||||
(see Python documentation on str.format)
|
||||
This option is not compatible with siPrefix
|
||||
============ ==================================================================================
|
||||
============== ==================================================================================
|
||||
"""
|
||||
QtGui.QLabel.__init__(self, parent)
|
||||
self.values = []
|
||||
|
128
widgets/tests/test_tablewidget.py
Normal file
128
widgets/tests/test_tablewidget.py
Normal 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()
|
||||
|
Loading…
Reference in New Issue
Block a user